mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +01:00 
			
		
		
		
	Compare commits
	
		
			49 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2d1bc46c04 | ||
| 
						 | 
					4bc44605fb | ||
| 
						 | 
					b868990fba | ||
| 
						 | 
					26c06c9826 | ||
| 
						 | 
					f5b89432a6 | ||
| 
						 | 
					0e7372adbf | ||
| 
						 | 
					d4fbe28517 | ||
| 
						 | 
					668528d5eb | ||
| 
						 | 
					17348a9cfe | ||
| 
						 | 
					09b610701d | ||
| 
						 | 
					71e687ad8e | ||
| 
						 | 
					171877ce08 | ||
| 
						 | 
					4f1e6ec70f | ||
| 
						 | 
					1938c317c3 | ||
| 
						 | 
					99d81059d0 | ||
| 
						 | 
					59d5a86110 | ||
| 
						 | 
					a5e56ea839 | ||
| 
						 | 
					44f85224e7 | ||
| 
						 | 
					0aa08b1c1e | ||
| 
						 | 
					406d74c4d7 | ||
| 
						 | 
					7f9a8a55ca | ||
| 
						 | 
					a42bbba0e5 | ||
| 
						 | 
					145efe67c3 | ||
| 
						 | 
					513748836e | ||
| 
						 | 
					427ce3972e | ||
| 
						 | 
					02c0f9a6cd | ||
| 
						 | 
					208771216e | ||
| 
						 | 
					385d97a9b3 | ||
| 
						 | 
					e39d1d08ac | ||
| 
						 | 
					0f106fb96f | ||
| 
						 | 
					df9acd0504 | ||
| 
						 | 
					dbe0eb3f3a | ||
| 
						 | 
					4513651e12 | ||
| 
						 | 
					3204291463 | ||
| 
						 | 
					510704a074 | ||
| 
						 | 
					f440493e45 | ||
| 
						 | 
					b897c6de13 | ||
| 
						 | 
					acbd18e8fc | ||
| 
						 | 
					ff5b84db10 | ||
| 
						 | 
					16535f6a73 | ||
| 
						 | 
					5b657ad961 | ||
| 
						 | 
					bbbc3e9dc4 | ||
| 
						 | 
					f43f0e10a1 | ||
| 
						 | 
					6d842a65a2 | ||
| 
						 | 
					50c4de021c | ||
| 
						 | 
					936d8449f6 | ||
| 
						 | 
					462bc0edd5 | ||
| 
						 | 
					35ef3c8470 | ||
| 
						 | 
					5117d43e29 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -7,3 +7,4 @@ yarn-error.log
 | 
				
			|||||||
config.ini
 | 
					config.ini
 | 
				
			||||||
cert.key
 | 
					cert.key
 | 
				
			||||||
cert.crt
 | 
					cert.crt
 | 
				
			||||||
 | 
					docs/
 | 
				
			||||||
@@ -47,7 +47,7 @@ bin/package.sh
 | 
				
			|||||||
LINUX_X64_BUILD=trilium-linux-x64-$VERSION.7z
 | 
					LINUX_X64_BUILD=trilium-linux-x64-$VERSION.7z
 | 
				
			||||||
LINUX_IA32_BUILD=trilium-linux-ia32-$VERSION.7z
 | 
					LINUX_IA32_BUILD=trilium-linux-ia32-$VERSION.7z
 | 
				
			||||||
WINDOWS_X64_BUILD=trilium-windows-x64-$VERSION.7z
 | 
					WINDOWS_X64_BUILD=trilium-windows-x64-$VERSION.7z
 | 
				
			||||||
SERVER_BUILD=trilium-linux-x64-server.elf
 | 
					SERVER_BUILD=trilium-linux-x64-server-$VERSION.7z
 | 
				
			||||||
 | 
					
 | 
				
			||||||
echo "Creating release in GitHub"
 | 
					echo "Creating release in GitHub"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,6 @@ instanceName=
 | 
				
			|||||||
port=8080
 | 
					port=8080
 | 
				
			||||||
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
 | 
					# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
 | 
				
			||||||
https=false
 | 
					https=false
 | 
				
			||||||
# path to certificate (run "bash generate-cert.sh" to generate self-signed certificate). Relevant only if https=true
 | 
					# path to certificate (run "bash bin/generate-cert.sh" to generate self-signed certificate). Relevant only if https=true
 | 
				
			||||||
certPath=
 | 
					certPath=
 | 
				
			||||||
keyPath=
 | 
					keyPath=
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,13 +12,13 @@ create table attributes
 | 
				
			|||||||
  hash         TEXT default "" not null);
 | 
					  hash         TEXT default "" not null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
create index IDX_attributes_name_value
 | 
					create index IDX_attributes_name_value
 | 
				
			||||||
  on labels (name, value);
 | 
					  on attributes (name, value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
create index IDX_attributes_value
 | 
					create index IDX_attributes_value
 | 
				
			||||||
  on labels (value);
 | 
					  on attributes (value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
create index IDX_attributes_noteId
 | 
					create index IDX_attributes_noteId
 | 
				
			||||||
  on labels (noteId);
 | 
					  on attributes (noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
INSERT INTO attributes (attributeId, noteId, type, name, value, position, dateCreated, dateModified, isDeleted, hash)
 | 
					INSERT INTO attributes (attributeId, noteId, type, name, value, position, dateCreated, dateModified, isDeleted, hash)
 | 
				
			||||||
SELECT labelId, noteId, 'label', name, value, position, dateCreated, dateModified, isDeleted, hash FROM labels;
 | 
					SELECT labelId, noteId, 'label', name, value, position, dateCreated, dateModified, isDeleted, hash FROM labels;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								db/migrations/0112__rename_inheritAttributes.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								db/migrations/0112__rename_inheritAttributes.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					UPDATE attributes SET name = 'template' WHERE name = 'inheritAttributes';
 | 
				
			||||||
@@ -36,7 +36,7 @@ async function createMainWindow() {
 | 
				
			|||||||
    win.on('closed', onClosed);
 | 
					    win.on('closed', onClosed);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    win.webContents.on('new-window', (e, url) => {
 | 
					    win.webContents.on('new-window', (e, url) => {
 | 
				
			||||||
        if (url !== mainWindow.webContents.getURL()) {
 | 
					        if (url !== win.webContents.getURL()) {
 | 
				
			||||||
            e.preventDefault();
 | 
					            e.preventDefault();
 | 
				
			||||||
            require('electron').shell.openExternal(url);
 | 
					            require('electron').shell.openExternal(url);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										3164
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3164
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										107
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										107
									
								
								package.json
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "trilium",
 | 
					  "name": "trilium",
 | 
				
			||||||
  "description": "Trilium Notes",
 | 
					  "description": "Trilium Notes",
 | 
				
			||||||
  "version": "0.19.0",
 | 
					  "version": "0.20.0",
 | 
				
			||||||
  "license": "AGPL-3.0-only",
 | 
					  "license": "AGPL-3.0-only",
 | 
				
			||||||
  "main": "electron.js",
 | 
					  "main": "electron.js",
 | 
				
			||||||
  "bin": {
 | 
					  "bin": {
 | 
				
			||||||
@@ -20,62 +20,65 @@
 | 
				
			|||||||
    "start-forge": "electron-forge start",
 | 
					    "start-forge": "electron-forge start",
 | 
				
			||||||
    "package-forge": "electron-forge package",
 | 
					    "package-forge": "electron-forge package",
 | 
				
			||||||
    "make-forge": "electron-forge make",
 | 
					    "make-forge": "electron-forge make",
 | 
				
			||||||
    "publish-forge": "electron-forge publish"
 | 
					    "publish-forge": "electron-forge publish",
 | 
				
			||||||
 | 
					    "build-backend-docs": "jsdoc -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js",
 | 
				
			||||||
 | 
					    "build-frontend-docs": "jsdoc -d ./docs/frontend_api src/public/javascripts/entities/*.js src/public/javascripts/services/frontend_script_api.js",
 | 
				
			||||||
 | 
					    "build-docs": "npm run build-backend-docs && npm run build-frontend-docs"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "async-mutex": "^0.1.3",
 | 
					    "async-mutex": "0.1.3",
 | 
				
			||||||
    "axios": "^0.18",
 | 
					    "axios": "0.18",
 | 
				
			||||||
    "body-parser": "^1.18.3",
 | 
					    "body-parser": "1.18.3",
 | 
				
			||||||
    "cls-hooked": "^4.2.2",
 | 
					    "cls-hooked": "4.2.2",
 | 
				
			||||||
    "cookie-parser": "~1.4.3",
 | 
					    "cookie-parser": "1.4.3",
 | 
				
			||||||
    "debug": "~3.1.0",
 | 
					    "debug": "3.1.0",
 | 
				
			||||||
    "devtron": "^1.4.0",
 | 
					    "devtron": "1.4.0",
 | 
				
			||||||
    "ejs": "~2.6.1",
 | 
					    "ejs": "2.6.1",
 | 
				
			||||||
    "electron-debug": "^2.0.0",
 | 
					    "electron-debug": "2.0.0",
 | 
				
			||||||
    "electron-dl": "^1.12.0",
 | 
					    "electron-dl": "1.12.0",
 | 
				
			||||||
    "electron-in-page-search": "^1.3.2",
 | 
					    "electron-in-page-search": "1.3.2",
 | 
				
			||||||
    "express": "~4.16.3",
 | 
					    "express": "4.16.3",
 | 
				
			||||||
    "express-session": "^1.15.6",
 | 
					    "express-session": "1.15.6",
 | 
				
			||||||
    "fs-extra": "^7.0.0",
 | 
					    "fs-extra": "7.0.0",
 | 
				
			||||||
    "get-port": "^4.0.0",
 | 
					    "get-port": "4.0.0",
 | 
				
			||||||
    "helmet": "^3.13.0",
 | 
					    "helmet": "3.13.0",
 | 
				
			||||||
    "html": "^1.0.0",
 | 
					    "html": "1.0.0",
 | 
				
			||||||
    "image-type": "^3.0.0",
 | 
					    "image-type": "3.0.0",
 | 
				
			||||||
    "imagemin": "^6.0.0",
 | 
					    "imagemin": "6.0.0",
 | 
				
			||||||
    "imagemin-giflossy": "^5.1.10",
 | 
					    "imagemin-giflossy": "5.1.10",
 | 
				
			||||||
    "imagemin-mozjpeg": "^7.0.0",
 | 
					    "imagemin-mozjpeg": "7.0.0",
 | 
				
			||||||
    "imagemin-pngquant": "^6.0.0",
 | 
					    "imagemin-pngquant": "6.0.0",
 | 
				
			||||||
    "ini": "^1.3.5",
 | 
					    "ini": "1.3.5",
 | 
				
			||||||
    "jimp": "^0.3.0",
 | 
					    "jimp": "0.3.5",
 | 
				
			||||||
    "moment": "^2.22.2",
 | 
					    "moment": "2.22.2",
 | 
				
			||||||
    "multer": "^1.3.1",
 | 
					    "multer": "1.3.1",
 | 
				
			||||||
    "open": "0.0.5",
 | 
					    "open": "0.0.5",
 | 
				
			||||||
    "rand-token": "^0.4.0",
 | 
					    "rand-token": "0.4.0",
 | 
				
			||||||
    "rcedit": "^1.1.0",
 | 
					    "rcedit": "1.1.0",
 | 
				
			||||||
    "request": "^2.87.0",
 | 
					    "request": "2.88.0",
 | 
				
			||||||
    "request-promise": "^4.2.2",
 | 
					    "request-promise": "4.2.2",
 | 
				
			||||||
    "rimraf": "^2.6.2",
 | 
					    "rimraf": "2.6.2",
 | 
				
			||||||
    "sanitize-filename": "^1.6.1",
 | 
					    "sanitize-filename": "1.6.1",
 | 
				
			||||||
    "scrypt": "^6.0.3",
 | 
					    "scrypt": "6.0.3",
 | 
				
			||||||
    "serve-favicon": "~2.5.0",
 | 
					    "serve-favicon": "2.5.0",
 | 
				
			||||||
    "session-file-store": "^1.2.0",
 | 
					    "session-file-store": "1.2.0",
 | 
				
			||||||
    "simple-node-logger": "^0.93.37",
 | 
					    "simple-node-logger": "0.93.37",
 | 
				
			||||||
    "sqlite": "^2.9.2",
 | 
					    "sqlite": "3.0.0",
 | 
				
			||||||
    "tar-stream": "^1.6.1",
 | 
					    "tar-stream": "1.6.1",
 | 
				
			||||||
    "unescape": "^1.0.1",
 | 
					    "unescape": "1.0.1",
 | 
				
			||||||
    "ws": "^6.0.0",
 | 
					    "ws": "6.0.0",
 | 
				
			||||||
    "xml2js": "^0.4.19"
 | 
					    "xml2js": "0.4.19"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "electron": "^2.0.6",
 | 
					    "electron": "2.0.7",
 | 
				
			||||||
    "electron-compile": "^6.4.3",
 | 
					    "electron-compile": "6.4.3",
 | 
				
			||||||
    "electron-packager": "^12.1.0",
 | 
					    "electron-packager": "12.1.1",
 | 
				
			||||||
    "electron-prebuilt-compile": "2.0.6",
 | 
					    "electron-prebuilt-compile": "2.0.7",
 | 
				
			||||||
    "electron-rebuild": "^1.8.2",
 | 
					    "electron-rebuild": "1.8.2",
 | 
				
			||||||
    "lorem-ipsum": "^1.0.5",
 | 
					    "lorem-ipsum": "1.0.5",
 | 
				
			||||||
    "tape": "^4.9.1",
 | 
					    "tape": "4.9.1",
 | 
				
			||||||
    "xo": "^0.21.1",
 | 
					    "xo": "0.22.0",
 | 
				
			||||||
    "pkg": "^4.3.3"
 | 
					    "pkg": "4.3.4"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "config": {
 | 
					  "config": {
 | 
				
			||||||
    "forge": {
 | 
					    "forge": {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,8 +3,18 @@
 | 
				
			|||||||
const Entity = require('./entity');
 | 
					const Entity = require('./entity');
 | 
				
			||||||
const dateUtils = require('../services/date_utils');
 | 
					const dateUtils = require('../services/date_utils');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * ApiToken is an entity representing token used to authenticate against Trilium API from client applications. Currently used only by Trilium Sender.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {string} apiTokenId - primary key
 | 
				
			||||||
 | 
					 * @param {string} token
 | 
				
			||||||
 | 
					 * @param {boolean} isDeleted - true if API token is deleted
 | 
				
			||||||
 | 
					 * @param {string} dateCreated
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @extends Entity
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
class ApiToken extends Entity {
 | 
					class ApiToken extends Entity {
 | 
				
			||||||
    static get tableName() { return "api_tokens"; }
 | 
					    static get entityName() { return "api_tokens"; }
 | 
				
			||||||
    static get primaryKeyName() { return "apiTokenId"; }
 | 
					    static get primaryKeyName() { return "apiTokenId"; }
 | 
				
			||||||
    static get hashedProperties() { return ["apiTokenId", "token", "dateCreated", "isDeleted"]; }
 | 
					    static get hashedProperties() { return ["apiTokenId", "token", "dateCreated", "isDeleted"]; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,8 +5,24 @@ const repository = require('../services/repository');
 | 
				
			|||||||
const dateUtils = require('../services/date_utils');
 | 
					const dateUtils = require('../services/date_utils');
 | 
				
			||||||
const sql = require('../services/sql');
 | 
					const sql = require('../services/sql');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Attribute is key value pair owned by a note.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {string} attributeId
 | 
				
			||||||
 | 
					 * @param {string} noteId
 | 
				
			||||||
 | 
					 * @param {string} type
 | 
				
			||||||
 | 
					 * @param {string} name
 | 
				
			||||||
 | 
					 * @param {string} value
 | 
				
			||||||
 | 
					 * @param {int} position
 | 
				
			||||||
 | 
					 * @param {boolean} isInheritable
 | 
				
			||||||
 | 
					 * @param {boolean} isDeleted
 | 
				
			||||||
 | 
					 * @param {string} dateCreated
 | 
				
			||||||
 | 
					 * @param {string} dateModified
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @extends Entity
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
class Attribute extends Entity {
 | 
					class Attribute extends Entity {
 | 
				
			||||||
    static get tableName() { return "attributes"; }
 | 
					    static get entityName() { return "attributes"; }
 | 
				
			||||||
    static get primaryKeyName() { return "attributeId"; }
 | 
					    static get primaryKeyName() { return "attributeId"; }
 | 
				
			||||||
    static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "isInheritable", "isDeleted", "dateCreated"]; }
 | 
					    static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "isInheritable", "isDeleted", "dateCreated"]; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,8 +5,24 @@ const dateUtils = require('../services/date_utils');
 | 
				
			|||||||
const repository = require('../services/repository');
 | 
					const repository = require('../services/repository');
 | 
				
			||||||
const sql = require('../services/sql');
 | 
					const sql = require('../services/sql');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Branch represents note's placement in the tree - it's essentially pair of noteId and parentNoteId.
 | 
				
			||||||
 | 
					 * Each note can have multiple (at least one) branches, meaning it can be placed into multiple places in the tree.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {string} branchId - primary key
 | 
				
			||||||
 | 
					 * @param {string} noteId
 | 
				
			||||||
 | 
					 * @param {string} parentNoteId
 | 
				
			||||||
 | 
					 * @param {int} notePosition
 | 
				
			||||||
 | 
					 * @param {string} prefix
 | 
				
			||||||
 | 
					 * @param {boolean} isExpanded
 | 
				
			||||||
 | 
					 * @param {boolean} isDeleted
 | 
				
			||||||
 | 
					 * @param {string} dateModified
 | 
				
			||||||
 | 
					 * @param {string} dateCreated
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @extends Entity
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
class Branch extends Entity {
 | 
					class Branch extends Entity {
 | 
				
			||||||
    static get tableName() { return "branches"; }
 | 
					    static get entityName() { return "branches"; }
 | 
				
			||||||
    static get primaryKeyName() { return "branchId"; }
 | 
					    static get primaryKeyName() { return "branchId"; }
 | 
				
			||||||
    // notePosition is not part of hash because it would produce a lot of updates in case of reordering
 | 
					    // notePosition is not part of hash because it would produce a lot of updates in case of reordering
 | 
				
			||||||
    static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "isDeleted", "prefix"]; }
 | 
					    static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "isDeleted", "prefix"]; }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,9 @@
 | 
				
			|||||||
const utils = require('../services/utils');
 | 
					const utils = require('../services/utils');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Entity {
 | 
					class Entity {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {object} [row] - database row representing given entity
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    constructor(row = {}) {
 | 
					    constructor(row = {}) {
 | 
				
			||||||
        for (const key in row) {
 | 
					        for (const key in row) {
 | 
				
			||||||
            this[key] = row[key];
 | 
					            this[key] = row[key];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@ const ApiToken = require('../entities/api_token');
 | 
				
			|||||||
const Option = require('../entities/option');
 | 
					const Option = require('../entities/option');
 | 
				
			||||||
const repository = require('../services/repository');
 | 
					const repository = require('../services/repository');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TABLE_NAME_TO_ENTITY = {
 | 
					const ENTITY_NAME_TO_ENTITY = {
 | 
				
			||||||
    "attributes": Attribute,
 | 
					    "attributes": Attribute,
 | 
				
			||||||
    "images": Image,
 | 
					    "images": Image,
 | 
				
			||||||
    "note_images": NoteImage,
 | 
					    "note_images": NoteImage,
 | 
				
			||||||
@@ -21,12 +21,12 @@ const TABLE_NAME_TO_ENTITY = {
 | 
				
			|||||||
    "api_tokens": ApiToken
 | 
					    "api_tokens": ApiToken
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getEntityFromTableName(tableName) {
 | 
					function getEntityFromEntityName(entityName) {
 | 
				
			||||||
    if (!(tableName in TABLE_NAME_TO_ENTITY)) {
 | 
					    if (!(entityName in ENTITY_NAME_TO_ENTITY)) {
 | 
				
			||||||
        throw new Error(`Entity for table ${tableName} not found!`);
 | 
					        throw new Error(`Entity for table ${entityName} not found!`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return TABLE_NAME_TO_ENTITY[tableName];
 | 
					    return ENTITY_NAME_TO_ENTITY[entityName];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function createEntityFromRow(row) {
 | 
					function createEntityFromRow(row) {
 | 
				
			||||||
@@ -68,7 +68,7 @@ function createEntityFromRow(row) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    createEntityFromRow,
 | 
					    createEntityFromRow,
 | 
				
			||||||
    getEntityFromTableName
 | 
					    getEntityFromEntityName
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
repository.setEntityConstructor(module.exports);
 | 
					repository.setEntityConstructor(module.exports);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,8 +3,22 @@
 | 
				
			|||||||
const Entity = require('./entity');
 | 
					const Entity = require('./entity');
 | 
				
			||||||
const dateUtils = require('../services/date_utils');
 | 
					const dateUtils = require('../services/date_utils');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This class represents image data.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {string} imageId
 | 
				
			||||||
 | 
					 * @param {string} format
 | 
				
			||||||
 | 
					 * @param {string} checksum
 | 
				
			||||||
 | 
					 * @param {string} name
 | 
				
			||||||
 | 
					 * @param {blob} data
 | 
				
			||||||
 | 
					 * @param {boolean} isDeleted
 | 
				
			||||||
 | 
					 * @param {string} dateModified
 | 
				
			||||||
 | 
					 * @param {string} dateCreated
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @extends Entity
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
class Image extends Entity {
 | 
					class Image extends Entity {
 | 
				
			||||||
    static get tableName() { return "images"; }
 | 
					    static get entityName() { return "images"; }
 | 
				
			||||||
    static get primaryKeyName() { return "imageId"; }
 | 
					    static get primaryKeyName() { return "imageId"; }
 | 
				
			||||||
    static get hashedProperties() { return ["imageId", "format", "checksum", "name", "isDeleted", "dateCreated"]; }
 | 
					    static get hashedProperties() { return ["imageId", "format", "checksum", "name", "isDeleted", "dateCreated"]; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,11 +6,32 @@ const protectedSessionService = require('../services/protected_session');
 | 
				
			|||||||
const repository = require('../services/repository');
 | 
					const repository = require('../services/repository');
 | 
				
			||||||
const dateUtils = require('../services/date_utils');
 | 
					const dateUtils = require('../services/date_utils');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const LABEL = 'label';
 | 
				
			||||||
 | 
					const RELATION = 'relation';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This represents a Note which is a central object in the Trilium Notes project.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @property {string} noteId - primary key
 | 
				
			||||||
 | 
					 * @property {string} type - one of "text", "code", "file" or "render"
 | 
				
			||||||
 | 
					 * @property {string} mime - MIME type, e.g. "text/html"
 | 
				
			||||||
 | 
					 * @property {string} title - note title
 | 
				
			||||||
 | 
					 * @property {string} content - note content - e.g. HTML text for text notes, file payload for files
 | 
				
			||||||
 | 
					 * @property {boolean} isProtected - true if note is protected
 | 
				
			||||||
 | 
					 * @property {boolean} isDeleted - true if note is deleted
 | 
				
			||||||
 | 
					 * @property {string} dateCreated
 | 
				
			||||||
 | 
					 * @property {string} dateModified
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @extends Entity
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
class Note extends Entity {
 | 
					class Note extends Entity {
 | 
				
			||||||
    static get tableName() { return "notes"; }
 | 
					    static get entityName() { return "notes"; }
 | 
				
			||||||
    static get primaryKeyName() { return "noteId"; }
 | 
					    static get primaryKeyName() { return "noteId"; }
 | 
				
			||||||
    static get hashedProperties() { return ["noteId", "title", "content", "type", "isProtected", "isDeleted"]; }
 | 
					    static get hashedProperties() { return ["noteId", "title", "content", "type", "isProtected", "isDeleted"]; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param row - object containing database row from "notes" table
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    constructor(row) {
 | 
					    constructor(row) {
 | 
				
			||||||
        super(row);
 | 
					        super(row);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -33,23 +54,28 @@ class Note extends Entity {
 | 
				
			|||||||
        catch(e) {}
 | 
					        catch(e) {}
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */
 | 
				
			||||||
    isRoot() {
 | 
					    isRoot() {
 | 
				
			||||||
        return this.noteId === 'root';
 | 
					        return this.noteId === 'root';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {boolean} true if this note is of application/json content type */
 | 
				
			||||||
    isJson() {
 | 
					    isJson() {
 | 
				
			||||||
        return this.mime === "application/json";
 | 
					        return this.mime === "application/json";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {boolean} true if this note is JavaScript (code or attachment) */
 | 
				
			||||||
    isJavaScript() {
 | 
					    isJavaScript() {
 | 
				
			||||||
        return (this.type === "code" || this.type === "file")
 | 
					        return (this.type === "code" || this.type === "file")
 | 
				
			||||||
            && (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript");
 | 
					            && (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {boolean} true if this note is HTML */
 | 
				
			||||||
    isHtml() {
 | 
					    isHtml() {
 | 
				
			||||||
        return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html";
 | 
					        return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {string} JS script environment - either "frontend" or "backend" */
 | 
				
			||||||
    getScriptEnv() {
 | 
					    getScriptEnv() {
 | 
				
			||||||
        if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
 | 
					        if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
 | 
				
			||||||
            return "frontend";
 | 
					            return "frontend";
 | 
				
			||||||
@@ -66,10 +92,14 @@ class Note extends Entity {
 | 
				
			|||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @returns {Promise<Attribute[]>} attributes belonging to this specific note (excludes inherited attributes)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    async getOwnedAttributes() {
 | 
					    async getOwnedAttributes() {
 | 
				
			||||||
        return await repository.getEntities(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`, [this.noteId]);
 | 
					        return await repository.getEntities(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`, [this.noteId]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {Promise<Attribute[]>} all note's attributes, including inherited ones */
 | 
				
			||||||
    async getAttributes() {
 | 
					    async getAttributes() {
 | 
				
			||||||
        if (!this.__attributeCache) {
 | 
					        if (!this.__attributeCache) {
 | 
				
			||||||
            await this.loadAttributesToCache();
 | 
					            await this.loadAttributesToCache();
 | 
				
			||||||
@@ -78,10 +108,25 @@ class Note extends Entity {
 | 
				
			|||||||
        return this.__attributeCache;
 | 
					        return this.__attributeCache;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {Promise<Attribute[]>} all note's labels (attributes with type label), including inherited ones */
 | 
				
			||||||
 | 
					    async getLabels() {
 | 
				
			||||||
 | 
					        return (await this.getAttributes()).filter(attr => attr.type === LABEL);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), including inherited ones */
 | 
				
			||||||
 | 
					    async getRelations() {
 | 
				
			||||||
 | 
					        return (await this.getAttributes()).filter(attr => attr.type === RELATION);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Clear note's attributes cache to force fresh reload for next attribute request.
 | 
				
			||||||
 | 
					     * Cache is note instance scoped.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    invalidateAttributeCache() {
 | 
					    invalidateAttributeCache() {
 | 
				
			||||||
        this.__attributeCache = null;
 | 
					        this.__attributeCache = null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {Promise<void>} */
 | 
				
			||||||
    async loadAttributesToCache() {
 | 
					    async loadAttributesToCache() {
 | 
				
			||||||
        const attributes = await repository.getEntities(`
 | 
					        const attributes = await repository.getEntities(`
 | 
				
			||||||
            WITH RECURSIVE
 | 
					            WITH RECURSIVE
 | 
				
			||||||
@@ -101,7 +146,7 @@ class Note extends Entity {
 | 
				
			|||||||
                     JOIN treeWithAttrs ON treeWithAttrs.noteId = attributes.noteId
 | 
					                     JOIN treeWithAttrs ON treeWithAttrs.noteId = attributes.noteId
 | 
				
			||||||
                WHERE attributes.isDeleted = 0
 | 
					                WHERE attributes.isDeleted = 0
 | 
				
			||||||
                  AND attributes.type = 'relation'
 | 
					                  AND attributes.type = 'relation'
 | 
				
			||||||
                  AND attributes.name = 'inheritAttributes'
 | 
					                  AND attributes.name = 'template'
 | 
				
			||||||
                  AND (attributes.noteId = ? OR attributes.isInheritable = 1)
 | 
					                  AND (attributes.noteId = ? OR attributes.isInheritable = 1)
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            SELECT attributes.* FROM attributes JOIN treeWithAttrs ON attributes.noteId = treeWithAttrs.noteId
 | 
					            SELECT attributes.* FROM attributes JOIN treeWithAttrs ON attributes.noteId = treeWithAttrs.noteId
 | 
				
			||||||
@@ -145,55 +190,94 @@ class Note extends Entity {
 | 
				
			|||||||
        this.__attributeCache = filteredAttributes;
 | 
					        this.__attributeCache = filteredAttributes;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async hasLabel(name) {
 | 
					    /**
 | 
				
			||||||
        return !!await this.getLabel(name);
 | 
					     * @param {string} type - attribute type (label, relation, etc.)
 | 
				
			||||||
 | 
					     * @param {string} name - attribute name
 | 
				
			||||||
 | 
					     * @returns {Promise<boolean>} true if note has an attribute with given type and name (including inherited)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async hasAttribute(type, name) {
 | 
				
			||||||
 | 
					        return !!await this.getAttribute(type, name);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // WARNING: this doesn't take into account the possibility to have multi-valued labels!
 | 
					    /**
 | 
				
			||||||
    async getLabel(name) {
 | 
					     * @param {string} type - attribute type (label, relation, etc.)
 | 
				
			||||||
 | 
					     * @param {string} name - attribute name
 | 
				
			||||||
 | 
					     * @returns {Promise<Attribute>} attribute of given type and name. If there's more such attributes, first is  returned. Returns null if there's no such attribute belonging to this note.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async getAttribute(type, name) {
 | 
				
			||||||
        const attributes = await this.getAttributes();
 | 
					        const attributes = await this.getAttributes();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return attributes.find(attr => attr.type === 'label' && attr.name === name);
 | 
					        return attributes.find(attr => attr.type === type && attr.name === name);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getLabelValue(name) {
 | 
					    /**
 | 
				
			||||||
        const label = await this.getLabel(name);
 | 
					     * @param {string} type - attribute type (label, relation, etc.)
 | 
				
			||||||
 | 
					     * @param {string} name - attribute name
 | 
				
			||||||
 | 
					     * @returns {Promise<string>} attribute value of given type and name or null if no such attribute exists.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async getAttributeValue(type, name) {
 | 
				
			||||||
 | 
					        const attr = await this.getAttribute(type, name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return label ? label.value : null;
 | 
					        return attr ? attr.value : null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async toggleLabel(enabled, name, value = "") {
 | 
					    /**
 | 
				
			||||||
 | 
					     * Based on enabled, attribute is either set or removed.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {string} type - attribute type ('relation', 'label' etc.)
 | 
				
			||||||
 | 
					     * @param {boolean} enabled - toggle On or Off
 | 
				
			||||||
 | 
					     * @param {string} name - attribute name
 | 
				
			||||||
 | 
					     * @param {string} [value] - attribute value (optional)
 | 
				
			||||||
 | 
					     * @returns {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async toggleAttribute(type, enabled, name, value) {
 | 
				
			||||||
        if (enabled) {
 | 
					        if (enabled) {
 | 
				
			||||||
            await this.setLabel(name, value);
 | 
					            await this.setAttribute(type, name, value);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
            await this.removeLabel(name, value);
 | 
					            await this.removeAttribute(type, name, value);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async setLabel(name, value = "") {
 | 
					    /**
 | 
				
			||||||
 | 
					     * Creates given attribute name-value pair if it doesn't exist.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {string} type - attribute type (label, relation, etc.)
 | 
				
			||||||
 | 
					     * @param {string} name - attribute name
 | 
				
			||||||
 | 
					     * @param {string} [value] - attribute value (optional)
 | 
				
			||||||
 | 
					     * @returns {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async setAttribute(type, name, value) {
 | 
				
			||||||
        const attributes = await this.getOwnedAttributes();
 | 
					        const attributes = await this.getOwnedAttributes();
 | 
				
			||||||
        let label = attributes.find(attr => attr.type === 'label' && attr.value === value);
 | 
					        let attr = attributes.find(attr => attr.type === type && (value === undefined || attr.value === value));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!label) {
 | 
					        if (!attr) {
 | 
				
			||||||
            label = new Attribute({
 | 
					            attr = new Attribute({
 | 
				
			||||||
                noteId: this.noteId,
 | 
					                noteId: this.noteId,
 | 
				
			||||||
                type: 'label',
 | 
					                type: type,
 | 
				
			||||||
                name: name,
 | 
					                name: name,
 | 
				
			||||||
                value: value
 | 
					                value: value !== undefined ? value : ""
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await label.save();
 | 
					            await attr.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            this.invalidateAttributeCache();
 | 
					            this.invalidateAttributeCache();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async removeLabel(name, value = "") {
 | 
					    /**
 | 
				
			||||||
 | 
					     * Removes given attribute name-value pair if it exists.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {string} type - attribute type (label, relation, etc.)
 | 
				
			||||||
 | 
					     * @param {string} name - attribute name
 | 
				
			||||||
 | 
					     * @param {string} [value] - attribute value (optional)
 | 
				
			||||||
 | 
					     * @returns {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async removeAttribute(type, name, value) {
 | 
				
			||||||
        const attributes = await this.getOwnedAttributes();
 | 
					        const attributes = await this.getOwnedAttributes();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const attribute of attributes) {
 | 
					        for (const attribute of attributes) {
 | 
				
			||||||
            if (attribute.type === 'label' && (!value || value === attribute.value)) {
 | 
					            if (attribute.type === type && (value === undefined || value === attribute.value)) {
 | 
				
			||||||
                attribute.isDeleted = true;
 | 
					                attribute.isDeleted = true;
 | 
				
			||||||
                await attribute.save();
 | 
					                await attribute.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -202,29 +286,191 @@ class Note extends Entity {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {string} name - label name
 | 
				
			||||||
 | 
					     * @returns {Promise<boolean>} true if label exists (including inherited)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async hasLabel(name) { return await this.hasAttribute(LABEL, name); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {string} name - relation name
 | 
				
			||||||
 | 
					     * @returns {Promise<boolean>} true if relation exists (including inherited)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async hasRelation(name) { return await this.hasAttribute(RELATION, name); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {string} name - label name
 | 
				
			||||||
 | 
					     * @returns {Promise<Attribute>} label if it exists, null otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async getLabel(name) { return await this.getAttribute(LABEL, name); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {string} name - relation name
 | 
				
			||||||
 | 
					     * @returns {Promise<Attribute>} relation if it exists, null otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async getRelation(name) { return await this.getAttribute(RELATION, name); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {string} name - label name
 | 
				
			||||||
 | 
					     * @returns {Promise<string>} label value if label exists, null otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async getLabelValue(name) { return await this.getAttributeValue(LABEL, name); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {string} name - relation name
 | 
				
			||||||
 | 
					     * @returns {Promise<string>} relation value if relation exists, null otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async getRelationValue(name) { return await this.getAttributeValue(RELATION, name); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Based on enabled, label is either set or removed.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {boolean} enabled - toggle On or Off
 | 
				
			||||||
 | 
					     * @param {string} name - label name
 | 
				
			||||||
 | 
					     * @param {string} [value] - label value (optional)
 | 
				
			||||||
 | 
					     * @returns {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async toggleLabel(enabled, name, value) { return await this.toggleAttribute(LABEL, enabled, name, value); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Based on enabled, relation is either set or removed.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {boolean} enabled - toggle On or Off
 | 
				
			||||||
 | 
					     * @param {string} name - relation name
 | 
				
			||||||
 | 
					     * @param {string} [value] - relation value (noteId)
 | 
				
			||||||
 | 
					     * @returns {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async toggleRelation(enabled, name, value) { return await this.toggleAttribute(RELATION, enabled, name, value); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Create label name-value pair if it doesn't exist yet.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {string} name - label name
 | 
				
			||||||
 | 
					     * @param {string} [value] - label value
 | 
				
			||||||
 | 
					     * @returns {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async setLabel(name, value) { return await this.setAttribute(LABEL, name, value); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Create relation name-value pair if it doesn't exist yet.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {string} name - relation name
 | 
				
			||||||
 | 
					     * @param {string} [value] - relation value (noteId)
 | 
				
			||||||
 | 
					     * @returns {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async setRelation(name, value) { return await this.setAttribute(RELATION, name, value); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Remove label name-value pair, if it exists.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {string} name - label name
 | 
				
			||||||
 | 
					     * @param {string} [value] - label value
 | 
				
			||||||
 | 
					     * @returns {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async removeLabel(name, value) { return await this.removeAttribute(LABEL, name, value); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Remove relation name-value pair, if it exists.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {string} name - relation name
 | 
				
			||||||
 | 
					     * @param {string} [value] - relation value (noteId)
 | 
				
			||||||
 | 
					     * @returns {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async removeRelation(name, value) { return await this.removeAttribute(RELATION, name, value); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {string} name
 | 
				
			||||||
 | 
					     * @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async getRelationTarget(name) {
 | 
				
			||||||
 | 
					        const relation = await this.getRelation(name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return relation ? await repository.getNote(relation.value) : null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Finds notes with given attribute name and value. Only own attributes are considered, not inherited ones
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {string} type - attribute type (label, relation, etc.)
 | 
				
			||||||
 | 
					     * @param {string} name - attribute name
 | 
				
			||||||
 | 
					     * @param {string} [value] - attribute value
 | 
				
			||||||
 | 
					     * @returns {Promise<Note[]>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async findNotesWithAttribute(type, name, value) {
 | 
				
			||||||
 | 
					        const params = [this.noteId, name];
 | 
				
			||||||
 | 
					        let valueCondition = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (value !== undefined) {
 | 
				
			||||||
 | 
					            params.push(value);
 | 
				
			||||||
 | 
					            valueCondition = " AND attributes.value = ?";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const notes = await repository.getEntities(`
 | 
				
			||||||
 | 
					            WITH RECURSIVE
 | 
				
			||||||
 | 
					            tree(noteId) AS (
 | 
				
			||||||
 | 
					                SELECT ?
 | 
				
			||||||
 | 
					                UNION
 | 
				
			||||||
 | 
					                SELECT branches.noteId FROM branches
 | 
				
			||||||
 | 
					                    JOIN tree ON branches.parentNoteId = tree.noteId
 | 
				
			||||||
 | 
					                    JOIN notes ON notes.noteId = branches.noteId
 | 
				
			||||||
 | 
					                WHERE notes.isDeleted = 0
 | 
				
			||||||
 | 
					                  AND branches.isDeleted = 0
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            SELECT notes.* FROM notes 
 | 
				
			||||||
 | 
					            JOIN tree ON tree.noteId = notes.noteId
 | 
				
			||||||
 | 
					            JOIN attributes ON attributes.noteId = notes.noteId
 | 
				
			||||||
 | 
					            WHERE attributes.isDeleted = 0 
 | 
				
			||||||
 | 
					              AND attributes.name = ?
 | 
				
			||||||
 | 
					              ${valueCondition} 
 | 
				
			||||||
 | 
					            ORDER BY noteId, position`, params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return notes;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Finds notes with given label name and value. Only own labels are considered, not inherited ones
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {string} name - label name
 | 
				
			||||||
 | 
					     * @param {string} [value] - label value
 | 
				
			||||||
 | 
					     * @returns {Promise<Note[]>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async findNotesWithLabel(name, value) { return await this.findNotesWithAttribute(LABEL, name, value); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Finds notes with given relation name and value. Only own relations are considered, not inherited ones
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {string} name - relation name
 | 
				
			||||||
 | 
					     * @param {string} [value] - relation value
 | 
				
			||||||
 | 
					     * @returns {Promise<Note[]>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async findNotesWithRelation(name, value) { return await this.findNotesWithAttribute(RELATION, name, value); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns note revisions of this note.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns {Promise<NoteRevision[]>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    async getRevisions() {
 | 
					    async getRevisions() {
 | 
				
			||||||
        return await repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]);
 | 
					        return await repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @returns {Promise<NoteImage[]>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    async getNoteImages() {
 | 
					    async getNoteImages() {
 | 
				
			||||||
        return await repository.getEntities("SELECT * FROM note_images WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
 | 
					        return await repository.getEntities("SELECT * FROM note_images WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @returns {Promise<Branch[]>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    async getBranches() {
 | 
					    async getBranches() {
 | 
				
			||||||
        return await repository.getEntities("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
 | 
					        return await repository.getEntities("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getChildNote(name) {
 | 
					    /**
 | 
				
			||||||
        return await repository.getEntity(`
 | 
					     * @returns {Promise<Note[]>} child notes of this note
 | 
				
			||||||
          SELECT notes.* 
 | 
					     */
 | 
				
			||||||
          FROM branches 
 | 
					 | 
				
			||||||
            JOIN notes USING(noteId) 
 | 
					 | 
				
			||||||
          WHERE notes.isDeleted = 0
 | 
					 | 
				
			||||||
                AND branches.isDeleted = 0
 | 
					 | 
				
			||||||
                AND branches.parentNoteId = ?
 | 
					 | 
				
			||||||
                AND notes.title = ?`, [this.noteId, name]);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async getChildNotes() {
 | 
					    async getChildNotes() {
 | 
				
			||||||
        return await repository.getEntities(`
 | 
					        return await repository.getEntities(`
 | 
				
			||||||
          SELECT notes.* 
 | 
					          SELECT notes.* 
 | 
				
			||||||
@@ -236,6 +482,9 @@ class Note extends Entity {
 | 
				
			|||||||
          ORDER BY branches.notePosition`, [this.noteId]);
 | 
					          ORDER BY branches.notePosition`, [this.noteId]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @returns {Promise<Branch[]>} child branches of this note
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    async getChildBranches() {
 | 
					    async getChildBranches() {
 | 
				
			||||||
        return await repository.getEntities(`
 | 
					        return await repository.getEntities(`
 | 
				
			||||||
          SELECT branches.* 
 | 
					          SELECT branches.* 
 | 
				
			||||||
@@ -245,6 +494,9 @@ class Note extends Entity {
 | 
				
			|||||||
          ORDER BY branches.notePosition`, [this.noteId]);
 | 
					          ORDER BY branches.notePosition`, [this.noteId]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @returns {Promise<Note[]>} parent notes of this note (note can have multiple parents because of cloning)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    async getParentNotes() {
 | 
					    async getParentNotes() {
 | 
				
			||||||
        return await repository.getEntities(`
 | 
					        return await repository.getEntities(`
 | 
				
			||||||
          SELECT parent_notes.* 
 | 
					          SELECT parent_notes.* 
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,8 +4,20 @@ const Entity = require('./entity');
 | 
				
			|||||||
const repository = require('../services/repository');
 | 
					const repository = require('../services/repository');
 | 
				
			||||||
const dateUtils = require('../services/date_utils');
 | 
					const dateUtils = require('../services/date_utils');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This class represents image's placement in the note(s). One image may be placed into several notes.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {string} noteImageId
 | 
				
			||||||
 | 
					 * @param {string} noteId
 | 
				
			||||||
 | 
					 * @param {string} imageId
 | 
				
			||||||
 | 
					 * @param {boolean} isDeleted
 | 
				
			||||||
 | 
					 * @param {string} dateModified
 | 
				
			||||||
 | 
					 * @param {string} dateCreated
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @extends Entity
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
class NoteImage extends Entity {
 | 
					class NoteImage extends Entity {
 | 
				
			||||||
    static get tableName() { return "note_images"; }
 | 
					    static get entityName() { return "note_images"; }
 | 
				
			||||||
    static get primaryKeyName() { return "noteImageId"; }
 | 
					    static get primaryKeyName() { return "noteImageId"; }
 | 
				
			||||||
    static get hashedProperties() { return ["noteImageId", "noteId", "imageId", "isDeleted", "dateCreated"]; }
 | 
					    static get hashedProperties() { return ["noteImageId", "noteId", "imageId", "isDeleted", "dateCreated"]; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,8 +4,23 @@ const Entity = require('./entity');
 | 
				
			|||||||
const protectedSessionService = require('../services/protected_session');
 | 
					const protectedSessionService = require('../services/protected_session');
 | 
				
			||||||
const repository = require('../services/repository');
 | 
					const repository = require('../services/repository');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * NoteRevision represents snapshot of note's title and content at some point in the past. It's used for seamless note versioning.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {string} noteRevisionId
 | 
				
			||||||
 | 
					 * @param {string} noteId
 | 
				
			||||||
 | 
					 * @param {string} type
 | 
				
			||||||
 | 
					 * @param {string} mime
 | 
				
			||||||
 | 
					 * @param {string} title
 | 
				
			||||||
 | 
					 * @param {string} content
 | 
				
			||||||
 | 
					 * @param {string} isProtected
 | 
				
			||||||
 | 
					 * @param {string} dateModifiedFrom
 | 
				
			||||||
 | 
					 * @param {string} dateModifiedTo
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @extends Entity
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
class NoteRevision extends Entity {
 | 
					class NoteRevision extends Entity {
 | 
				
			||||||
    static get tableName() { return "note_revisions"; }
 | 
					    static get entityName() { return "note_revisions"; }
 | 
				
			||||||
    static get primaryKeyName() { return "noteRevisionId"; }
 | 
					    static get primaryKeyName() { return "noteRevisionId"; }
 | 
				
			||||||
    static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "isProtected", "dateModifiedFrom", "dateModifiedTo"]; }
 | 
					    static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "isProtected", "dateModifiedFrom", "dateModifiedTo"]; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,8 +3,19 @@
 | 
				
			|||||||
const Entity = require('./entity');
 | 
					const Entity = require('./entity');
 | 
				
			||||||
const dateUtils = require('../services/date_utils');
 | 
					const dateUtils = require('../services/date_utils');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Option represents name-value pair, either directly configurable by the user or some system property.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {string} name
 | 
				
			||||||
 | 
					 * @param {string} value
 | 
				
			||||||
 | 
					 * @param {boolean} isSynced
 | 
				
			||||||
 | 
					 * @param {string} dateModified
 | 
				
			||||||
 | 
					 * @param {string} dateCreated
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @extends Entity
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
class Option extends Entity {
 | 
					class Option extends Entity {
 | 
				
			||||||
    static get tableName() { return "options"; }
 | 
					    static get entityName() { return "options"; }
 | 
				
			||||||
    static get primaryKeyName() { return "name"; }
 | 
					    static get primaryKeyName() { return "name"; }
 | 
				
			||||||
    static get hashedProperties() { return ["name", "value"]; }
 | 
					    static get hashedProperties() { return ["name", "value"]; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,8 +3,18 @@
 | 
				
			|||||||
const Entity = require('./entity');
 | 
					const Entity = require('./entity');
 | 
				
			||||||
const dateUtils = require('../services/date_utils');
 | 
					const dateUtils = require('../services/date_utils');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * RecentNote represents recently visited note.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {string} branchId
 | 
				
			||||||
 | 
					 * @param {string} notePath
 | 
				
			||||||
 | 
					 * @param {boolean} isDeleted
 | 
				
			||||||
 | 
					 * @param {string} dateModified
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @extends Entity
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
class RecentNote extends Entity {
 | 
					class RecentNote extends Entity {
 | 
				
			||||||
    static get tableName() { return "recent_notes"; }
 | 
					    static get entityName() { return "recent_notes"; }
 | 
				
			||||||
    static get primaryKeyName() { return "branchId"; }
 | 
					    static get primaryKeyName() { return "branchId"; }
 | 
				
			||||||
    static get hashedProperties() { return ["branchId", "notePath", "dateCreated", "isDeleted"]; }
 | 
					    static get hashedProperties() { return ["branchId", "notePath", "dateCreated", "isDeleted"]; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								src/public/images/shield.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/public/images/shield.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fafafa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shield"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 274 B  | 
@@ -2,8 +2,8 @@ import cloningService from '../services/cloning.js';
 | 
				
			|||||||
import linkService from '../services/link.js';
 | 
					import linkService from '../services/link.js';
 | 
				
			||||||
import noteDetailService from '../services/note_detail.js';
 | 
					import noteDetailService from '../services/note_detail.js';
 | 
				
			||||||
import treeUtils from '../services/tree_utils.js';
 | 
					import treeUtils from '../services/tree_utils.js';
 | 
				
			||||||
import server from "../services/server.js";
 | 
					 | 
				
			||||||
import noteDetailText from "../services/note_detail_text.js";
 | 
					import noteDetailText from "../services/note_detail_text.js";
 | 
				
			||||||
 | 
					import noteAutocompleteService from "../services/note_autocomplete.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const $dialog = $("#add-link-dialog");
 | 
					const $dialog = $("#add-link-dialog");
 | 
				
			||||||
const $form = $("#add-link-form");
 | 
					const $form = $("#add-link-form");
 | 
				
			||||||
@@ -15,7 +15,7 @@ const $prefixFormGroup = $("#add-link-prefix-form-group");
 | 
				
			|||||||
const $linkTypeDiv = $("#add-link-type-div");
 | 
					const $linkTypeDiv = $("#add-link-type-div");
 | 
				
			||||||
const $linkTypes = $("input[name='add-link-type']");
 | 
					const $linkTypes = $("input[name='add-link-type']");
 | 
				
			||||||
const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
 | 
					const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
 | 
				
			||||||
const $showRecentNotesButton = $("#add-link-show-recent-notes");
 | 
					const $showRecentNotesButton = $dialog.find(".show-recent-notes-button");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setLinkType(linkType) {
 | 
					function setLinkType(linkType) {
 | 
				
			||||||
    $linkTypes.each(function () {
 | 
					    $linkTypes.each(function () {
 | 
				
			||||||
@@ -55,24 +55,7 @@ async function showDialog() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await $autoComplete.autocomplete({
 | 
					    await $autoComplete.autocomplete({
 | 
				
			||||||
        source: async function(request, response) {
 | 
					        source: noteAutocompleteService.autocompleteSource,
 | 
				
			||||||
            const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (result.length > 0) {
 | 
					 | 
				
			||||||
                response(result.map(row => {
 | 
					 | 
				
			||||||
                    return {
 | 
					 | 
				
			||||||
                        label: row.label,
 | 
					 | 
				
			||||||
                        value: row.label + ' (' + row.value + ')'
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            else {
 | 
					 | 
				
			||||||
                response([{
 | 
					 | 
				
			||||||
                    label: "No results",
 | 
					 | 
				
			||||||
                    value: "No results"
 | 
					 | 
				
			||||||
                }]);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        minLength: 0,
 | 
					        minLength: 0,
 | 
				
			||||||
        change: async (event, ui) => {
 | 
					        change: async (event, ui) => {
 | 
				
			||||||
            if (!ui.item) {
 | 
					            if (!ui.item) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,7 +27,8 @@ function AttributesModel() {
 | 
				
			|||||||
        { text: "Text", value: "text" },
 | 
					        { text: "Text", value: "text" },
 | 
				
			||||||
        { text: "Number", value: "number" },
 | 
					        { text: "Number", value: "number" },
 | 
				
			||||||
        { text: "Boolean", value: "boolean" },
 | 
					        { text: "Boolean", value: "boolean" },
 | 
				
			||||||
        { text: "Date", value: "date" }
 | 
					        { text: "Date", value: "date" },
 | 
				
			||||||
 | 
					        { text: "URL", value: "url"}
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.multiplicityTypes = [
 | 
					    this.multiplicityTypes = [
 | 
				
			||||||
@@ -66,7 +67,8 @@ function AttributesModel() {
 | 
				
			|||||||
                multiplicityType: "singlevalue",
 | 
					                multiplicityType: "singlevalue",
 | 
				
			||||||
                isPromoted: true
 | 
					                isPromoted: true
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
            attr.relationDefinition = attr.type === ('relation-definition' && attr.value) ? attr.value : {
 | 
					
 | 
				
			||||||
 | 
					            attr.relationDefinition = (attr.type === 'relation-definition' && attr.value) ? attr.value : {
 | 
				
			||||||
                multiplicityType: "singlevalue",
 | 
					                multiplicityType: "singlevalue",
 | 
				
			||||||
                isPromoted: true
 | 
					                isPromoted: true
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
@@ -91,7 +93,7 @@ function AttributesModel() {
 | 
				
			|||||||
        await showAttributes(attributes);
 | 
					        await showAttributes(attributes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // attribute might not be rendered immediatelly so could not focus
 | 
					        // attribute might not be rendered immediatelly so could not focus
 | 
				
			||||||
        setTimeout(() => $(".attribute-name:last").focus(), 100);
 | 
					        setTimeout(() => $(".attribute-type-select:last").focus(), 100);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $ownedAttributesBody.sortable({
 | 
					        $ownedAttributesBody.sortable({
 | 
				
			||||||
            handle: '.handle',
 | 
					            handle: '.handle',
 | 
				
			||||||
@@ -206,24 +208,6 @@ function AttributesModel() {
 | 
				
			|||||||
        attribute.valueHasMutated();
 | 
					        attribute.valueHasMutated();
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.isNotUnique = function(index) {
 | 
					 | 
				
			||||||
        const cur = self.ownedAttributes()[index]();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (cur.name.trim() === "") {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for (let attributes = self.ownedAttributes(), i = 0; i < attributes.length; i++) {
 | 
					 | 
				
			||||||
            const attribute = attributes[i]();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (index !== i && cur.name === attribute.name && cur.type === attribute.type) {
 | 
					 | 
				
			||||||
                return true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.isEmptyName = function(index) {
 | 
					    this.isEmptyName = function(index) {
 | 
				
			||||||
        const cur = self.ownedAttributes()[index]();
 | 
					        const cur = self.ownedAttributes()[index]();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -246,7 +230,7 @@ async function showDialog() {
 | 
				
			|||||||
    $dialog.dialog({
 | 
					    $dialog.dialog({
 | 
				
			||||||
        modal: true,
 | 
					        modal: true,
 | 
				
			||||||
        width: 950,
 | 
					        width: 950,
 | 
				
			||||||
        height: 500
 | 
					        height: 700
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,7 +15,7 @@ async function showDialog() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    await $dialog.dialog({
 | 
					    await $dialog.dialog({
 | 
				
			||||||
        modal: true,
 | 
					        modal: true,
 | 
				
			||||||
        width: 500
 | 
					        width: 600
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const currentNode = treeService.getCurrentNode();
 | 
					    const currentNode = treeService.getCurrentNode();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,12 @@
 | 
				
			|||||||
import treeService from '../services/tree.js';
 | 
					import treeService from '../services/tree.js';
 | 
				
			||||||
import server from '../services/server.js';
 | 
					 | 
				
			||||||
import searchNotesService from '../services/search_notes.js';
 | 
					import searchNotesService from '../services/search_notes.js';
 | 
				
			||||||
 | 
					import noteautocompleteService from '../services/note_autocomplete.js';
 | 
				
			||||||
 | 
					import linkService from "../services/link.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const $dialog = $("#jump-to-note-dialog");
 | 
					const $dialog = $("#jump-to-note-dialog");
 | 
				
			||||||
const $autoComplete = $("#jump-to-note-autocomplete");
 | 
					const $autoComplete = $("#jump-to-note-autocomplete");
 | 
				
			||||||
const $showInFullTextButton = $("#show-in-full-text-button");
 | 
					const $showInFullTextButton = $("#show-in-full-text-button");
 | 
				
			||||||
const $showRecentNotesButton = $("#jump-to-note-show-recent-notes");
 | 
					const $showRecentNotesButton = $dialog.find(".show-recent-notes-button");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function showDialog() {
 | 
					async function showDialog() {
 | 
				
			||||||
    glob.activeDialog = $dialog;
 | 
					    glob.activeDialog = $dialog;
 | 
				
			||||||
@@ -19,22 +20,8 @@ async function showDialog() {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await $autoComplete.autocomplete({
 | 
					    await $autoComplete.autocomplete({
 | 
				
			||||||
        source: async function(request, response) {
 | 
					        source: noteautocompleteService.autocompleteSource,
 | 
				
			||||||
            const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
 | 
					        focus: event => event.preventDefault(),
 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (result.length > 0) {
 | 
					 | 
				
			||||||
                response(result);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            else {
 | 
					 | 
				
			||||||
                response([{
 | 
					 | 
				
			||||||
                    label: "No results",
 | 
					 | 
				
			||||||
                    value: "No results"
 | 
					 | 
				
			||||||
                }]);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        focus: function(event, ui) {
 | 
					 | 
				
			||||||
            event.preventDefault();
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        minLength: 0,
 | 
					        minLength: 0,
 | 
				
			||||||
        autoFocus: true,
 | 
					        autoFocus: true,
 | 
				
			||||||
        select: function (event, ui) {
 | 
					        select: function (event, ui) {
 | 
				
			||||||
@@ -42,7 +29,9 @@ async function showDialog() {
 | 
				
			|||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            treeService.activateNode(ui.item.value);
 | 
					            const notePath = linkService.getNotePathFromLabel(ui.item.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            treeService.activateNote(notePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            $dialog.dialog('close');
 | 
					            $dialog.dialog('close');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -63,10 +63,10 @@ $list.on('change', () => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$(document).on('click', "a[action='note-revision']", event => {
 | 
					$(document).on('click', "a[data-action='note-revision']", event => {
 | 
				
			||||||
    const linkEl = $(event.target);
 | 
					    const linkEl = $(event.target);
 | 
				
			||||||
    const noteId = linkEl.attr('note-path');
 | 
					    const noteId = linkEl.attr('data-note-path');
 | 
				
			||||||
    const noteRevisionId = linkEl.attr('note-revision-id');
 | 
					    const noteRevisionId = linkEl.attr('data-note-revision-id');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    showNoteRevisionsDialog(noteId, noteRevisionId);
 | 
					    showNoteRevisionsDialog(noteId, noteRevisionId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,7 +15,11 @@ async function showDialog() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const result = await server.get('recent-changes/');
 | 
					    const result = await server.get('recent-changes/');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $dialog.html('');
 | 
					    $dialog.empty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (result.length === 0) {
 | 
				
			||||||
 | 
					        $dialog.append("No changes yet ...");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const groupedByDate = groupByDate(result);
 | 
					    const groupedByDate = groupByDate(result);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,9 +34,9 @@ async function showDialog() {
 | 
				
			|||||||
            const revLink = $("<a>", {
 | 
					            const revLink = $("<a>", {
 | 
				
			||||||
                href: 'javascript:',
 | 
					                href: 'javascript:',
 | 
				
			||||||
                text: 'rev'
 | 
					                text: 'rev'
 | 
				
			||||||
            }).attr('action', 'note-revision')
 | 
					            }).attr('data-action', 'note-revision')
 | 
				
			||||||
                .attr('note-path', change.noteId)
 | 
					                .attr('data-note-path', change.noteId)
 | 
				
			||||||
                .attr('note-revision-id', change.noteRevisionId);
 | 
					                .attr('data-note-revision-id', change.noteRevisionId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let noteLink;
 | 
					            let noteLink;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,28 @@
 | 
				
			|||||||
 | 
					/** Represents mapping between note and parent note */
 | 
				
			||||||
class Branch {
 | 
					class Branch {
 | 
				
			||||||
    constructor(treeCache, row) {
 | 
					    constructor(treeCache, row) {
 | 
				
			||||||
        this.treeCache = treeCache;
 | 
					        this.treeCache = treeCache;
 | 
				
			||||||
 | 
					        /** @param {string} primary key */
 | 
				
			||||||
        this.branchId = row.branchId;
 | 
					        this.branchId = row.branchId;
 | 
				
			||||||
 | 
					        /** @param {string} */
 | 
				
			||||||
        this.noteId = row.noteId;
 | 
					        this.noteId = row.noteId;
 | 
				
			||||||
        this.note = null;
 | 
					        this.note = null;
 | 
				
			||||||
 | 
					        /** @param {string} */
 | 
				
			||||||
        this.parentNoteId = row.parentNoteId;
 | 
					        this.parentNoteId = row.parentNoteId;
 | 
				
			||||||
 | 
					        /** @param {int} */
 | 
				
			||||||
        this.notePosition = row.notePosition;
 | 
					        this.notePosition = row.notePosition;
 | 
				
			||||||
 | 
					        /** @param {string} */
 | 
				
			||||||
        this.prefix = row.prefix;
 | 
					        this.prefix = row.prefix;
 | 
				
			||||||
 | 
					        /** @param {boolean} */
 | 
				
			||||||
        this.isExpanded = row.isExpanded;
 | 
					        this.isExpanded = row.isExpanded;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {NoteShort} */
 | 
				
			||||||
    async getNote() {
 | 
					    async getNote() {
 | 
				
			||||||
        return await this.treeCache.getNote(this.noteId);
 | 
					        return await this.treeCache.getNote(this.noteId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {boolean} true if it's top level, meaning its parent is root note */
 | 
				
			||||||
    isTopLevel() {
 | 
					    isTopLevel() {
 | 
				
			||||||
        return this.parentNoteId === 'root';
 | 
					        return this.parentNoteId === 'root';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,18 @@
 | 
				
			|||||||
import NoteShort from './note_short.js';
 | 
					import NoteShort from './note_short.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Represents full note, specifically including note's content.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
class NoteFull extends NoteShort {
 | 
					class NoteFull extends NoteShort {
 | 
				
			||||||
    constructor(treeCache, row) {
 | 
					    constructor(treeCache, row) {
 | 
				
			||||||
        super(treeCache, row);
 | 
					        super(treeCache, row);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @param {string} */
 | 
				
			||||||
        this.content = row.content;
 | 
					        this.content = row.content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.content !== "" && this.isJson()) {
 | 
					        if (this.content !== "" && this.isJson()) {
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
 | 
					                /** @param {object} */
 | 
				
			||||||
                this.jsonContent = JSON.parse(this.content);
 | 
					                this.jsonContent = JSON.parse(this.content);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            catch(e) {}
 | 
					            catch(e) {}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,31 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This note's representation is used in note tree and is kept in TreeCache.
 | 
				
			||||||
 | 
					 * Its notable omission is the note content.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
class NoteShort {
 | 
					class NoteShort {
 | 
				
			||||||
    constructor(treeCache, row) {
 | 
					    constructor(treeCache, row) {
 | 
				
			||||||
        this.treeCache = treeCache;
 | 
					        this.treeCache = treeCache;
 | 
				
			||||||
 | 
					        /** @param {string} */
 | 
				
			||||||
        this.noteId = row.noteId;
 | 
					        this.noteId = row.noteId;
 | 
				
			||||||
 | 
					        /** @param {string} */
 | 
				
			||||||
        this.title = row.title;
 | 
					        this.title = row.title;
 | 
				
			||||||
 | 
					        /** @param {boolean} */
 | 
				
			||||||
        this.isProtected = row.isProtected;
 | 
					        this.isProtected = row.isProtected;
 | 
				
			||||||
 | 
					        /** @param {string} one of 'text', 'code', 'file' or 'render' */
 | 
				
			||||||
        this.type = row.type;
 | 
					        this.type = row.type;
 | 
				
			||||||
 | 
					        /** @param {string} content-type, e.g. "application/json" */
 | 
				
			||||||
        this.mime = row.mime;
 | 
					        this.mime = row.mime;
 | 
				
			||||||
 | 
					        /** @param {boolean} */
 | 
				
			||||||
        this.archived = row.archived;
 | 
					        this.archived = row.archived;
 | 
				
			||||||
        this.cssClass = row.cssClass;
 | 
					        this.cssClass = row.cssClass;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {boolean} */
 | 
				
			||||||
    isJson() {
 | 
					    isJson() {
 | 
				
			||||||
        return this.mime === "application/json";
 | 
					        return this.mime === "application/json";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {Promise<Branch[]>} */
 | 
				
			||||||
    async getBranches() {
 | 
					    async getBranches() {
 | 
				
			||||||
        const branchIds = this.treeCache.parents[this.noteId].map(
 | 
					        const branchIds = this.treeCache.parents[this.noteId].map(
 | 
				
			||||||
            parentNoteId => this.treeCache.getBranchIdByChildParent(this.noteId, parentNoteId));
 | 
					            parentNoteId => this.treeCache.getBranchIdByChildParent(this.noteId, parentNoteId));
 | 
				
			||||||
@@ -21,11 +33,13 @@ class NoteShort {
 | 
				
			|||||||
        return this.treeCache.getBranches(branchIds);
 | 
					        return this.treeCache.getBranches(branchIds);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {boolean} */
 | 
				
			||||||
    hasChildren() {
 | 
					    hasChildren() {
 | 
				
			||||||
        return this.treeCache.children[this.noteId]
 | 
					        return this.treeCache.children[this.noteId]
 | 
				
			||||||
            && this.treeCache.children[this.noteId].length > 0;
 | 
					            && this.treeCache.children[this.noteId].length > 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {Promise<Branch[]>} */
 | 
				
			||||||
    async getChildBranches() {
 | 
					    async getChildBranches() {
 | 
				
			||||||
        if (!this.treeCache.children[this.noteId]) {
 | 
					        if (!this.treeCache.children[this.noteId]) {
 | 
				
			||||||
            return [];
 | 
					            return [];
 | 
				
			||||||
@@ -37,18 +51,22 @@ class NoteShort {
 | 
				
			|||||||
        return await this.treeCache.getBranches(branchIds);
 | 
					        return await this.treeCache.getBranches(branchIds);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {string[]} */
 | 
				
			||||||
    getParentNoteIds() {
 | 
					    getParentNoteIds() {
 | 
				
			||||||
        return this.treeCache.parents[this.noteId] || [];
 | 
					        return this.treeCache.parents[this.noteId] || [];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {Promise<NoteShort[]>} */
 | 
				
			||||||
    async getParentNotes() {
 | 
					    async getParentNotes() {
 | 
				
			||||||
        return await this.treeCache.getNotes(this.getParentNoteIds());
 | 
					        return await this.treeCache.getNotes(this.getParentNoteIds());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {string[]} */
 | 
				
			||||||
    getChildNoteIds() {
 | 
					    getChildNoteIds() {
 | 
				
			||||||
        return this.treeCache.children[this.noteId] || [];
 | 
					        return this.treeCache.children[this.noteId] || [];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @returns {Promise<NoteShort[]>} */
 | 
				
			||||||
    async getChildNotes() {
 | 
					    async getChildNotes() {
 | 
				
			||||||
        return await this.treeCache.getNotes(this.getChildNoteIds());
 | 
					        return await this.treeCache.getNotes(this.getChildNoteIds());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							@@ -17,7 +17,7 @@ import noteDetailService from './note_detail.js';
 | 
				
			|||||||
import noteType from './note_type.js';
 | 
					import noteType from './note_type.js';
 | 
				
			||||||
import protected_session from './protected_session.js';
 | 
					import protected_session from './protected_session.js';
 | 
				
			||||||
import searchNotesService from './search_notes.js';
 | 
					import searchNotesService from './search_notes.js';
 | 
				
			||||||
import ScriptApi from './script_api.js';
 | 
					import FrontendScriptApi from './frontend_script_api.js';
 | 
				
			||||||
import ScriptContext from './script_context.js';
 | 
					import ScriptContext from './script_context.js';
 | 
				
			||||||
import sync from './sync.js';
 | 
					import sync from './sync.js';
 | 
				
			||||||
import treeService from './tree.js';
 | 
					import treeService from './tree.js';
 | 
				
			||||||
@@ -71,6 +71,14 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
 | 
				
			|||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$(document).on("click", "button[data-help-page]", e => {
 | 
				
			||||||
 | 
					    const $button = $(e.target);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.open(wikiBaseUrl + $button.attr("data-help-page"), '_blank');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$("#logout-button").toggle(!utils.isElectron());
 | 
					$("#logout-button").toggle(!utils.isElectron());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (utils.isElectron()) {
 | 
					if (utils.isElectron()) {
 | 
				
			||||||
@@ -80,7 +88,7 @@ if (utils.isElectron()) {
 | 
				
			|||||||
            await treeService.reload();
 | 
					            await treeService.reload();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await treeService.activateNode(parentNoteId);
 | 
					        await treeService.activateNote(parentNoteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        setTimeout(() => {
 | 
					        setTimeout(() => {
 | 
				
			||||||
            const node = treeService.getCurrentNode();
 | 
					            const node = treeService.getCurrentNode();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import ScriptContext from "./script_context.js";
 | 
					import ScriptContext from "./script_context.js";
 | 
				
			||||||
import server from "./server.js";
 | 
					import server from "./server.js";
 | 
				
			||||||
 | 
					import infoService from "./info.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getAndExecuteBundle(noteId, originEntity = null) {
 | 
					async function getAndExecuteBundle(noteId, originEntity = null) {
 | 
				
			||||||
    const bundle = await server.get('script/bundle/' + noteId);
 | 
					    const bundle = await server.get('script/bundle/' + noteId);
 | 
				
			||||||
@@ -10,9 +11,14 @@ async function getAndExecuteBundle(noteId, originEntity = null) {
 | 
				
			|||||||
async function executeBundle(bundle, originEntity) {
 | 
					async function executeBundle(bundle, originEntity) {
 | 
				
			||||||
    const apiContext = ScriptContext(bundle.note, bundle.allNotes, originEntity);
 | 
					    const apiContext = ScriptContext(bundle.note, bundle.allNotes, originEntity);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
        return await (function () {
 | 
					        return await (function () {
 | 
				
			||||||
            return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
 | 
					            return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
 | 
				
			||||||
        }.call(apiContext));
 | 
					        }.call(apiContext));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    catch (e) {
 | 
				
			||||||
 | 
					        infoService.showAndLogError(`Execution of script "${bundle.note.title}" (${bundle.note.noteId}) failed with error: ${e.message}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function executeStartupBundles() {
 | 
					async function executeStartupBundles() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,7 @@ $("#file-upload").change(async function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    await treeService.reload();
 | 
					    await treeService.reload();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await treeService.activateNode(resp.noteId);
 | 
					    await treeService.activateNote(resp.noteId);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										196
									
								
								src/public/javascripts/services/frontend_script_api.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								src/public/javascripts/services/frontend_script_api.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,196 @@
 | 
				
			|||||||
 | 
					import treeService from './tree.js';
 | 
				
			||||||
 | 
					import server from './server.js';
 | 
				
			||||||
 | 
					import utils from './utils.js';
 | 
				
			||||||
 | 
					import infoService from './info.js';
 | 
				
			||||||
 | 
					import linkService from './link.js';
 | 
				
			||||||
 | 
					import treeCache from './tree_cache.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This is the main frontend API interface for scripts. It's published in the local "api" object.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @constructor
 | 
				
			||||||
 | 
					 * @hideconstructor
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function FrontendScriptApi(startNote, currentNote, originEntity = null) {
 | 
				
			||||||
 | 
					    const $pluginButtons = $("#plugin-buttons");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @property {object} note where script started executing */
 | 
				
			||||||
 | 
					    this.startNote = startNote;
 | 
				
			||||||
 | 
					    /** @property {object} note where script is currently executing */
 | 
				
			||||||
 | 
					    this.currentNote = currentNote;
 | 
				
			||||||
 | 
					    /** @property {object|null} entity whose event triggered this execution */
 | 
				
			||||||
 | 
					    this.originEntity = originEntity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Activates note in the tree and in the note detail.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} notePath (or noteId)
 | 
				
			||||||
 | 
					     * @returns {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.activateNote = treeService.activateNote;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Activates newly created note. Compared to this.activateNote() also refreshes tree.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {string} notePath (or noteId)
 | 
				
			||||||
 | 
					     * @return {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.activateNewNote = async notePath => {
 | 
				
			||||||
 | 
					        await treeService.reload();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await treeService.activateNote(notePath, true);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @typedef {Object} ToolbarButtonOptions
 | 
				
			||||||
 | 
					     * @property {string} title
 | 
				
			||||||
 | 
					     * @property {string} [icon] - name of the jQuery UI icon to be used (e.g. "clock" for "ui-icon-clock" icon)
 | 
				
			||||||
 | 
					     * @property {function} action - callback handling the click on the button
 | 
				
			||||||
 | 
					     * @property {string} [shortcut] - keyboard shortcut for the button, e.g. "alt+t"
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Adds new button the the plugin area.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {ToolbarButtonOptions} opts
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.addButtonToToolbar = opts => {
 | 
				
			||||||
 | 
					        const buttonId = "toolbar-button-" + opts.title.replace(/[^a-zA-Z0-9]/g, "-");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $("#" + buttonId).remove();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const icon = $("<span>")
 | 
				
			||||||
 | 
					            .addClass("ui-icon ui-icon-" + opts.icon);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const button = $('<button>')
 | 
				
			||||||
 | 
					            .addClass("btn btn-xs")
 | 
				
			||||||
 | 
					            .click(opts.action)
 | 
				
			||||||
 | 
					            .append(icon)
 | 
				
			||||||
 | 
					            .append($("<span>").text(opts.title));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        button.attr('id', buttonId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $pluginButtons.append(button);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (opts.shortcut) {
 | 
				
			||||||
 | 
					            $(document).bind('keydown', opts.shortcut, opts.action);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            button.attr("title", "Shortcut " + opts.shortcut);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function prepareParams(params) {
 | 
				
			||||||
 | 
					        if (!params) {
 | 
				
			||||||
 | 
					            return params;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return params.map(p => {
 | 
				
			||||||
 | 
					            if (typeof p === "function") {
 | 
				
			||||||
 | 
					                return "!@#Function: " + p.toString();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            else {
 | 
				
			||||||
 | 
					                return p;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Executes given anonymous function on the server.
 | 
				
			||||||
 | 
					     * Internally this serializes the anonymous function into string and sends it to backend via AJAX.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {string} script - script to be executed on the backend
 | 
				
			||||||
 | 
					     * @param {Array.<?>} params - list of parameters to the anonymous function to be send to backend
 | 
				
			||||||
 | 
					     * @return {Promise<*>} return value of the executed function on the backend
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.runOnServer = async (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,
 | 
				
			||||||
 | 
					            originEntityName: "notes", // currently there's no other entity on frontend which can trigger event
 | 
				
			||||||
 | 
					            originEntityId: originEntity ? originEntity.noteId : null
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (ret.success) {
 | 
				
			||||||
 | 
					            return ret.executionResult;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else {
 | 
				
			||||||
 | 
					            throw new Error("server error: " + ret.error);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns list of notes. If note is missing from cache, it's loaded.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * This is often used to bulk-fill the cache with notes which would have to be picked one by one
 | 
				
			||||||
 | 
					     * otherwise (by e.g. createNoteLink())
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {string[]} noteIds
 | 
				
			||||||
 | 
					     * @param {boolean} [silentNotFoundError] - don't report error if the note is not found
 | 
				
			||||||
 | 
					     * @return {Promise<NoteShort[]>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.getNotes = async (noteIds, silentNotFoundError = false) => await treeCache.getNotes(noteIds, silentNotFoundError);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Instance name identifies particular Trilium instance. It can be useful for scripts
 | 
				
			||||||
 | 
					     * if some action needs to happen on only one specific instance.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.getInstanceName = () => window.glob.instanceName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {Date} date
 | 
				
			||||||
 | 
					     * @returns {string} date in YYYY-MM-DD format
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.formatDateISO = utils.formatDateISO;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} str
 | 
				
			||||||
 | 
					     * @returns {Date} parsed object
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.parseDate = utils.parseDate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Show info message to the user.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} message
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.showMessage = infoService.showMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Show error message to the user.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} message
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.showError = infoService.showError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Refresh tree
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @returns {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.refreshTree = treeService.reload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Create note link (jQuery object) for given note.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} notePath (or noteId)
 | 
				
			||||||
 | 
					     * @param {string} [noteTitle] - if not present we'll use note title
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.createNoteLink = linkService.createNoteLink;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default FrontendScriptApi;
 | 
				
			||||||
@@ -14,6 +14,12 @@ function showMessage(message) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function showAndLogError(message, delay = 10000) {
 | 
				
			||||||
 | 
					    showError(message, delay);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    messagingService.logError(message);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function showError(message, delay = 10000) {
 | 
					function showError(message, delay = 10000) {
 | 
				
			||||||
    console.log(utils.now(), "error: ", message);
 | 
					    console.log(utils.now(), "error: ", message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -36,5 +42,6 @@ function throwError(message) {
 | 
				
			|||||||
export default {
 | 
					export default {
 | 
				
			||||||
    showMessage,
 | 
					    showMessage,
 | 
				
			||||||
    showError,
 | 
					    showError,
 | 
				
			||||||
 | 
					    showAndLogError,
 | 
				
			||||||
    throwError
 | 
					    throwError
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -3,7 +3,7 @@ import noteDetailText from './note_detail_text.js';
 | 
				
			|||||||
import treeUtils from './tree_utils.js';
 | 
					import treeUtils from './tree_utils.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getNotePathFromLink(url) {
 | 
					function getNotePathFromLink(url) {
 | 
				
			||||||
    const notePathMatch = /#([A-Za-z0-9/]+)$/.exec(url);
 | 
					    const notePathMatch = /#root([A-Za-z0-9/]*)$/.exec(url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (notePathMatch === null) {
 | 
					    if (notePathMatch === null) {
 | 
				
			||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
@@ -14,7 +14,7 @@ function getNotePathFromLink(url) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getNotePathFromLabel(label) {
 | 
					function getNotePathFromLabel(label) {
 | 
				
			||||||
    const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label);
 | 
					    const notePathMatch = / \(([#A-Za-z0-9/]+)\)/.exec(label);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (notePathMatch !== null) {
 | 
					    if (notePathMatch !== null) {
 | 
				
			||||||
        return notePathMatch[1];
 | 
					        return notePathMatch[1];
 | 
				
			||||||
@@ -33,8 +33,8 @@ async function createNoteLink(notePath, noteTitle = null) {
 | 
				
			|||||||
    const noteLink = $("<a>", {
 | 
					    const noteLink = $("<a>", {
 | 
				
			||||||
        href: 'javascript:',
 | 
					        href: 'javascript:',
 | 
				
			||||||
        text: noteTitle
 | 
					        text: noteTitle
 | 
				
			||||||
    }).attr('action', 'note')
 | 
					    }).attr('data-action', 'note')
 | 
				
			||||||
        .attr('note-path', notePath);
 | 
					        .attr('data-note-path', notePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return noteLink;
 | 
					    return noteLink;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -43,10 +43,10 @@ function goToLink(e) {
 | 
				
			|||||||
    e.preventDefault();
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const $link = $(e.target);
 | 
					    const $link = $(e.target);
 | 
				
			||||||
    let notePath = $link.attr("note-path");
 | 
					    let notePath = $link.attr("data-note-path");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!notePath) {
 | 
					    if (!notePath) {
 | 
				
			||||||
        const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href');
 | 
					        const address = $link.attr("data-note-path") ? $link.attr("data-note-path") : $link.attr('href');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!address) {
 | 
					        if (!address) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
@@ -61,7 +61,7 @@ function goToLink(e) {
 | 
				
			|||||||
        notePath = getNotePathFromLink(address);
 | 
					        notePath = getNotePathFromLink(address);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    treeService.activateNode(notePath);
 | 
					    treeService.activateNote(notePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // this is quite ugly hack, but it seems like we can't close the tooltip otherwise
 | 
					    // this is quite ugly hack, but it seems like we can't close the tooltip otherwise
 | 
				
			||||||
    $("[role='tooltip']").remove();
 | 
					    $("[role='tooltip']").remove();
 | 
				
			||||||
@@ -104,7 +104,7 @@ ko.bindingHandlers.noteLink = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
 | 
					// when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
 | 
				
			||||||
// of opening the link in new window/tab
 | 
					// of opening the link in new window/tab
 | 
				
			||||||
$(document).on('click', "a[action='note']", goToLink);
 | 
					$(document).on('click', "a[data-action='note']", goToLink);
 | 
				
			||||||
$(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
 | 
					$(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
 | 
				
			||||||
$(document).on('dblclick', '#note-detail-text a', goToLink);
 | 
					$(document).on('dblclick', '#note-detail-text a', goToLink);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,10 @@
 | 
				
			|||||||
import server from "./server.js";
 | 
					import server from "./server.js";
 | 
				
			||||||
 | 
					import noteDetailService from "./note_detail.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function initNoteAutocomplete($el) {
 | 
					async function autocompleteSource(request, response) {
 | 
				
			||||||
    if (!$el.hasClass("ui-autocomplete-input")) {
 | 
					    const result = await server.get('autocomplete'
 | 
				
			||||||
        const $showRecentNotesButton = $("<span>")
 | 
					        + '?query=' + encodeURIComponent(request.term)
 | 
				
			||||||
            .addClass("input-group-addon show-recent-notes-button")
 | 
					        + '¤tNoteId=' + noteDetailService.getCurrentNoteId());
 | 
				
			||||||
            .prop("title", "Show recent notes");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $el.after($showRecentNotesButton);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $showRecentNotesButton.click(() => $el.autocomplete("search", ""));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        await $el.autocomplete({
 | 
					 | 
				
			||||||
            appendTo: $el.parent().parent(),
 | 
					 | 
				
			||||||
            source: async function (request, response) {
 | 
					 | 
				
			||||||
                const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (result.length > 0) {
 | 
					    if (result.length > 0) {
 | 
				
			||||||
        response(result.map(row => {
 | 
					        response(result.map(row => {
 | 
				
			||||||
@@ -29,7 +20,21 @@ async function initNoteAutocomplete($el) {
 | 
				
			|||||||
            value: "No results"
 | 
					            value: "No results"
 | 
				
			||||||
        }]);
 | 
					        }]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
            },
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function initNoteAutocomplete($el) {
 | 
				
			||||||
 | 
					    if (!$el.hasClass("ui-autocomplete-input")) {
 | 
				
			||||||
 | 
					        const $showRecentNotesButton = $("<span>")
 | 
				
			||||||
 | 
					            .addClass("input-group-addon show-recent-notes-button")
 | 
				
			||||||
 | 
					            .prop("title", "Show recent notes");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $el.after($showRecentNotesButton);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $showRecentNotesButton.click(() => $el.autocomplete("search", ""));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await $el.autocomplete({
 | 
				
			||||||
 | 
					            appendTo: $el.parent().parent(),
 | 
				
			||||||
 | 
					            source: autocompleteSource,
 | 
				
			||||||
            minLength: 0,
 | 
					            minLength: 0,
 | 
				
			||||||
            change: function (event, ui) {
 | 
					            change: function (event, ui) {
 | 
				
			||||||
                $el.trigger("change");
 | 
					                $el.trigger("change");
 | 
				
			||||||
@@ -50,5 +55,6 @@ ko.bindingHandlers.noteAutocomplete = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
    initNoteAutocomplete
 | 
					    initNoteAutocomplete,
 | 
				
			||||||
 | 
					    autocompleteSource
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -25,7 +25,6 @@ const $noteDetailComponents = $(".note-detail-component");
 | 
				
			|||||||
const $protectButton = $("#protect-button");
 | 
					const $protectButton = $("#protect-button");
 | 
				
			||||||
const $unprotectButton = $("#unprotect-button");
 | 
					const $unprotectButton = $("#unprotect-button");
 | 
				
			||||||
const $noteDetailWrapper = $("#note-detail-wrapper");
 | 
					const $noteDetailWrapper = $("#note-detail-wrapper");
 | 
				
			||||||
const $noteDetailComponentWrapper = $("#note-detail-component-wrapper");
 | 
					 | 
				
			||||||
const $noteIdDisplay = $("#note-id-display");
 | 
					const $noteIdDisplay = $("#note-id-display");
 | 
				
			||||||
const $attributeList = $("#attribute-list");
 | 
					const $attributeList = $("#attribute-list");
 | 
				
			||||||
const $attributeListInner = $("#attribute-list-inner");
 | 
					const $attributeListInner = $("#attribute-list-inner");
 | 
				
			||||||
@@ -120,11 +119,12 @@ async function saveNoteIfChanged() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setNoteBackgroundIfProtected(note) {
 | 
					function setNoteBackgroundIfProtected(note) {
 | 
				
			||||||
    const isProtected = !!note.isProtected;
 | 
					    const isProtected = note.isProtected;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $noteDetailComponentWrapper.toggleClass("protected", isProtected);
 | 
					    $noteDetailWrapper.toggleClass("protected", isProtected);
 | 
				
			||||||
    $protectButton.toggleClass("active", isProtected);
 | 
					    $protectButton.toggleClass("active", isProtected);
 | 
				
			||||||
    $unprotectButton.toggleClass("active", !isProtected);
 | 
					    $unprotectButton.toggleClass("active", !isProtected);
 | 
				
			||||||
 | 
					    $unprotectButton.prop("disabled", !protectedSessionHolder.isProtectedSessionAvailable());
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let isNewNoteCreated = false;
 | 
					let isNewNoteCreated = false;
 | 
				
			||||||
@@ -158,8 +158,6 @@ async function loadNoteDetail(noteId) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    setNoteBackgroundIfProtected(currentNote);
 | 
					    setNoteBackgroundIfProtected(currentNote);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await handleProtectedSession();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $noteDetailWrapper.show();
 | 
					    $noteDetailWrapper.show();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    noteChangeDisabled = true;
 | 
					    noteChangeDisabled = true;
 | 
				
			||||||
@@ -172,6 +170,8 @@ async function loadNoteDetail(noteId) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        $noteDetailComponents.hide();
 | 
					        $noteDetailComponents.hide();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await handleProtectedSession();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await getComponent(currentNote.type).show();
 | 
					        await getComponent(currentNote.type).show();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    finally {
 | 
					    finally {
 | 
				
			||||||
@@ -209,7 +209,7 @@ async function showChildrenOverview(hideChildrenOverview) {
 | 
				
			|||||||
        const link = $('<a>', {
 | 
					        const link = $('<a>', {
 | 
				
			||||||
            href: 'javascript:',
 | 
					            href: 'javascript:',
 | 
				
			||||||
            text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId)
 | 
					            text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId)
 | 
				
			||||||
        }).attr('action', 'note').attr('note-path', notePath + '/' + childBranch.noteId);
 | 
					        }).attr('data-action', 'note').attr('data-note-path', notePath + '/' + childBranch.noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const childEl = $('<div class="child-overview">').html(link);
 | 
					        const childEl = $('<div class="child-overview">').html(link);
 | 
				
			||||||
        $childrenOverview.append(childEl);
 | 
					        $childrenOverview.append(childEl);
 | 
				
			||||||
@@ -226,7 +226,10 @@ async function loadAttributes() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const attributes = await server.get('notes/' + noteId + '/attributes');
 | 
					    const attributes = await server.get('notes/' + noteId + '/attributes');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const promoted = attributes.filter(attr => (attr.type === 'label-definition' || attr.type === 'relation-definition') && attr.value.isPromoted);
 | 
					    const promoted = attributes.filter(attr =>
 | 
				
			||||||
 | 
					        (attr.type === 'label-definition' || attr.type === 'relation-definition')
 | 
				
			||||||
 | 
					        && !attr.name.startsWith("child:")
 | 
				
			||||||
 | 
					        && attr.value.isPromoted);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let idx = 1;
 | 
					    let idx = 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -299,6 +302,7 @@ async function loadAttributes() {
 | 
				
			|||||||
                $input.datepicker({
 | 
					                $input.datepicker({
 | 
				
			||||||
                    changeMonth: true,
 | 
					                    changeMonth: true,
 | 
				
			||||||
                    changeYear: true,
 | 
					                    changeYear: true,
 | 
				
			||||||
 | 
					                    yearRange: "c-200:c+10",
 | 
				
			||||||
                    dateFormat: "yy-mm-dd"
 | 
					                    dateFormat: "yy-mm-dd"
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -309,6 +313,15 @@ async function loadAttributes() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                $actionCell.append($todayButton);
 | 
					                $actionCell.append($todayButton);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            else if (definition.labelType === 'url') {
 | 
				
			||||||
 | 
					                $input.prop("placeholder", "http://website...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const $openButton = $("<button>").addClass("btn btn-small").text("Open").click(() => {
 | 
				
			||||||
 | 
					                    window.open($input.val(), '_blank');
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $actionCell.append($openButton);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            else {
 | 
					            else {
 | 
				
			||||||
                messagingService.logError("Unknown labelType=" + definitionAttr.labelType);
 | 
					                messagingService.logError("Unknown labelType=" + definitionAttr.labelType);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -320,6 +333,16 @@ async function loadAttributes() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            // no need to wait for this
 | 
					            // no need to wait for this
 | 
				
			||||||
            noteAutocompleteService.initNoteAutocomplete($input);
 | 
					            noteAutocompleteService.initNoteAutocomplete($input);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // ideally we'd use link instead of button which would allow tooltip preview, but
 | 
				
			||||||
 | 
					            // we can't guarantee updating the link in the a element
 | 
				
			||||||
 | 
					            const $openButton = $("<button>").addClass("btn btn-small").text("Open").click(() => {
 | 
				
			||||||
 | 
					                const notePath = linkService.getNotePathFromLabel($input.val());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                treeService.activateNote(notePath);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $actionCell.append($openButton);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
            messagingService.logError("Unknown attribute type=" + valueAttr.type);
 | 
					            messagingService.logError("Unknown attribute type=" + valueAttr.type);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,8 +19,8 @@ async function show() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    $noteDetailFile.show();
 | 
					    $noteDetailFile.show();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $fileFileName.text(attributeMap.original_file_name);
 | 
					    $fileFileName.text(attributeMap.originalFileName);
 | 
				
			||||||
    $fileFileSize.text(attributeMap.file_size + " bytes");
 | 
					    $fileFileSize.text(attributeMap.fileSize + " bytes");
 | 
				
			||||||
    $fileFileType.text(currentNote.mime);
 | 
					    $fileFileType.text(currentNote.mime);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,7 @@ let codeEditorInitialized;
 | 
				
			|||||||
async function show() {
 | 
					async function show() {
 | 
				
			||||||
    codeEditorInitialized = false;
 | 
					    codeEditorInitialized = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $noteDetailRender.show();
 | 
					    $noteDetailRender.empty().show();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await render();
 | 
					    await render();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,6 +32,7 @@ function ensureProtectedSession(requireProtectedSession, modal) {
 | 
				
			|||||||
    const dfd = $.Deferred();
 | 
					    const dfd = $.Deferred();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (requireProtectedSession && !protectedSessionHolder.isProtectedSessionAvailable()) {
 | 
					    if (requireProtectedSession && !protectedSessionHolder.isProtectedSessionAvailable()) {
 | 
				
			||||||
 | 
					        // using deferred instead of promise because it allows resolving from outside
 | 
				
			||||||
        protectedSessionDeferred = dfd;
 | 
					        protectedSessionDeferred = dfd;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (treeService.getCurrentNode().data.isProtected) {
 | 
					        if (treeService.getCurrentNode().data.isProtected) {
 | 
				
			||||||
@@ -39,7 +40,6 @@ function ensureProtectedSession(requireProtectedSession, modal) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $dialog.dialog({
 | 
					        $dialog.dialog({
 | 
				
			||||||
            // modal: modal,
 | 
					 | 
				
			||||||
            // everything is now non-modal, because modal dialog caused weird high CPU usage on opening
 | 
					            // everything is now non-modal, because modal dialog caused weird high CPU usage on opening
 | 
				
			||||||
            // and tearing of text input
 | 
					            // and tearing of text input
 | 
				
			||||||
            modal: false,
 | 
					            modal: false,
 | 
				
			||||||
@@ -128,7 +128,14 @@ async function unprotectNoteAndSendToServer() {
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await ensureProtectedSession(true, true);
 | 
					    if (!protectedSessionHolder.isProtectedSessionAvailable()) {
 | 
				
			||||||
 | 
					        console.log("Unprotecting notes outside of protected session is not allowed.");
 | 
				
			||||||
 | 
					        // the reason is that it's not easy to handle even with ensureProtectedSession,
 | 
				
			||||||
 | 
					        // because we would first have to make sure the note is loaded and only then unprotect
 | 
				
			||||||
 | 
					        // we used to have a bug where we would overwrite the previous note with unprotected content.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const note = noteDetailService.getCurrentNote();
 | 
					    const note = noteDetailService.getCurrentNote();
 | 
				
			||||||
    note.isProtected = false;
 | 
					    note.isProtected = false;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,79 +0,0 @@
 | 
				
			|||||||
import treeService from './tree.js';
 | 
					 | 
				
			||||||
import server from './server.js';
 | 
					 | 
				
			||||||
import utils from './utils.js';
 | 
					 | 
				
			||||||
import infoService from './info.js';
 | 
					 | 
				
			||||||
import linkService from './link.js';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function ScriptApi(startNote, currentNote, originEntity = null) {
 | 
					 | 
				
			||||||
    const $pluginButtons = $("#plugin-buttons");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function activateNote(notePath) {
 | 
					 | 
				
			||||||
        await treeService.activateNode(notePath);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function activateNewNote(notePath) {
 | 
					 | 
				
			||||||
        await treeService.reload();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        await treeService.activateNode(notePath, true);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function addButtonToToolbar(buttonId, button) {
 | 
					 | 
				
			||||||
        $("#" + buttonId).remove();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        button.attr('id', buttonId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $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,
 | 
					 | 
				
			||||||
            originEntityName: originEntity ? originEntity.constructor.tableName : null,
 | 
					 | 
				
			||||||
            originEntityId: originEntity ? originEntity.noteId : null
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return ret.executionResult;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        startNote: startNote,
 | 
					 | 
				
			||||||
        currentNote: currentNote,
 | 
					 | 
				
			||||||
        originEntity: originEntity,
 | 
					 | 
				
			||||||
        addButtonToToolbar,
 | 
					 | 
				
			||||||
        activateNote,
 | 
					 | 
				
			||||||
        activateNewNote,
 | 
					 | 
				
			||||||
        getInstanceName: () => window.glob.instanceName,
 | 
					 | 
				
			||||||
        runOnServer,
 | 
					 | 
				
			||||||
        formatDateISO: utils.formatDateISO,
 | 
					 | 
				
			||||||
        parseDate: utils.parseDate,
 | 
					 | 
				
			||||||
        showMessage: infoService.showMessage,
 | 
					 | 
				
			||||||
        showError: infoService.showError,
 | 
					 | 
				
			||||||
        reloadTree: treeService.reload, // deprecated
 | 
					 | 
				
			||||||
        refreshTree: treeService.reload,
 | 
					 | 
				
			||||||
        createNoteLink: linkService.createNoteLink
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default ScriptApi;
 | 
					 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import ScriptApi from './script_api.js';
 | 
					import FrontendScriptApi from './frontend_script_api.js';
 | 
				
			||||||
import utils from './utils.js';
 | 
					import utils from './utils.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function ScriptContext(startNote, allNotes, originEntity = null) {
 | 
					function ScriptContext(startNote, allNotes, originEntity = null) {
 | 
				
			||||||
@@ -7,7 +7,7 @@ function ScriptContext(startNote, allNotes, originEntity = null) {
 | 
				
			|||||||
    return {
 | 
					    return {
 | 
				
			||||||
        modules: modules,
 | 
					        modules: modules,
 | 
				
			||||||
        notes: utils.toObject(allNotes, note => [note.noteId, note]),
 | 
					        notes: utils.toObject(allNotes, note => [note.noteId, note]),
 | 
				
			||||||
        apis: utils.toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note, originEntity)]),
 | 
					        apis: utils.toObject(allNotes, note => [note.noteId, new FrontendScriptApi(startNote, note, originEntity)]),
 | 
				
			||||||
        require: moduleNoteIds => {
 | 
					        require: moduleNoteIds => {
 | 
				
			||||||
            return moduleName => {
 | 
					            return moduleName => {
 | 
				
			||||||
                const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
 | 
					                const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -58,7 +58,7 @@ async function doSearch(searchText) {
 | 
				
			|||||||
        const link = $('<a>', {
 | 
					        const link = $('<a>', {
 | 
				
			||||||
            href: 'javascript:',
 | 
					            href: 'javascript:',
 | 
				
			||||||
            text: result.title
 | 
					            text: result.title
 | 
				
			||||||
        }).attr('action', 'note').attr('note-path', result.path);
 | 
					        }).attr('data-action', 'note').attr('data-note-path', result.path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const $result = $('<li>').append(link);
 | 
					        const $result = $('<li>').append(link);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -73,7 +73,7 @@ async function saveSearch() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    await treeService.reload();
 | 
					    await treeService.reload();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await treeService.activateNode(noteId);
 | 
					    await treeService.activateNote(noteId);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$searchInput.keyup(e => {
 | 
					$searchInput.keyup(e => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,33 +1,32 @@
 | 
				
			|||||||
import noteDetailService from "./note_detail.js";
 | 
					import noteDetailService from "./note_detail.js";
 | 
				
			||||||
import treeUtils from "./tree_utils.js";
 | 
					import treeUtils from "./tree_utils.js";
 | 
				
			||||||
import linkService from "./link.js";
 | 
					import linkService from "./link.js";
 | 
				
			||||||
 | 
					import server from "./server.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setupTooltip() {
 | 
					function setupTooltip() {
 | 
				
			||||||
    $(document).tooltip({
 | 
					    $(document).tooltip({
 | 
				
			||||||
        items: "body a",
 | 
					        items: "body a",
 | 
				
			||||||
        content: function (callback) {
 | 
					        content: function (callback) {
 | 
				
			||||||
            let notePath = linkService.getNotePathFromLink($(this).attr("href"));
 | 
					            const $link = $(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if ($link.hasClass("no-tooltip-preview")) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let notePath = linkService.getNotePathFromLink($link.attr("href"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!notePath) {
 | 
					            if (!notePath) {
 | 
				
			||||||
                notePath = $(this).attr("note-path");
 | 
					                notePath = $link.attr("data-note-path");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (notePath) {
 | 
					            if (notePath) {
 | 
				
			||||||
                const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
					                const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                noteDetailService.loadNote(noteId).then(note => {
 | 
					                const notePromise = noteDetailService.loadNote(noteId);
 | 
				
			||||||
                    if (!note.content.trim()) {
 | 
					                const attributePromise = server.get('notes/' + noteId + '/attributes');
 | 
				
			||||||
                        return;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (note.type === 'text') {
 | 
					                Promise.all([notePromise, attributePromise])
 | 
				
			||||||
                        callback(note.content);
 | 
					                    .then(([note, attributes]) => renderTooltip(callback, note, attributes));
 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    else if (note.type === 'code') {
 | 
					 | 
				
			||||||
                        callback($("<pre>").text(note.content).prop('outerHTML'));
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    // other types of notes don't have tooltip preview
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        close: function (event, ui) {
 | 
					        close: function (event, ui) {
 | 
				
			||||||
@@ -43,6 +42,62 @@ function setupTooltip() {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function renderTooltip(callback, note, attributes) {
 | 
				
			||||||
 | 
					    let content = '';
 | 
				
			||||||
 | 
					    const promoted = attributes.filter(attr =>
 | 
				
			||||||
 | 
					        (attr.type === 'label-definition' || attr.type === 'relation-definition')
 | 
				
			||||||
 | 
					        && !attr.name.startsWith("child:")
 | 
				
			||||||
 | 
					        && attr.value.isPromoted);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (promoted.length > 0) {
 | 
				
			||||||
 | 
					        const $table = $("<table>").addClass("promoted-attributes-in-tooltip");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (const definitionAttr of promoted) {
 | 
				
			||||||
 | 
					            const definitionType = definitionAttr.type;
 | 
				
			||||||
 | 
					            const valueType = definitionType.substr(0, definitionType.length - 11);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let valueAttrs = attributes.filter(el => el.name === definitionAttr.name && el.type === valueType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (const valueAttr of valueAttrs) {
 | 
				
			||||||
 | 
					                if (!valueAttr.value) {
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let $value = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (valueType === 'label') {
 | 
				
			||||||
 | 
					                    $value = $("<td>").text(valueAttr.value);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                else if (valueType === 'relation' && valueAttr.value) {
 | 
				
			||||||
 | 
					                    $value = $("<td>").append(await linkService.createNoteLink(valueAttr.value));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const $row = $("<tr>")
 | 
				
			||||||
 | 
					                    .append($("<th>").text(definitionAttr.name))
 | 
				
			||||||
 | 
					                    .append($value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $table.append($row);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        content += $table.prop('outerHTML');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (note.type === 'text') {
 | 
				
			||||||
 | 
					        content += note.content;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else if (note.type === 'code') {
 | 
				
			||||||
 | 
					        content += $("<pre>").text(note.content).prop('outerHTML');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // other types of notes don't have tooltip preview
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!content.trim()) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    callback(content);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
    setupTooltip
 | 
					    setupTooltip
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -100,7 +100,7 @@ async function expandToNote(notePath, expandOpts) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function activateNode(notePath, newNote) {
 | 
					async function activateNote(notePath, newNote) {
 | 
				
			||||||
    utils.assertArguments(notePath);
 | 
					    utils.assertArguments(notePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const node = await expandToNote(notePath);
 | 
					    const node = await expandToNote(notePath);
 | 
				
			||||||
@@ -206,7 +206,11 @@ async function showPaths(noteId, node) {
 | 
				
			|||||||
        const notePath = parentNotePath ? (parentNotePath + '/' + noteId) : noteId;
 | 
					        const notePath = parentNotePath ? (parentNotePath + '/' + noteId) : noteId;
 | 
				
			||||||
        const title = await treeUtils.getNotePathTitle(notePath);
 | 
					        const title = await treeUtils.getNotePathTitle(notePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const item = $("<li/>").append(await linkService.createNoteLink(notePath, title));
 | 
					        const noteLink = await linkService.createNoteLink(notePath, title);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        noteLink.addClass("no-tooltip-preview");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const item = $("<li/>").append(noteLink);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (node.getParent().data.noteId === parentNote.noteId) {
 | 
					        if (node.getParent().data.noteId === parentNote.noteId) {
 | 
				
			||||||
            item.addClass("current");
 | 
					            item.addClass("current");
 | 
				
			||||||
@@ -291,7 +295,7 @@ async function treeInitialized() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (startNotePath) {
 | 
					    if (startNotePath) {
 | 
				
			||||||
        const node = await activateNode(startNotePath);
 | 
					        const node = await activateNote(startNotePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // looks like this this doesn't work when triggered immediatelly after activating node
 | 
					        // looks like this this doesn't work when triggered immediatelly after activating node
 | 
				
			||||||
        // so waiting a second helps
 | 
					        // so waiting a second helps
 | 
				
			||||||
@@ -357,6 +361,7 @@ function initFancyTree(tree) {
 | 
				
			|||||||
        dnd: dragAndDropSetup,
 | 
					        dnd: dragAndDropSetup,
 | 
				
			||||||
        lazyLoad: function(event, data) {
 | 
					        lazyLoad: function(event, data) {
 | 
				
			||||||
            const noteId = data.node.data.noteId;
 | 
					            const noteId = data.node.data.noteId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            data.result = treeCache.getNote(noteId).then(note => treeBuilder.prepareBranch(note));
 | 
					            data.result = treeCache.getNote(noteId).then(note => treeBuilder.prepareBranch(note));
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        clones: {
 | 
					        clones: {
 | 
				
			||||||
@@ -414,7 +419,7 @@ function scrollToCurrentNote() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setBranchBackgroundBasedOnProtectedStatus(noteId) {
 | 
					function setBranchBackgroundBasedOnProtectedStatus(noteId) {
 | 
				
			||||||
    getNodesByNoteId(noteId).map(node => node.toggleClass("protected", !!node.data.isProtected));
 | 
					    getNodesByNoteId(noteId).map(node => node.toggleClass("protected", node.data.isProtected));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setProtected(noteId, isProtected) {
 | 
					function setProtected(noteId, isProtected) {
 | 
				
			||||||
@@ -557,7 +562,7 @@ $(window).bind('hashchange', function() {
 | 
				
			|||||||
    if (getCurrentNotePath() !== notePath) {
 | 
					    if (getCurrentNotePath() !== notePath) {
 | 
				
			||||||
        console.log("Switching to " + notePath + " because of hash change");
 | 
					        console.log("Switching to " + notePath + " because of hash change");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        activateNode(notePath);
 | 
					        activateNote(notePath);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -574,7 +579,7 @@ export default {
 | 
				
			|||||||
    setBranchBackgroundBasedOnProtectedStatus,
 | 
					    setBranchBackgroundBasedOnProtectedStatus,
 | 
				
			||||||
    setProtected,
 | 
					    setProtected,
 | 
				
			||||||
    expandToNote,
 | 
					    expandToNote,
 | 
				
			||||||
    activateNode,
 | 
					    activateNote,
 | 
				
			||||||
    getCurrentNode,
 | 
					    getCurrentNode,
 | 
				
			||||||
    getCurrentNotePath,
 | 
					    getCurrentNotePath,
 | 
				
			||||||
    setCurrentNotePathToHash,
 | 
					    setCurrentNotePathToHash,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -74,12 +74,11 @@ async function prepareRealBranch(parentNote) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
async function prepareSearchBranch(note) {
 | 
					async function prepareSearchBranch(note) {
 | 
				
			||||||
    const fullNote = await noteDetailService.loadNote(note.noteId);
 | 
					    const fullNote = await noteDetailService.loadNote(note.noteId);
 | 
				
			||||||
    const results = await server.get('search/' + encodeURIComponent(fullNote.jsonContent.searchString));
 | 
					    const results = (await server.get('search/' + encodeURIComponent(fullNote.jsonContent.searchString)))
 | 
				
			||||||
 | 
					        .filter(res => res.noteId !== note.noteId); // this is necessary because title of the search note is often the same as the search text which would match and create circle
 | 
				
			||||||
    const noteIds = results.map(res => res.noteId);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // force to load all the notes at once instead of one by one
 | 
					    // force to load all the notes at once instead of one by one
 | 
				
			||||||
    await treeCache.getNotes(noteIds);
 | 
					    await treeCache.getNotes(results.map(res => res.noteId));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const result of results) {
 | 
					    for (const result of results) {
 | 
				
			||||||
        const origBranch = await treeCache.getBranch(result.branchId);
 | 
					        const origBranch = await treeCache.getBranch(result.branchId);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,17 @@ import messagingService from "./messaging.js";
 | 
				
			|||||||
import server from "./server.js";
 | 
					import server from "./server.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TreeCache {
 | 
					class TreeCache {
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        this.init();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    load(noteRows, branchRows, relations) {
 | 
					    load(noteRows, branchRows, relations) {
 | 
				
			||||||
 | 
					        this.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.addResp(noteRows, branchRows, relations);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    init() {
 | 
				
			||||||
        this.parents = {};
 | 
					        this.parents = {};
 | 
				
			||||||
        this.children = {};
 | 
					        this.children = {};
 | 
				
			||||||
        this.childParentToBranch = {};
 | 
					        this.childParentToBranch = {};
 | 
				
			||||||
@@ -16,8 +26,6 @@ class TreeCache {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        /** @type {Object.<string, Branch>} */
 | 
					        /** @type {Object.<string, Branch>} */
 | 
				
			||||||
        this.branches = {};
 | 
					        this.branches = {};
 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.addResp(noteRows, branchRows, relations);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    addResp(noteRows, branchRows, relations) {
 | 
					    addResp(noteRows, branchRows, relations) {
 | 
				
			||||||
@@ -38,7 +46,7 @@ class TreeCache {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getNotes(noteIds) {
 | 
					    async getNotes(noteIds, silentNotFoundError = false) {
 | 
				
			||||||
        const missingNoteIds = noteIds.filter(noteId => this.notes[noteId] === undefined);
 | 
					        const missingNoteIds = noteIds.filter(noteId => this.notes[noteId] === undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (missingNoteIds.length > 0) {
 | 
					        if (missingNoteIds.length > 0) {
 | 
				
			||||||
@@ -48,7 +56,7 @@ class TreeCache {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return noteIds.map(noteId => {
 | 
					        return noteIds.map(noteId => {
 | 
				
			||||||
            if (!this.notes[noteId]) {
 | 
					            if (!this.notes[noteId] && !silentNotFoundError) {
 | 
				
			||||||
                messagingService.logError(`Can't find note ${noteId}`);
 | 
					                messagingService.logError(`Can't find note ${noteId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return null;
 | 
					                return null;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,7 +34,12 @@ function getNotePath(node) {
 | 
				
			|||||||
async function getNoteTitle(noteId, parentNoteId = null) {
 | 
					async function getNoteTitle(noteId, parentNoteId = null) {
 | 
				
			||||||
    utils.assertArguments(noteId);
 | 
					    utils.assertArguments(noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let {title} = await treeCache.getNote(noteId);
 | 
					    const note = await treeCache.getNote(noteId);
 | 
				
			||||||
 | 
					    if (!note) {
 | 
				
			||||||
 | 
					        return "[not found]";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let {title} = note;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (parentNoteId !== null) {
 | 
					    if (parentNoteId !== null) {
 | 
				
			||||||
        const branch = await treeCache.getBranchByChildParent(noteId, parentNoteId);
 | 
					        const branch = await treeCache.getBranchByChildParent(noteId, parentNoteId);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,10 @@ function formatTimeWithSeconds(date) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function formatDate(date) {
 | 
					function formatDate(date) {
 | 
				
			||||||
    return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear();
 | 
					//    return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear();
 | 
				
			||||||
 | 
					    // instead of european format we'll just use ISO as that's pretty unambiguous
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return formatDateISO(date);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function formatDateISO(date) {
 | 
					function formatDateISO(date) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -137,12 +137,14 @@
 | 
				
			|||||||
  CodeMirror.registerHelper("fold", "xml", function(cm, start) {
 | 
					  CodeMirror.registerHelper("fold", "xml", function(cm, start) {
 | 
				
			||||||
    var iter = new Iter(cm, start.line, 0);
 | 
					    var iter = new Iter(cm, start.line, 0);
 | 
				
			||||||
    for (;;) {
 | 
					    for (;;) {
 | 
				
			||||||
      var openTag = toNextTag(iter), end;
 | 
					      var openTag = toNextTag(iter)
 | 
				
			||||||
      if (!openTag || !(end = toTagEnd(iter)) || iter.line != start.line) return;
 | 
					      if (!openTag || iter.line != start.line) return
 | 
				
			||||||
 | 
					      var end = toTagEnd(iter)
 | 
				
			||||||
 | 
					      if (!end) return
 | 
				
			||||||
      if (!openTag[1] && end != "selfClose") {
 | 
					      if (!openTag[1] && end != "selfClose") {
 | 
				
			||||||
        var startPos = Pos(iter.line, iter.ch);
 | 
					        var startPos = Pos(iter.line, iter.ch);
 | 
				
			||||||
        var endPos = findMatchingClose(iter, openTag[2]);
 | 
					        var endPos = findMatchingClose(iter, openTag[2]);
 | 
				
			||||||
        return endPos && {from: startPos, to: endPos.from};
 | 
					        return endPos && cmp(endPos.from, startPos) > 0 ? {from: startPos, to: endPos.from} : null
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -90,7 +90,7 @@
 | 
				
			|||||||
    var state = cm.state.matchHighlighter;
 | 
					    var state = cm.state.matchHighlighter;
 | 
				
			||||||
    cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
 | 
					    cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
 | 
				
			||||||
    if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
 | 
					    if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
 | 
				
			||||||
      var searchFor = hasBoundary ? new RegExp("\\b" + query.replace(/[\\\[+*?(){|^$]/g, "\\$&") + "\\b") : query;
 | 
					      var searchFor = hasBoundary ? new RegExp("\\b" + query.replace(/[\\\[.+*?(){|^$]/g, "\\$&") + "\\b") : query;
 | 
				
			||||||
      state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
 | 
					      state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
 | 
				
			||||||
        {className: "CodeMirror-selection-highlight-scrollbar"});
 | 
					        {className: "CodeMirror-selection-highlight-scrollbar"});
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -746,6 +746,16 @@ function collapsedSpanAtSide(line, start) {
 | 
				
			|||||||
function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true) }
 | 
					function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true) }
 | 
				
			||||||
function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) }
 | 
					function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function collapsedSpanAround(line, ch) {
 | 
				
			||||||
 | 
					  var sps = sawCollapsedSpans && line.markedSpans, found
 | 
				
			||||||
 | 
					  if (sps) { for (var i = 0; i < sps.length; ++i) {
 | 
				
			||||||
 | 
					    var sp = sps[i]
 | 
				
			||||||
 | 
					    if (sp.marker.collapsed && (sp.from == null || sp.from < ch) && (sp.to == null || sp.to > ch) &&
 | 
				
			||||||
 | 
					        (!found || compareCollapsedMarkers(found, sp.marker) < 0)) { found = sp.marker }
 | 
				
			||||||
 | 
					  } }
 | 
				
			||||||
 | 
					  return found
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Test whether there exists a collapsed span that partially
 | 
					// Test whether there exists a collapsed span that partially
 | 
				
			||||||
// overlaps (covers the start or end, but not both) of a new span.
 | 
					// overlaps (covers the start or end, but not both) of a new span.
 | 
				
			||||||
// Such overlap is not allowed.
 | 
					// Such overlap is not allowed.
 | 
				
			||||||
@@ -2778,12 +2788,11 @@ function coordsChar(cm, x, y) {
 | 
				
			|||||||
  var lineObj = getLine(doc, lineN)
 | 
					  var lineObj = getLine(doc, lineN)
 | 
				
			||||||
  for (;;) {
 | 
					  for (;;) {
 | 
				
			||||||
    var found = coordsCharInner(cm, lineObj, lineN, x, y)
 | 
					    var found = coordsCharInner(cm, lineObj, lineN, x, y)
 | 
				
			||||||
    var merged = collapsedSpanAtEnd(lineObj)
 | 
					    var collapsed = collapsedSpanAround(lineObj, found.ch + (found.xRel > 0 ? 1 : 0))
 | 
				
			||||||
    var mergedPos = merged && merged.find(0, true)
 | 
					    if (!collapsed) { return found }
 | 
				
			||||||
    if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0))
 | 
					    var rangeEnd = collapsed.find(1)
 | 
				
			||||||
      { lineN = lineNo(lineObj = mergedPos.to.line) }
 | 
					    if (rangeEnd.line == lineN) { return rangeEnd }
 | 
				
			||||||
    else
 | 
					    lineObj = getLine(doc, lineN = rangeEnd.line)
 | 
				
			||||||
      { return found }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -3543,6 +3552,7 @@ var NativeScrollbars = function(place, scroll, cm) {
 | 
				
			|||||||
  this.cm = cm
 | 
					  this.cm = cm
 | 
				
			||||||
  var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar")
 | 
					  var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar")
 | 
				
			||||||
  var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar")
 | 
					  var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar")
 | 
				
			||||||
 | 
					  vert.tabIndex = horiz.tabIndex = -1
 | 
				
			||||||
  place(vert); place(horiz)
 | 
					  place(vert); place(horiz)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  on(vert, "scroll", function () {
 | 
					  on(vert, "scroll", function () {
 | 
				
			||||||
@@ -4783,7 +4793,7 @@ function addChangeToHistory(doc, change, selAfter, opId) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  if ((hist.lastOp == opId ||
 | 
					  if ((hist.lastOp == opId ||
 | 
				
			||||||
       hist.lastOrigin == change.origin && change.origin &&
 | 
					       hist.lastOrigin == change.origin && change.origin &&
 | 
				
			||||||
       ((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) ||
 | 
					       ((change.origin.charAt(0) == "+" && hist.lastModTime > time - (doc.cm ? doc.cm.options.historyEventDelay : 500)) ||
 | 
				
			||||||
        change.origin.charAt(0) == "*")) &&
 | 
					        change.origin.charAt(0) == "*")) &&
 | 
				
			||||||
      (cur = lastChangeEvent(hist, hist.lastOp == opId))) {
 | 
					      (cur = lastChangeEvent(hist, hist.lastOp == opId))) {
 | 
				
			||||||
    // Merge this change into the last event
 | 
					    // Merge this change into the last event
 | 
				
			||||||
@@ -5684,7 +5694,7 @@ LineWidget.prototype.changed = function () {
 | 
				
			|||||||
  this.height = null
 | 
					  this.height = null
 | 
				
			||||||
  var diff = widgetHeight(this) - oldH
 | 
					  var diff = widgetHeight(this) - oldH
 | 
				
			||||||
  if (!diff) { return }
 | 
					  if (!diff) { return }
 | 
				
			||||||
  updateLineHeight(line, line.height + diff)
 | 
					  if (!lineIsHidden(this.doc, line)) { updateLineHeight(line, line.height + diff) }
 | 
				
			||||||
  if (cm) {
 | 
					  if (cm) {
 | 
				
			||||||
    runInOp(cm, function () {
 | 
					    runInOp(cm, function () {
 | 
				
			||||||
      cm.curOp.forceUpdate = true
 | 
					      cm.curOp.forceUpdate = true
 | 
				
			||||||
@@ -6567,8 +6577,6 @@ function registerGlobalHandlers() {
 | 
				
			|||||||
// Called when the window resizes
 | 
					// Called when the window resizes
 | 
				
			||||||
function onResize(cm) {
 | 
					function onResize(cm) {
 | 
				
			||||||
  var d = cm.display
 | 
					  var d = cm.display
 | 
				
			||||||
  if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth)
 | 
					 | 
				
			||||||
    { return }
 | 
					 | 
				
			||||||
  // Might be a text scaling operation, clear size caches.
 | 
					  // Might be a text scaling operation, clear size caches.
 | 
				
			||||||
  d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null
 | 
					  d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null
 | 
				
			||||||
  d.scrollbarsClipped = false
 | 
					  d.scrollbarsClipped = false
 | 
				
			||||||
@@ -6614,7 +6622,7 @@ keyMap.pcDefault = {
 | 
				
			|||||||
  "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll",
 | 
					  "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll",
 | 
				
			||||||
  "Ctrl-[": "indentLess", "Ctrl-]": "indentMore",
 | 
					  "Ctrl-[": "indentLess", "Ctrl-]": "indentMore",
 | 
				
			||||||
  "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection",
 | 
					  "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection",
 | 
				
			||||||
  fallthrough: "basic"
 | 
					  "fallthrough": "basic"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
// Very basic readline/emacs-style bindings, which are standard on Mac.
 | 
					// Very basic readline/emacs-style bindings, which are standard on Mac.
 | 
				
			||||||
keyMap.emacsy = {
 | 
					keyMap.emacsy = {
 | 
				
			||||||
@@ -6632,7 +6640,7 @@ keyMap.macDefault = {
 | 
				
			|||||||
  "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll",
 | 
					  "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll",
 | 
				
			||||||
  "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight",
 | 
					  "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight",
 | 
				
			||||||
  "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd",
 | 
					  "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd",
 | 
				
			||||||
  fallthrough: ["basic", "emacsy"]
 | 
					  "fallthrough": ["basic", "emacsy"]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault
 | 
					keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -7312,8 +7320,8 @@ function leftButtonStartDrag(cm, event, pos, behavior) {
 | 
				
			|||||||
  var dragEnd = operation(cm, function (e) {
 | 
					  var dragEnd = operation(cm, function (e) {
 | 
				
			||||||
    if (webkit) { display.scroller.draggable = false }
 | 
					    if (webkit) { display.scroller.draggable = false }
 | 
				
			||||||
    cm.state.draggingText = false
 | 
					    cm.state.draggingText = false
 | 
				
			||||||
    off(document, "mouseup", dragEnd)
 | 
					    off(display.wrapper.ownerDocument, "mouseup", dragEnd)
 | 
				
			||||||
    off(document, "mousemove", mouseMove)
 | 
					    off(display.wrapper.ownerDocument, "mousemove", mouseMove)
 | 
				
			||||||
    off(display.scroller, "dragstart", dragStart)
 | 
					    off(display.scroller, "dragstart", dragStart)
 | 
				
			||||||
    off(display.scroller, "drop", dragEnd)
 | 
					    off(display.scroller, "drop", dragEnd)
 | 
				
			||||||
    if (!moved) {
 | 
					    if (!moved) {
 | 
				
			||||||
@@ -7322,7 +7330,7 @@ function leftButtonStartDrag(cm, event, pos, behavior) {
 | 
				
			|||||||
        { extendSelection(cm.doc, pos, null, null, behavior.extend) }
 | 
					        { extendSelection(cm.doc, pos, null, null, behavior.extend) }
 | 
				
			||||||
      // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081)
 | 
					      // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081)
 | 
				
			||||||
      if (webkit || ie && ie_version == 9)
 | 
					      if (webkit || ie && ie_version == 9)
 | 
				
			||||||
        { setTimeout(function () {document.body.focus(); display.input.focus()}, 20) }
 | 
					        { setTimeout(function () {display.wrapper.ownerDocument.body.focus(); display.input.focus()}, 20) }
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
        { display.input.focus() }
 | 
					        { display.input.focus() }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -7337,8 +7345,8 @@ function leftButtonStartDrag(cm, event, pos, behavior) {
 | 
				
			|||||||
  dragEnd.copy = !behavior.moveOnDrag
 | 
					  dragEnd.copy = !behavior.moveOnDrag
 | 
				
			||||||
  // IE's approach to draggable
 | 
					  // IE's approach to draggable
 | 
				
			||||||
  if (display.scroller.dragDrop) { display.scroller.dragDrop() }
 | 
					  if (display.scroller.dragDrop) { display.scroller.dragDrop() }
 | 
				
			||||||
  on(document, "mouseup", dragEnd)
 | 
					  on(display.wrapper.ownerDocument, "mouseup", dragEnd)
 | 
				
			||||||
  on(document, "mousemove", mouseMove)
 | 
					  on(display.wrapper.ownerDocument, "mousemove", mouseMove)
 | 
				
			||||||
  on(display.scroller, "dragstart", dragStart)
 | 
					  on(display.scroller, "dragstart", dragStart)
 | 
				
			||||||
  on(display.scroller, "drop", dragEnd)
 | 
					  on(display.scroller, "drop", dragEnd)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -7470,19 +7478,19 @@ function leftButtonSelect(cm, event, start, behavior) {
 | 
				
			|||||||
    counter = Infinity
 | 
					    counter = Infinity
 | 
				
			||||||
    e_preventDefault(e)
 | 
					    e_preventDefault(e)
 | 
				
			||||||
    display.input.focus()
 | 
					    display.input.focus()
 | 
				
			||||||
    off(document, "mousemove", move)
 | 
					    off(display.wrapper.ownerDocument, "mousemove", move)
 | 
				
			||||||
    off(document, "mouseup", up)
 | 
					    off(display.wrapper.ownerDocument, "mouseup", up)
 | 
				
			||||||
    doc.history.lastSelOrigin = null
 | 
					    doc.history.lastSelOrigin = null
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var move = operation(cm, function (e) {
 | 
					  var move = operation(cm, function (e) {
 | 
				
			||||||
    if (!e_button(e)) { done(e) }
 | 
					    if (e.buttons === 0 || !e_button(e)) { done(e) }
 | 
				
			||||||
    else { extend(e) }
 | 
					    else { extend(e) }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  var up = operation(cm, done)
 | 
					  var up = operation(cm, done)
 | 
				
			||||||
  cm.state.selectingText = up
 | 
					  cm.state.selectingText = up
 | 
				
			||||||
  on(document, "mousemove", move)
 | 
					  on(display.wrapper.ownerDocument, "mousemove", move)
 | 
				
			||||||
  on(document, "mouseup", up)
 | 
					  on(display.wrapper.ownerDocument, "mouseup", up)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Used when mouse-selecting to adjust the anchor to the proper side
 | 
					// Used when mouse-selecting to adjust the anchor to the proper side
 | 
				
			||||||
@@ -7765,6 +7773,7 @@ function CodeMirror(place, options) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  var doc = options.value
 | 
					  var doc = options.value
 | 
				
			||||||
  if (typeof doc == "string") { doc = new Doc(doc, options.mode, null, options.lineSeparator, options.direction) }
 | 
					  if (typeof doc == "string") { doc = new Doc(doc, options.mode, null, options.lineSeparator, options.direction) }
 | 
				
			||||||
 | 
					  else if (options.mode) { doc.modeOption = options.mode }
 | 
				
			||||||
  this.doc = doc
 | 
					  this.doc = doc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var input = new CodeMirror.inputStyles[options.inputStyle](this)
 | 
					  var input = new CodeMirror.inputStyles[options.inputStyle](this)
 | 
				
			||||||
@@ -8755,8 +8764,12 @@ ContentEditableInput.prototype.showSelection = function (info, takeFocus) {
 | 
				
			|||||||
  this.showMultipleSelections(info)
 | 
					  this.showMultipleSelections(info)
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ContentEditableInput.prototype.getSelection = function () {
 | 
				
			||||||
 | 
					  return this.cm.display.wrapper.ownerDocument.getSelection()
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ContentEditableInput.prototype.showPrimarySelection = function () {
 | 
					ContentEditableInput.prototype.showPrimarySelection = function () {
 | 
				
			||||||
  var sel = window.getSelection(), cm = this.cm, prim = cm.doc.sel.primary()
 | 
					  var sel = this.getSelection(), cm = this.cm, prim = cm.doc.sel.primary()
 | 
				
			||||||
  var from = prim.from(), to = prim.to()
 | 
					  var from = prim.from(), to = prim.to()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (cm.display.viewTo == cm.display.viewFrom || from.line >= cm.display.viewTo || to.line < cm.display.viewFrom) {
 | 
					  if (cm.display.viewTo == cm.display.viewFrom || from.line >= cm.display.viewTo || to.line < cm.display.viewFrom) {
 | 
				
			||||||
@@ -8823,13 +8836,13 @@ ContentEditableInput.prototype.showMultipleSelections = function (info) {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ContentEditableInput.prototype.rememberSelection = function () {
 | 
					ContentEditableInput.prototype.rememberSelection = function () {
 | 
				
			||||||
  var sel = window.getSelection()
 | 
					  var sel = this.getSelection()
 | 
				
			||||||
  this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset
 | 
					  this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset
 | 
				
			||||||
  this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset
 | 
					  this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ContentEditableInput.prototype.selectionInEditor = function () {
 | 
					ContentEditableInput.prototype.selectionInEditor = function () {
 | 
				
			||||||
  var sel = window.getSelection()
 | 
					  var sel = this.getSelection()
 | 
				
			||||||
  if (!sel.rangeCount) { return false }
 | 
					  if (!sel.rangeCount) { return false }
 | 
				
			||||||
  var node = sel.getRangeAt(0).commonAncestorContainer
 | 
					  var node = sel.getRangeAt(0).commonAncestorContainer
 | 
				
			||||||
  return contains(this.div, node)
 | 
					  return contains(this.div, node)
 | 
				
			||||||
@@ -8864,14 +8877,14 @@ ContentEditableInput.prototype.receivedFocus = function () {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ContentEditableInput.prototype.selectionChanged = function () {
 | 
					ContentEditableInput.prototype.selectionChanged = function () {
 | 
				
			||||||
  var sel = window.getSelection()
 | 
					  var sel = this.getSelection()
 | 
				
			||||||
  return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||
 | 
					  return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||
 | 
				
			||||||
    sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset
 | 
					    sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ContentEditableInput.prototype.pollSelection = function () {
 | 
					ContentEditableInput.prototype.pollSelection = function () {
 | 
				
			||||||
  if (this.readDOMTimeout != null || this.gracePeriod || !this.selectionChanged()) { return }
 | 
					  if (this.readDOMTimeout != null || this.gracePeriod || !this.selectionChanged()) { return }
 | 
				
			||||||
  var sel = window.getSelection(), cm = this.cm
 | 
					  var sel = this.getSelection(), cm = this.cm
 | 
				
			||||||
  // On Android Chrome (version 56, at least), backspacing into an
 | 
					  // On Android Chrome (version 56, at least), backspacing into an
 | 
				
			||||||
  // uneditable block element will put the cursor in that element,
 | 
					  // uneditable block element will put the cursor in that element,
 | 
				
			||||||
  // and then, because it's not editable, hide the virtual keyboard.
 | 
					  // and then, because it's not editable, hide the virtual keyboard.
 | 
				
			||||||
@@ -9005,7 +9018,7 @@ ContentEditableInput.prototype.setUneditable = function (node) {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ContentEditableInput.prototype.onKeyPress = function (e) {
 | 
					ContentEditableInput.prototype.onKeyPress = function (e) {
 | 
				
			||||||
  if (e.charCode == 0) { return }
 | 
					  if (e.charCode == 0 || this.composing) { return }
 | 
				
			||||||
  e.preventDefault()
 | 
					  e.preventDefault()
 | 
				
			||||||
  if (!this.cm.isReadOnly())
 | 
					  if (!this.cm.isReadOnly())
 | 
				
			||||||
    { operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0) }
 | 
					    { operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0) }
 | 
				
			||||||
@@ -9045,12 +9058,13 @@ function isInGutter(node) {
 | 
				
			|||||||
function badPos(pos, bad) { if (bad) { pos.bad = true; } return pos }
 | 
					function badPos(pos, bad) { if (bad) { pos.bad = true; } return pos }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function domTextBetween(cm, from, to, fromLine, toLine) {
 | 
					function domTextBetween(cm, from, to, fromLine, toLine) {
 | 
				
			||||||
  var text = "", closing = false, lineSep = cm.doc.lineSeparator()
 | 
					  var text = "", closing = false, lineSep = cm.doc.lineSeparator(), extraLinebreak = false
 | 
				
			||||||
  function recognizeMarker(id) { return function (marker) { return marker.id == id; } }
 | 
					  function recognizeMarker(id) { return function (marker) { return marker.id == id; } }
 | 
				
			||||||
  function close() {
 | 
					  function close() {
 | 
				
			||||||
    if (closing) {
 | 
					    if (closing) {
 | 
				
			||||||
      text += lineSep
 | 
					      text += lineSep
 | 
				
			||||||
      closing = false
 | 
					      if (extraLinebreak) { text += lineSep }
 | 
				
			||||||
 | 
					      closing = extraLinebreak = false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  function addText(str) {
 | 
					  function addText(str) {
 | 
				
			||||||
@@ -9062,8 +9076,8 @@ function domTextBetween(cm, from, to, fromLine, toLine) {
 | 
				
			|||||||
  function walk(node) {
 | 
					  function walk(node) {
 | 
				
			||||||
    if (node.nodeType == 1) {
 | 
					    if (node.nodeType == 1) {
 | 
				
			||||||
      var cmText = node.getAttribute("cm-text")
 | 
					      var cmText = node.getAttribute("cm-text")
 | 
				
			||||||
      if (cmText != null) {
 | 
					      if (cmText) {
 | 
				
			||||||
        addText(cmText || node.textContent.replace(/\u200b/g, ""))
 | 
					        addText(cmText)
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      var markerID = node.getAttribute("cm-marker"), range
 | 
					      var markerID = node.getAttribute("cm-marker"), range
 | 
				
			||||||
@@ -9074,19 +9088,24 @@ function domTextBetween(cm, from, to, fromLine, toLine) {
 | 
				
			|||||||
        return
 | 
					        return
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (node.getAttribute("contenteditable") == "false") { return }
 | 
					      if (node.getAttribute("contenteditable") == "false") { return }
 | 
				
			||||||
      var isBlock = /^(pre|div|p)$/i.test(node.nodeName)
 | 
					      var isBlock = /^(pre|div|p|li|table|br)$/i.test(node.nodeName)
 | 
				
			||||||
 | 
					      if (!/^br$/i.test(node.nodeName) && node.textContent.length == 0) { return }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (isBlock) { close() }
 | 
					      if (isBlock) { close() }
 | 
				
			||||||
      for (var i = 0; i < node.childNodes.length; i++)
 | 
					      for (var i = 0; i < node.childNodes.length; i++)
 | 
				
			||||||
        { walk(node.childNodes[i]) }
 | 
					        { walk(node.childNodes[i]) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (/^(pre|p)$/i.test(node.nodeName)) { extraLinebreak = true }
 | 
				
			||||||
      if (isBlock) { closing = true }
 | 
					      if (isBlock) { closing = true }
 | 
				
			||||||
    } else if (node.nodeType == 3) {
 | 
					    } else if (node.nodeType == 3) {
 | 
				
			||||||
      addText(node.nodeValue)
 | 
					      addText(node.nodeValue.replace(/\u200b/g, "").replace(/\u00a0/g, " "))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  for (;;) {
 | 
					  for (;;) {
 | 
				
			||||||
    walk(from)
 | 
					    walk(from)
 | 
				
			||||||
    if (from == to) { break }
 | 
					    if (from == to) { break }
 | 
				
			||||||
    from = from.nextSibling
 | 
					    from = from.nextSibling
 | 
				
			||||||
 | 
					    extraLinebreak = false
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return text
 | 
					  return text
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -9187,13 +9206,10 @@ TextareaInput.prototype.init = function (display) {
 | 
				
			|||||||
    var this$1 = this;
 | 
					    var this$1 = this;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var input = this, cm = this.cm
 | 
					  var input = this, cm = this.cm
 | 
				
			||||||
 | 
					  this.createField(display)
 | 
				
			||||||
 | 
					  var te = this.textarea
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Wraps and hides input textarea
 | 
					  display.wrapper.insertBefore(this.wrapper, display.wrapper.firstChild)
 | 
				
			||||||
  var div = this.wrapper = hiddenTextarea()
 | 
					 | 
				
			||||||
  // The semihidden textarea that is focused when the editor is
 | 
					 | 
				
			||||||
  // focused, and receives input.
 | 
					 | 
				
			||||||
  var te = this.textarea = div.firstChild
 | 
					 | 
				
			||||||
  display.wrapper.insertBefore(div, display.wrapper.firstChild)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore)
 | 
					  // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore)
 | 
				
			||||||
  if (ios) { te.style.width = "0px" }
 | 
					  if (ios) { te.style.width = "0px" }
 | 
				
			||||||
@@ -9260,6 +9276,14 @@ TextareaInput.prototype.init = function (display) {
 | 
				
			|||||||
  })
 | 
					  })
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TextareaInput.prototype.createField = function (_display) {
 | 
				
			||||||
 | 
					  // Wraps and hides input textarea
 | 
				
			||||||
 | 
					  this.wrapper = hiddenTextarea()
 | 
				
			||||||
 | 
					  // The semihidden textarea that is focused when the editor is
 | 
				
			||||||
 | 
					  // focused, and receives input.
 | 
				
			||||||
 | 
					  this.textarea = this.wrapper.firstChild
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
TextareaInput.prototype.prepareSelection = function () {
 | 
					TextareaInput.prototype.prepareSelection = function () {
 | 
				
			||||||
  // Redraw the selection and/or cursor
 | 
					  // Redraw the selection and/or cursor
 | 
				
			||||||
  var cm = this.cm, display = cm.display, doc = cm.doc
 | 
					  var cm = this.cm, display = cm.display, doc = cm.doc
 | 
				
			||||||
@@ -9653,7 +9677,7 @@ CodeMirror.fromTextArea = fromTextArea
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
addLegacyProps(CodeMirror)
 | 
					addLegacyProps(CodeMirror)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CodeMirror.version = "5.35.0"
 | 
					CodeMirror.version = "5.39.2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
return CodeMirror;
 | 
					return CodeMirror;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -216,15 +216,15 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
 | 
				
			|||||||
    indent: function(state, textAfter) {
 | 
					    indent: function(state, textAfter) {
 | 
				
			||||||
      if (state.tokenize != tokenBase && state.tokenize != null || state.typeAtEndOfLine) return CodeMirror.Pass;
 | 
					      if (state.tokenize != tokenBase && state.tokenize != null || state.typeAtEndOfLine) return CodeMirror.Pass;
 | 
				
			||||||
      var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
 | 
					      var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
 | 
				
			||||||
 | 
					      var closing = firstChar == ctx.type;
 | 
				
			||||||
      if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev;
 | 
					      if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev;
 | 
				
			||||||
      if (parserConfig.dontIndentStatements)
 | 
					      if (parserConfig.dontIndentStatements)
 | 
				
			||||||
        while (ctx.type == "statement" && parserConfig.dontIndentStatements.test(ctx.info))
 | 
					        while (ctx.type == "statement" && parserConfig.dontIndentStatements.test(ctx.info))
 | 
				
			||||||
          ctx = ctx.prev
 | 
					          ctx = ctx.prev
 | 
				
			||||||
      if (hooks.indent) {
 | 
					      if (hooks.indent) {
 | 
				
			||||||
        var hook = hooks.indent(state, ctx, textAfter);
 | 
					        var hook = hooks.indent(state, ctx, textAfter, indentUnit);
 | 
				
			||||||
        if (typeof hook == "number") return hook
 | 
					        if (typeof hook == "number") return hook
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      var closing = firstChar == ctx.type;
 | 
					 | 
				
			||||||
      var switchBlock = ctx.prev && ctx.prev.info == "switch";
 | 
					      var switchBlock = ctx.prev && ctx.prev.info == "switch";
 | 
				
			||||||
      if (parserConfig.allmanIndentation && /[{(]/.test(firstChar)) {
 | 
					      if (parserConfig.allmanIndentation && /[{(]/.test(firstChar)) {
 | 
				
			||||||
        while (ctx.type != "top" && ctx.type != "}") ctx = ctx.prev
 | 
					        while (ctx.type != "top" && ctx.type != "}") ctx = ctx.prev
 | 
				
			||||||
@@ -374,7 +374,7 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
 | 
				
			|||||||
    blockKeywords: words("case do else for if switch while struct"),
 | 
					    blockKeywords: words("case do else for if switch while struct"),
 | 
				
			||||||
    defKeywords: words("struct"),
 | 
					    defKeywords: words("struct"),
 | 
				
			||||||
    typeFirstDefinitions: true,
 | 
					    typeFirstDefinitions: true,
 | 
				
			||||||
    atoms: words("null true false"),
 | 
					    atoms: words("NULL true false"),
 | 
				
			||||||
    hooks: {"#": cppHook, "*": pointerHook},
 | 
					    hooks: {"#": cppHook, "*": pointerHook},
 | 
				
			||||||
    modeProps: {fold: ["brace", "include"]}
 | 
					    modeProps: {fold: ["brace", "include"]}
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@@ -390,7 +390,7 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
 | 
				
			|||||||
    blockKeywords: words("catch class do else finally for if struct switch try while"),
 | 
					    blockKeywords: words("catch class do else finally for if struct switch try while"),
 | 
				
			||||||
    defKeywords: words("class namespace struct enum union"),
 | 
					    defKeywords: words("class namespace struct enum union"),
 | 
				
			||||||
    typeFirstDefinitions: true,
 | 
					    typeFirstDefinitions: true,
 | 
				
			||||||
    atoms: words("true false null"),
 | 
					    atoms: words("true false NULL"),
 | 
				
			||||||
    dontIndentStatements: /^template$/,
 | 
					    dontIndentStatements: /^template$/,
 | 
				
			||||||
    isIdentifierChar: /[\w\$_~\xa1-\uffff]/,
 | 
					    isIdentifierChar: /[\w\$_~\xa1-\uffff]/,
 | 
				
			||||||
    hooks: {
 | 
					    hooks: {
 | 
				
			||||||
@@ -597,34 +597,51 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
 | 
				
			|||||||
    name: "clike",
 | 
					    name: "clike",
 | 
				
			||||||
    keywords: words(
 | 
					    keywords: words(
 | 
				
			||||||
      /*keywords*/
 | 
					      /*keywords*/
 | 
				
			||||||
      "package as typealias class interface this super val " +
 | 
					      "package as typealias class interface this super val operator " +
 | 
				
			||||||
      "var fun for is in This throw return " +
 | 
					      "var fun for is in This throw return annotation " +
 | 
				
			||||||
      "break continue object if else while do try when !in !is as? " +
 | 
					      "break continue object if else while do try when !in !is as? " +
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      /*soft keywords*/
 | 
					      /*soft keywords*/
 | 
				
			||||||
      "file import where by get set abstract enum open inner override private public internal " +
 | 
					      "file import where by get set abstract enum open inner override private public internal " +
 | 
				
			||||||
      "protected catch finally out final vararg reified dynamic companion constructor init " +
 | 
					      "protected catch finally out final vararg reified dynamic companion constructor init " +
 | 
				
			||||||
      "sealed field property receiver param sparam lateinit data inline noinline tailrec " +
 | 
					      "sealed field property receiver param sparam lateinit data inline noinline tailrec " +
 | 
				
			||||||
      "external annotation crossinline const operator infix suspend actual expect"
 | 
					      "external annotation crossinline const operator infix suspend actual expect setparam"
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    types: words(
 | 
					    types: words(
 | 
				
			||||||
      /* package java.lang */
 | 
					      /* package java.lang */
 | 
				
			||||||
      "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " +
 | 
					      "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " +
 | 
				
			||||||
      "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " +
 | 
					      "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " +
 | 
				
			||||||
      "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " +
 | 
					      "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " +
 | 
				
			||||||
      "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void"
 | 
					      "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void Annotation Any BooleanArray " +
 | 
				
			||||||
 | 
					      "ByteArray Char CharArray DeprecationLevel DoubleArray Enum FloatArray Function Int IntArray Lazy " +
 | 
				
			||||||
 | 
					      "LazyThreadSafetyMode LongArray Nothing ShortArray Unit"
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    intendSwitch: false,
 | 
					    intendSwitch: false,
 | 
				
			||||||
    indentStatements: false,
 | 
					    indentStatements: false,
 | 
				
			||||||
    multiLineStrings: true,
 | 
					    multiLineStrings: true,
 | 
				
			||||||
    number: /^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+\.?\d*|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i,
 | 
					    number: /^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+(\.\d+)?|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i,
 | 
				
			||||||
    blockKeywords: words("catch class do else finally for if where try while enum"),
 | 
					    blockKeywords: words("catch class do else finally for if where try while enum"),
 | 
				
			||||||
    defKeywords: words("class val var object interface fun"),
 | 
					    defKeywords: words("class val var object interface fun"),
 | 
				
			||||||
    atoms: words("true false null this"),
 | 
					    atoms: words("true false null this"),
 | 
				
			||||||
    hooks: {
 | 
					    hooks: {
 | 
				
			||||||
 | 
					      "@": function(stream) {
 | 
				
			||||||
 | 
					        stream.eatWhile(/[\w\$_]/);
 | 
				
			||||||
 | 
					        return "meta";
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      '"': function(stream, state) {
 | 
					      '"': function(stream, state) {
 | 
				
			||||||
        state.tokenize = tokenKotlinString(stream.match('""'));
 | 
					        state.tokenize = tokenKotlinString(stream.match('""'));
 | 
				
			||||||
        return state.tokenize(stream, state);
 | 
					        return state.tokenize(stream, state);
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      indent: function(state, ctx, textAfter, indentUnit) {
 | 
				
			||||||
 | 
					        var firstChar = textAfter && textAfter.charAt(0);
 | 
				
			||||||
 | 
					        if ((state.prevToken == "}" || state.prevToken == ")") && textAfter == "")
 | 
				
			||||||
 | 
					          return state.indented;
 | 
				
			||||||
 | 
					        if (state.prevToken == "operator" && textAfter != "}" ||
 | 
				
			||||||
 | 
					          state.prevToken == "variable" && firstChar == "." ||
 | 
				
			||||||
 | 
					          (state.prevToken == "}" || state.prevToken == ")") && firstChar == ".")
 | 
				
			||||||
 | 
					          return indentUnit * 2 + ctx.indented;
 | 
				
			||||||
 | 
					        if (ctx.align && ctx.type == "}")
 | 
				
			||||||
 | 
					          return ctx.indented + (state.context.type == (textAfter || "").charAt(0) ? 0 : indentUnit);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    modeProps: {closeBrackets: {triples: '"'}}
 | 
					    modeProps: {closeBrackets: {triples: '"'}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,30 +11,64 @@
 | 
				
			|||||||
})(function(CodeMirror) {
 | 
					})(function(CodeMirror) {
 | 
				
			||||||
  "use strict";
 | 
					  "use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var from = "from";
 | 
				
			||||||
 | 
					  var fromRegex = new RegExp("^(\\s*)\\b(" + from + ")\\b", "i");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var shells = ["run", "cmd", "entrypoint", "shell"];
 | 
				
			||||||
 | 
					  var shellsAsArrayRegex = new RegExp("^(\\s*)(" + shells.join('|') + ")(\\s+\\[)", "i");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var expose = "expose";
 | 
				
			||||||
 | 
					  var exposeRegex = new RegExp("^(\\s*)(" + expose + ")(\\s+)", "i");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var others = [
 | 
				
			||||||
 | 
					    "arg", "from", "maintainer", "label", "env",
 | 
				
			||||||
 | 
					    "add", "copy", "volume", "user",
 | 
				
			||||||
 | 
					    "workdir", "onbuild", "stopsignal", "healthcheck", "shell"
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Collect all Dockerfile directives
 | 
					  // Collect all Dockerfile directives
 | 
				
			||||||
  var instructions = ["from", "maintainer", "run", "cmd", "expose", "env",
 | 
					  var instructions = [from, expose].concat(shells).concat(others),
 | 
				
			||||||
                      "add", "copy", "entrypoint", "volume", "user",
 | 
					 | 
				
			||||||
                      "workdir", "onbuild"],
 | 
					 | 
				
			||||||
      instructionRegex = "(" + instructions.join('|') + ")",
 | 
					      instructionRegex = "(" + instructions.join('|') + ")",
 | 
				
			||||||
      instructionOnlyLine = new RegExp(instructionRegex + "\\s*$", "i"),
 | 
					      instructionOnlyLine = new RegExp("^(\\s*)" + instructionRegex + "(\\s*)(#.*)?$", "i"),
 | 
				
			||||||
      instructionWithArguments = new RegExp(instructionRegex + "(\\s+)", "i");
 | 
					      instructionWithArguments = new RegExp("^(\\s*)" + instructionRegex + "(\\s+)", "i");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  CodeMirror.defineSimpleMode("dockerfile", {
 | 
					  CodeMirror.defineSimpleMode("dockerfile", {
 | 
				
			||||||
    start: [
 | 
					    start: [
 | 
				
			||||||
      // Block comment: This is a line starting with a comment
 | 
					      // Block comment: This is a line starting with a comment
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        regex: /#.*$/,
 | 
					        regex: /^\s*#.*$/,
 | 
				
			||||||
 | 
					        sol: true,
 | 
				
			||||||
        token: "comment"
 | 
					        token: "comment"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: fromRegex,
 | 
				
			||||||
 | 
					        token: [null, "keyword"],
 | 
				
			||||||
 | 
					        sol: true,
 | 
				
			||||||
 | 
					        next: "from"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      // Highlight an instruction without any arguments (for convenience)
 | 
					      // Highlight an instruction without any arguments (for convenience)
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        regex: instructionOnlyLine,
 | 
					        regex: instructionOnlyLine,
 | 
				
			||||||
        token: "variable-2"
 | 
					        token: [null, "keyword", null, "error"],
 | 
				
			||||||
 | 
					        sol: true
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: shellsAsArrayRegex,
 | 
				
			||||||
 | 
					        token: [null, "keyword", null],
 | 
				
			||||||
 | 
					        sol: true,
 | 
				
			||||||
 | 
					        next: "array"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: exposeRegex,
 | 
				
			||||||
 | 
					        token: [null, "keyword", null],
 | 
				
			||||||
 | 
					        sol: true,
 | 
				
			||||||
 | 
					        next: "expose"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      // Highlight an instruction followed by arguments
 | 
					      // Highlight an instruction followed by arguments
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        regex: instructionWithArguments,
 | 
					        regex: instructionWithArguments,
 | 
				
			||||||
        token: ["variable-2", null],
 | 
					        token: [null, "keyword", null],
 | 
				
			||||||
 | 
					        sol: true,
 | 
				
			||||||
        next: "arguments"
 | 
					        next: "arguments"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
@@ -42,27 +76,125 @@
 | 
				
			|||||||
        token: null
 | 
					        token: null
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    arguments: [
 | 
					    from: [
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        // Line comment without instruction arguments is an error
 | 
					        regex: /\s*$/,
 | 
				
			||||||
        regex: /#.*$/,
 | 
					        token: null,
 | 
				
			||||||
        token: "error",
 | 
					 | 
				
			||||||
        next: "start"
 | 
					        next: "start"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        regex: /[^#]+\\$/,
 | 
					        // Line comment without instruction arguments is an error
 | 
				
			||||||
 | 
					        regex: /(\s*)(#.*)$/,
 | 
				
			||||||
 | 
					        token: [null, "error"],
 | 
				
			||||||
 | 
					        next: "start"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /(\s*\S+\s+)(as)/i,
 | 
				
			||||||
 | 
					        token: [null, "keyword"],
 | 
				
			||||||
 | 
					        next: "start"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      // Fail safe return to start
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        token: null,
 | 
				
			||||||
 | 
					        next: "start"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    single: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /(?:[^\\']|\\.)/,
 | 
				
			||||||
 | 
					        token: "string"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /'/,
 | 
				
			||||||
 | 
					        token: "string",
 | 
				
			||||||
 | 
					        pop: true
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    double: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /(?:[^\\"]|\\.)/,
 | 
				
			||||||
 | 
					        token: "string"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /"/,
 | 
				
			||||||
 | 
					        token: "string",
 | 
				
			||||||
 | 
					        pop: true
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    array: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /\]/,
 | 
				
			||||||
 | 
					        token: null,
 | 
				
			||||||
 | 
					        next: "start"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /"(?:[^\\"]|\\.)*"?/,
 | 
				
			||||||
 | 
					        token: "string"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    expose: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /\d+$/,
 | 
				
			||||||
 | 
					        token: "number",
 | 
				
			||||||
 | 
					        next: "start"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /[^\d]+$/,
 | 
				
			||||||
 | 
					        token: null,
 | 
				
			||||||
 | 
					        next: "start"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /\d+/,
 | 
				
			||||||
 | 
					        token: "number"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /[^\d]+/,
 | 
				
			||||||
 | 
					        token: null
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      // Fail safe return to start
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        token: null,
 | 
				
			||||||
 | 
					        next: "start"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    arguments: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /^\s*#.*$/,
 | 
				
			||||||
 | 
					        sol: true,
 | 
				
			||||||
 | 
					        token: "comment"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /"(?:[^\\"]|\\.)*"?$/,
 | 
				
			||||||
 | 
					        token: "string",
 | 
				
			||||||
 | 
					        next: "start"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /"/,
 | 
				
			||||||
 | 
					        token: "string",
 | 
				
			||||||
 | 
					        push: "double"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /'(?:[^\\']|\\.)*'?$/,
 | 
				
			||||||
 | 
					        token: "string",
 | 
				
			||||||
 | 
					        next: "start"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /'/,
 | 
				
			||||||
 | 
					        token: "string",
 | 
				
			||||||
 | 
					        push: "single"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        regex: /[^#"']+[\\`]$/,
 | 
				
			||||||
        token: null
 | 
					        token: null
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        // Match everything except for the inline comment
 | 
					        regex: /[^#"']+$/,
 | 
				
			||||||
        regex: /[^#]+/,
 | 
					 | 
				
			||||||
        token: null,
 | 
					        token: null,
 | 
				
			||||||
        next: "start"
 | 
					        next: "start"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        regex: /$/,
 | 
					        regex: /[^#"']+/,
 | 
				
			||||||
        token: null,
 | 
					        token: null
 | 
				
			||||||
        next: "start"
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      // Fail safe return to start
 | 
					      // Fail safe return to start
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										128
									
								
								src/public/libraries/codemirror/mode/dockerfile/test.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								src/public/libraries/codemirror/mode/dockerfile/test.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					// CodeMirror, copyright (c) by Marijn Haverbeke and others
 | 
				
			||||||
 | 
					// Distributed under an MIT license: http://codemirror.net/LICENSE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					(function() {
 | 
				
			||||||
 | 
					  var mode = CodeMirror.getMode({indentUnit: 2}, "text/x-dockerfile");
 | 
				
			||||||
 | 
					  function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("simple_nodejs_dockerfile",
 | 
				
			||||||
 | 
					     "[keyword FROM] node:carbon",
 | 
				
			||||||
 | 
					     "[comment # Create app directory]",
 | 
				
			||||||
 | 
					     "[keyword WORKDIR] /usr/src/app",
 | 
				
			||||||
 | 
					     "[comment # Install app dependencies]",
 | 
				
			||||||
 | 
					     "[comment # A wildcard is used to ensure both package.json AND package-lock.json are copied]",
 | 
				
			||||||
 | 
					     "[comment # where available (npm@5+)]",
 | 
				
			||||||
 | 
					     "[keyword COPY] package*.json ./",
 | 
				
			||||||
 | 
					     "[keyword RUN] npm install",
 | 
				
			||||||
 | 
					     "[keyword COPY] . .",
 | 
				
			||||||
 | 
					     "[keyword EXPOSE] [number 8080] [number 3000]",
 | 
				
			||||||
 | 
					     "[keyword ENV] NODE_ENV development",
 | 
				
			||||||
 | 
					     "[keyword CMD] [[ [string \"npm\"], [string \"start\"] ]]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Ideally the last space should not be highlighted.
 | 
				
			||||||
 | 
					  MT("instruction_without_args_1",
 | 
				
			||||||
 | 
					     "[keyword CMD] ");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("instruction_without_args_2",
 | 
				
			||||||
 | 
					     "[comment # An instruction without args...]",
 | 
				
			||||||
 | 
					     "[keyword ARG] [error #...is an error]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("multiline",
 | 
				
			||||||
 | 
					     "[keyword RUN] apt-get update && apt-get install -y \\",
 | 
				
			||||||
 | 
					     "  mercurial \\",
 | 
				
			||||||
 | 
					     "  subversion \\",
 | 
				
			||||||
 | 
					     "  && apt-get clean \\",
 | 
				
			||||||
 | 
					     "  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("from_comment",
 | 
				
			||||||
 | 
					     "  [keyword FROM] debian:stretch # I tend to use stable as that is more stable",
 | 
				
			||||||
 | 
					     "  [keyword FROM] debian:stretch [keyword AS] stable # I am even more stable",
 | 
				
			||||||
 | 
					     " [keyword FROM] [error # this is an error]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("from_as",
 | 
				
			||||||
 | 
					     "[keyword FROM] golang:1.9.2-alpine3.6 [keyword AS] build",
 | 
				
			||||||
 | 
					     "[keyword COPY] --from=build /bin/project /bin/project",
 | 
				
			||||||
 | 
					     "[keyword ENTRYPOINT] [[ [string \"/bin/project\"] ]]",
 | 
				
			||||||
 | 
					     "[keyword CMD] [[ [string \"--help\"] ]]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("arg",
 | 
				
			||||||
 | 
					     "[keyword ARG] VERSION=latest",
 | 
				
			||||||
 | 
					     "[keyword FROM] busybox:$VERSION",
 | 
				
			||||||
 | 
					     "[keyword ARG] VERSION",
 | 
				
			||||||
 | 
					     "[keyword RUN] echo $VERSION > image_version");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("label",
 | 
				
			||||||
 | 
					     "[keyword LABEL] com.example.label-with-value=[string \"foo\"]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("label_multiline",
 | 
				
			||||||
 | 
					     "[keyword LABEL] description=[string \"This text illustrates ]\\",
 | 
				
			||||||
 | 
					     "[string that label-values can span multiple lines.\"]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("maintainer",
 | 
				
			||||||
 | 
					     "[keyword MAINTAINER] Foo Bar [string \"foo@bar.com\"] ",
 | 
				
			||||||
 | 
					     "[keyword MAINTAINER] Bar Baz <bar@baz.com>");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("env",
 | 
				
			||||||
 | 
					     "[keyword ENV] BUNDLE_PATH=[string \"$GEM_HOME\"] \\",
 | 
				
			||||||
 | 
					     "  BUNDLE_APP_CONFIG=[string \"$GEM_HOME\"]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("verify_keyword",
 | 
				
			||||||
 | 
					     "[keyword RUN] add-apt-repository ppa:chris-lea/node.js");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("scripts",
 | 
				
			||||||
 | 
					     "[comment # Set an entrypoint, to automatically install node modules]",
 | 
				
			||||||
 | 
					     "[keyword ENTRYPOINT] [[ [string \"/bin/bash\"], [string \"-c\"], [string \"if [[ ! -d node_modules ]]; then npm install; fi; exec \\\"${@:0}\\\";\"] ]]",
 | 
				
			||||||
 | 
					     "[keyword CMD] npm start",
 | 
				
			||||||
 | 
					     "[keyword RUN] npm run build && \\",
 | 
				
			||||||
 | 
					     "[comment # a comment between the shell commands]",
 | 
				
			||||||
 | 
					     "  npm run test");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("strings_single",
 | 
				
			||||||
 | 
					     "[keyword FROM] buildpack-deps:stretch",
 | 
				
			||||||
 | 
					     "[keyword RUN] { \\",
 | 
				
			||||||
 | 
					     "        echo [string 'install: --no-document']; \\",
 | 
				
			||||||
 | 
					     "        echo [string 'update: --no-document']; \\",
 | 
				
			||||||
 | 
					     "    } >> /usr/local/etc/gemrc");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("strings_single_multiline",
 | 
				
			||||||
 | 
					     "[keyword RUN] set -ex \\",
 | 
				
			||||||
 | 
					     "    \\",
 | 
				
			||||||
 | 
					     "    && buildDeps=[string ' ]\\",
 | 
				
			||||||
 | 
					     "[string        bison ]\\",
 | 
				
			||||||
 | 
					     "[string        dpkg-dev ]\\",
 | 
				
			||||||
 | 
					     "[string        libgdbm-dev ]\\",
 | 
				
			||||||
 | 
					     "[string        ruby ]\\",
 | 
				
			||||||
 | 
					     "[string    '] \\",
 | 
				
			||||||
 | 
					     "    && apt-get update");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("strings_single_multiline_2",
 | 
				
			||||||
 | 
					     "[keyword RUN] echo [string 'say \\' ]\\",
 | 
				
			||||||
 | 
					     "[string   it works'] ");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("strings_double",
 | 
				
			||||||
 | 
					     "[keyword RUN] apt-get install -y --no-install-recommends $buildDeps \\",
 | 
				
			||||||
 | 
					     " \\",
 | 
				
			||||||
 | 
					     " && wget -O ruby.tar.xz [string \"https://cache.ruby-lang.org/pub/ruby/${RUBY_MAJOR%-rc}/ruby-$RUBY_VERSION.tar.xz\"] \\",
 | 
				
			||||||
 | 
					     " && echo [string \"$RUBY_DOWNLOAD_SHA256 *ruby.tar.xz\"] | sha256sum -c - ");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("strings_double_multiline",
 | 
				
			||||||
 | 
					     "[keyword RUN] echo [string \"say \\\" ]\\",
 | 
				
			||||||
 | 
					     "[string   it works\"] ");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("escape",
 | 
				
			||||||
 | 
					     "[comment # escape=`]",
 | 
				
			||||||
 | 
					     "[keyword FROM] microsoft/windowsservercore",
 | 
				
			||||||
 | 
					     "[keyword RUN] powershell.exe -Command `",
 | 
				
			||||||
 | 
					     "    $ErrorActionPreference = [string 'Stop']; `",
 | 
				
			||||||
 | 
					     "    wget https://www.python.org/ftp/python/3.5.1/python-3.5.1.exe -OutFile c:\python-3.5.1.exe ; `",
 | 
				
			||||||
 | 
					     "    Start-Process c:\python-3.5.1.exe -ArgumentList [string '/quiet InstallAllUsers=1 PrependPath=1'] -Wait ; `",
 | 
				
			||||||
 | 
					     "    Remove-Item c:\python-3.5.1.exe -Force)");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("escape_strings",
 | 
				
			||||||
 | 
					     "[comment # escape=`]",
 | 
				
			||||||
 | 
					     "[keyword FROM] python:3.6-windowsservercore [keyword AS] python",
 | 
				
			||||||
 | 
					     "[keyword RUN] $env:PATH = [string 'C:\\Python;C:\\Python\\Scripts;{0}'] -f $env:PATH ; `",
 | 
				
			||||||
 | 
					     // It should not consider \' as escaped.
 | 
				
			||||||
 | 
					     // "  Set-ItemProperty -Path [string 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\\'] -Name Path -Value $env:PATH ;");
 | 
				
			||||||
 | 
					     "  Set-ItemProperty -Path [string 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\\' -Name Path -Value $env:PATH ;]");
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
@@ -46,7 +46,11 @@
 | 
				
			|||||||
    comment: [
 | 
					    comment: [
 | 
				
			||||||
      { regex: /\}\}/, pop: true, token: "comment" },
 | 
					      { regex: /\}\}/, pop: true, token: "comment" },
 | 
				
			||||||
      { regex: /./, token: "comment" }
 | 
					      { regex: /./, token: "comment" }
 | 
				
			||||||
    ]
 | 
					    ],
 | 
				
			||||||
 | 
					    meta: {
 | 
				
			||||||
 | 
					      blockCommentStart: "{{--",
 | 
				
			||||||
 | 
					      blockCommentEnd: "--}}"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  CodeMirror.defineMode("handlebars", function(config, parserConfig) {
 | 
					  CodeMirror.defineMode("handlebars", function(config, parserConfig) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -197,13 +197,14 @@ CodeMirror.defineMode("haskell", function(_config, modeConfig) {
 | 
				
			|||||||
      "\.\.", ":", "::", "=", "\\", "<-", "->", "@", "~", "=>");
 | 
					      "\.\.", ":", "::", "=", "\\", "<-", "->", "@", "~", "=>");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setType("builtin")(
 | 
					    setType("builtin")(
 | 
				
			||||||
      "!!", "$!", "$", "&&", "+", "++", "-", ".", "/", "/=", "<", "<=", "=<<",
 | 
					      "!!", "$!", "$", "&&", "+", "++", "-", ".", "/", "/=", "<", "<*", "<=",
 | 
				
			||||||
      "==", ">", ">=", ">>", ">>=", "^", "^^", "||", "*", "**");
 | 
					      "<$>", "<*>", "=<<", "==", ">", ">=", ">>", ">>=", "^", "^^", "||", "*",
 | 
				
			||||||
 | 
					      "*>", "**");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setType("builtin")(
 | 
					    setType("builtin")(
 | 
				
			||||||
      "Bool", "Bounded", "Char", "Double", "EQ", "Either", "Enum", "Eq",
 | 
					      "Applicative", "Bool", "Bounded", "Char", "Double", "EQ", "Either", "Enum",
 | 
				
			||||||
      "False", "FilePath", "Float", "Floating", "Fractional", "Functor", "GT",
 | 
					      "Eq", "False", "FilePath", "Float", "Floating", "Fractional", "Functor",
 | 
				
			||||||
      "IO", "IOError", "Int", "Integer", "Integral", "Just", "LT", "Left",
 | 
					      "GT", "IO", "IOError", "Int", "Integer", "Integral", "Just", "LT", "Left",
 | 
				
			||||||
      "Maybe", "Monad", "Nothing", "Num", "Ord", "Ordering", "Rational", "Read",
 | 
					      "Maybe", "Monad", "Nothing", "Num", "Ord", "Ordering", "Rational", "Read",
 | 
				
			||||||
      "ReadS", "Real", "RealFloat", "RealFrac", "Right", "Show", "ShowS",
 | 
					      "ReadS", "Real", "RealFloat", "RealFrac", "Right", "Show", "ShowS",
 | 
				
			||||||
      "String", "True");
 | 
					      "String", "True");
 | 
				
			||||||
@@ -223,7 +224,7 @@ CodeMirror.defineMode("haskell", function(_config, modeConfig) {
 | 
				
			|||||||
      "lcm", "length", "lex", "lines", "log", "logBase", "lookup", "map",
 | 
					      "lcm", "length", "lex", "lines", "log", "logBase", "lookup", "map",
 | 
				
			||||||
      "mapM", "mapM_", "max", "maxBound", "maximum", "maybe", "min", "minBound",
 | 
					      "mapM", "mapM_", "max", "maxBound", "maximum", "maybe", "min", "minBound",
 | 
				
			||||||
      "minimum", "mod", "negate", "not", "notElem", "null", "odd", "or",
 | 
					      "minimum", "mod", "negate", "not", "notElem", "null", "odd", "or",
 | 
				
			||||||
      "otherwise", "pi", "pred", "print", "product", "properFraction",
 | 
					      "otherwise", "pi", "pred", "print", "product", "properFraction", "pure",
 | 
				
			||||||
      "putChar", "putStr", "putStrLn", "quot", "quotRem", "read", "readFile",
 | 
					      "putChar", "putStr", "putStrLn", "quot", "quotRem", "read", "readFile",
 | 
				
			||||||
      "readIO", "readList", "readLn", "readParen", "reads", "readsPrec",
 | 
					      "readIO", "readList", "readLn", "readParen", "reads", "readsPrec",
 | 
				
			||||||
      "realToFrac", "recip", "rem", "repeat", "replicate", "return", "reverse",
 | 
					      "realToFrac", "recip", "rem", "repeat", "replicate", "return", "reverse",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -80,7 +80,7 @@ option.</p>
 | 
				
			|||||||
      <li><a href="javascript/index.html">JavaScript</a> (<a href="jsx/index.html">JSX</a>)</li>
 | 
					      <li><a href="javascript/index.html">JavaScript</a> (<a href="jsx/index.html">JSX</a>)</li>
 | 
				
			||||||
      <li><a href="jinja2/index.html">Jinja2</a></li>
 | 
					      <li><a href="jinja2/index.html">Jinja2</a></li>
 | 
				
			||||||
      <li><a href="julia/index.html">Julia</a></li>
 | 
					      <li><a href="julia/index.html">Julia</a></li>
 | 
				
			||||||
      <li><a href="kotlin/index.html">Kotlin</a></li>
 | 
					      <li><a href="clike/index.html">Kotlin</a></li>
 | 
				
			||||||
      <li><a href="css/less.html">LESS</a></li>
 | 
					      <li><a href="css/less.html">LESS</a></li>
 | 
				
			||||||
      <li><a href="livescript/index.html">LiveScript</a></li>
 | 
					      <li><a href="livescript/index.html">LiveScript</a></li>
 | 
				
			||||||
      <li><a href="lua/index.html">Lua</a></li>
 | 
					      <li><a href="lua/index.html">Lua</a></li>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -75,17 +75,10 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
      return ret(ch);
 | 
					      return ret(ch);
 | 
				
			||||||
    } else if (ch == "=" && stream.eat(">")) {
 | 
					    } else if (ch == "=" && stream.eat(">")) {
 | 
				
			||||||
      return ret("=>", "operator");
 | 
					      return ret("=>", "operator");
 | 
				
			||||||
    } else if (ch == "0" && stream.eat(/x/i)) {
 | 
					    } else if (ch == "0" && stream.match(/^(?:x[\da-f]+|o[0-7]+|b[01]+)n?/i)) {
 | 
				
			||||||
      stream.eatWhile(/[\da-f]/i);
 | 
					 | 
				
			||||||
      return ret("number", "number");
 | 
					 | 
				
			||||||
    } else if (ch == "0" && stream.eat(/o/i)) {
 | 
					 | 
				
			||||||
      stream.eatWhile(/[0-7]/i);
 | 
					 | 
				
			||||||
      return ret("number", "number");
 | 
					 | 
				
			||||||
    } else if (ch == "0" && stream.eat(/b/i)) {
 | 
					 | 
				
			||||||
      stream.eatWhile(/[01]/i);
 | 
					 | 
				
			||||||
      return ret("number", "number");
 | 
					      return ret("number", "number");
 | 
				
			||||||
    } else if (/\d/.test(ch)) {
 | 
					    } else if (/\d/.test(ch)) {
 | 
				
			||||||
      stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/);
 | 
					      stream.match(/^\d*(?:n|(?:\.\d*)?(?:[eE][+\-]?\d+)?)?/);
 | 
				
			||||||
      return ret("number", "number");
 | 
					      return ret("number", "number");
 | 
				
			||||||
    } else if (ch == "/") {
 | 
					    } else if (ch == "/") {
 | 
				
			||||||
      if (stream.eat("*")) {
 | 
					      if (stream.eat("*")) {
 | 
				
			||||||
@@ -96,7 +89,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
        return ret("comment", "comment");
 | 
					        return ret("comment", "comment");
 | 
				
			||||||
      } else if (expressionAllowed(stream, state, 1)) {
 | 
					      } else if (expressionAllowed(stream, state, 1)) {
 | 
				
			||||||
        readRegexp(stream);
 | 
					        readRegexp(stream);
 | 
				
			||||||
        stream.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/);
 | 
					        stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/);
 | 
				
			||||||
        return ret("regexp", "string-2");
 | 
					        return ret("regexp", "string-2");
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        stream.eat("=");
 | 
					        stream.eat("=");
 | 
				
			||||||
@@ -126,7 +119,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
          var kw = keywords[word]
 | 
					          var kw = keywords[word]
 | 
				
			||||||
          return ret(kw.type, kw.style, word)
 | 
					          return ret(kw.type, kw.style, word)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\(\w]/, false))
 | 
					        if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\[\(\w]/, false))
 | 
				
			||||||
          return ret("async", "keyword", word)
 | 
					          return ret("async", "keyword", word)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return ret("variable", "variable", word)
 | 
					      return ret("variable", "variable", word)
 | 
				
			||||||
@@ -265,21 +258,42 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
    pass.apply(null, arguments);
 | 
					    pass.apply(null, arguments);
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  function register(varname) {
 | 
					  function inList(name, list) {
 | 
				
			||||||
    function inList(list) {
 | 
					    for (var v = list; v; v = v.next) if (v.name == name) return true
 | 
				
			||||||
      for (var v = list; v; v = v.next)
 | 
					 | 
				
			||||||
        if (v.name == varname) return true;
 | 
					 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  function register(varname) {
 | 
				
			||||||
    var state = cx.state;
 | 
					    var state = cx.state;
 | 
				
			||||||
    cx.marked = "def";
 | 
					    cx.marked = "def";
 | 
				
			||||||
    if (state.context) {
 | 
					    if (state.context) {
 | 
				
			||||||
      if (inList(state.localVars)) return;
 | 
					      if (state.lexical.info == "var" && state.context && state.context.block) {
 | 
				
			||||||
      state.localVars = {name: varname, next: state.localVars};
 | 
					        // FIXME function decls are also not block scoped
 | 
				
			||||||
 | 
					        var newContext = registerVarScoped(varname, state.context)
 | 
				
			||||||
 | 
					        if (newContext != null) {
 | 
				
			||||||
 | 
					          state.context = newContext
 | 
				
			||||||
 | 
					          return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else if (!inList(varname, state.localVars)) {
 | 
				
			||||||
 | 
					        state.localVars = new Var(varname, state.localVars)
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // Fall through means this is global
 | 
				
			||||||
 | 
					    if (parserConfig.globalVars && !inList(varname, state.globalVars))
 | 
				
			||||||
 | 
					      state.globalVars = new Var(varname, state.globalVars)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  function registerVarScoped(varname, context) {
 | 
				
			||||||
 | 
					    if (!context) {
 | 
				
			||||||
 | 
					      return null
 | 
				
			||||||
 | 
					    } else if (context.block) {
 | 
				
			||||||
 | 
					      var inner = registerVarScoped(varname, context.prev)
 | 
				
			||||||
 | 
					      if (!inner) return null
 | 
				
			||||||
 | 
					      if (inner == context.prev) return context
 | 
				
			||||||
 | 
					      return new Context(inner, context.vars, true)
 | 
				
			||||||
 | 
					    } else if (inList(varname, context.vars)) {
 | 
				
			||||||
 | 
					      return context
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      if (inList(state.globalVars)) return;
 | 
					      return new Context(context.prev, new Var(varname, context.vars), false)
 | 
				
			||||||
      if (parserConfig.globalVars)
 | 
					 | 
				
			||||||
        state.globalVars = {name: varname, next: state.globalVars};
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -289,15 +303,23 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Combinators
 | 
					  // Combinators
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var defaultVars = {name: "this", next: {name: "arguments"}};
 | 
					  function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block }
 | 
				
			||||||
 | 
					  function Var(name, next) { this.name = name; this.next = next }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var defaultVars = new Var("this", new Var("arguments", null))
 | 
				
			||||||
  function pushcontext() {
 | 
					  function pushcontext() {
 | 
				
			||||||
    cx.state.context = {prev: cx.state.context, vars: cx.state.localVars};
 | 
					    cx.state.context = new Context(cx.state.context, cx.state.localVars, false)
 | 
				
			||||||
    cx.state.localVars = defaultVars;
 | 
					    cx.state.localVars = defaultVars
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  function pushblockcontext() {
 | 
				
			||||||
 | 
					    cx.state.context = new Context(cx.state.context, cx.state.localVars, true)
 | 
				
			||||||
 | 
					    cx.state.localVars = null
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  function popcontext() {
 | 
					  function popcontext() {
 | 
				
			||||||
    cx.state.localVars = cx.state.context.vars;
 | 
					    cx.state.localVars = cx.state.context.vars
 | 
				
			||||||
    cx.state.context = cx.state.context.prev;
 | 
					    cx.state.context = cx.state.context.prev
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  popcontext.lex = true
 | 
				
			||||||
  function pushlex(type, info) {
 | 
					  function pushlex(type, info) {
 | 
				
			||||||
    var result = function() {
 | 
					    var result = function() {
 | 
				
			||||||
      var state = cx.state, indent = state.indented;
 | 
					      var state = cx.state, indent = state.indented;
 | 
				
			||||||
@@ -322,19 +344,19 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
  function expect(wanted) {
 | 
					  function expect(wanted) {
 | 
				
			||||||
    function exp(type) {
 | 
					    function exp(type) {
 | 
				
			||||||
      if (type == wanted) return cont();
 | 
					      if (type == wanted) return cont();
 | 
				
			||||||
      else if (wanted == ";") return pass();
 | 
					      else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass();
 | 
				
			||||||
      else return cont(exp);
 | 
					      else return cont(exp);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    return exp;
 | 
					    return exp;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function statement(type, value) {
 | 
					  function statement(type, value) {
 | 
				
			||||||
    if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex);
 | 
					    if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex);
 | 
				
			||||||
    if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex);
 | 
					    if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex);
 | 
				
			||||||
    if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
 | 
					    if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
 | 
				
			||||||
    if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex);
 | 
					    if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex);
 | 
				
			||||||
    if (type == "debugger") return cont(expect(";"));
 | 
					    if (type == "debugger") return cont(expect(";"));
 | 
				
			||||||
    if (type == "{") return cont(pushlex("}"), block, poplex);
 | 
					    if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext);
 | 
				
			||||||
    if (type == ";") return cont();
 | 
					    if (type == ";") return cont();
 | 
				
			||||||
    if (type == "if") {
 | 
					    if (type == "if") {
 | 
				
			||||||
      if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
 | 
					      if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
 | 
				
			||||||
@@ -345,34 +367,38 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
    if (type == "for") return cont(pushlex("form"), forspec, statement, poplex);
 | 
					    if (type == "for") return cont(pushlex("form"), forspec, statement, poplex);
 | 
				
			||||||
    if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), className, poplex); }
 | 
					    if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), className, poplex); }
 | 
				
			||||||
    if (type == "variable") {
 | 
					    if (type == "variable") {
 | 
				
			||||||
      if (isTS && value == "type") {
 | 
					      if (isTS && value == "declare") {
 | 
				
			||||||
        cx.marked = "keyword"
 | 
					 | 
				
			||||||
        return cont(typeexpr, expect("operator"), typeexpr, expect(";"));
 | 
					 | 
				
			||||||
      } else if (isTS && value == "declare") {
 | 
					 | 
				
			||||||
        cx.marked = "keyword"
 | 
					        cx.marked = "keyword"
 | 
				
			||||||
        return cont(statement)
 | 
					        return cont(statement)
 | 
				
			||||||
      } else if (isTS && (value == "module" || value == "enum") && cx.stream.match(/^\s*\w/, false)) {
 | 
					      } else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) {
 | 
				
			||||||
        cx.marked = "keyword"
 | 
					        cx.marked = "keyword"
 | 
				
			||||||
        return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex)
 | 
					        if (value == "enum") return cont(enumdef);
 | 
				
			||||||
 | 
					        else if (value == "type") return cont(typeexpr, expect("operator"), typeexpr, expect(";"));
 | 
				
			||||||
 | 
					        else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex)
 | 
				
			||||||
      } else if (isTS && value == "namespace") {
 | 
					      } else if (isTS && value == "namespace") {
 | 
				
			||||||
        cx.marked = "keyword"
 | 
					        cx.marked = "keyword"
 | 
				
			||||||
        return cont(pushlex("form"), expression, block, poplex)
 | 
					        return cont(pushlex("form"), expression, block, poplex)
 | 
				
			||||||
 | 
					      } else if (isTS && value == "abstract") {
 | 
				
			||||||
 | 
					        cx.marked = "keyword"
 | 
				
			||||||
 | 
					        return cont(statement)
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        return cont(pushlex("stat"), maybelabel);
 | 
					        return cont(pushlex("stat"), maybelabel);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"),
 | 
					    if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext,
 | 
				
			||||||
                                      block, poplex, poplex);
 | 
					                                      block, poplex, poplex, popcontext);
 | 
				
			||||||
    if (type == "case") return cont(expression, expect(":"));
 | 
					    if (type == "case") return cont(expression, expect(":"));
 | 
				
			||||||
    if (type == "default") return cont(expect(":"));
 | 
					    if (type == "default") return cont(expect(":"));
 | 
				
			||||||
    if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"),
 | 
					    if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext);
 | 
				
			||||||
                                     statement, poplex, popcontext);
 | 
					 | 
				
			||||||
    if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
 | 
					    if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
 | 
				
			||||||
    if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
 | 
					    if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
 | 
				
			||||||
    if (type == "async") return cont(statement)
 | 
					    if (type == "async") return cont(statement)
 | 
				
			||||||
    if (value == "@") return cont(expression, statement)
 | 
					    if (value == "@") return cont(expression, statement)
 | 
				
			||||||
    return pass(pushlex("stat"), expression, expect(";"), poplex);
 | 
					    return pass(pushlex("stat"), expression, expect(";"), poplex);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  function maybeCatchBinding(type) {
 | 
				
			||||||
 | 
					    if (type == "(") return cont(funarg, expect(")"))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  function expression(type, value) {
 | 
					  function expression(type, value) {
 | 
				
			||||||
    return expressionInner(type, value, false);
 | 
					    return expressionInner(type, value, false);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -401,6 +427,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
    if (type == "{") return contCommasep(objprop, "}", null, maybeop);
 | 
					    if (type == "{") return contCommasep(objprop, "}", null, maybeop);
 | 
				
			||||||
    if (type == "quasi") return pass(quasi, maybeop);
 | 
					    if (type == "quasi") return pass(quasi, maybeop);
 | 
				
			||||||
    if (type == "new") return cont(maybeTarget(noComma));
 | 
					    if (type == "new") return cont(maybeTarget(noComma));
 | 
				
			||||||
 | 
					    if (type == "import") return cont(expression);
 | 
				
			||||||
    return cont();
 | 
					    return cont();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  function maybeexpression(type) {
 | 
					  function maybeexpression(type) {
 | 
				
			||||||
@@ -560,19 +587,19 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  function typeexpr(type, value) {
 | 
					  function typeexpr(type, value) {
 | 
				
			||||||
    if (type == "variable" || value == "void") {
 | 
					    if (value == "keyof" || value == "typeof") {
 | 
				
			||||||
      if (value == "keyof") {
 | 
					 | 
				
			||||||
      cx.marked = "keyword"
 | 
					      cx.marked = "keyword"
 | 
				
			||||||
        return cont(typeexpr)
 | 
					      return cont(value == "keyof" ? typeexpr : expressionNoComma)
 | 
				
			||||||
      } else {
 | 
					    }
 | 
				
			||||||
 | 
					    if (type == "variable" || value == "void") {
 | 
				
			||||||
      cx.marked = "type"
 | 
					      cx.marked = "type"
 | 
				
			||||||
      return cont(afterType)
 | 
					      return cont(afterType)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (type == "string" || type == "number" || type == "atom") return cont(afterType);
 | 
					    if (type == "string" || type == "number" || type == "atom") return cont(afterType);
 | 
				
			||||||
    if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType)
 | 
					    if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType)
 | 
				
			||||||
    if (type == "{") return cont(pushlex("}"), commasep(typeprop, "}", ",;"), poplex, afterType)
 | 
					    if (type == "{") return cont(pushlex("}"), commasep(typeprop, "}", ",;"), poplex, afterType)
 | 
				
			||||||
    if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType)
 | 
					    if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType)
 | 
				
			||||||
 | 
					    if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  function maybeReturnType(type) {
 | 
					  function maybeReturnType(type) {
 | 
				
			||||||
    if (type == "=>") return cont(typeexpr)
 | 
					    if (type == "=>") return cont(typeexpr)
 | 
				
			||||||
@@ -589,13 +616,14 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
      return cont(expression, maybetype, expect("]"), typeprop)
 | 
					      return cont(expression, maybetype, expect("]"), typeprop)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  function typearg(type) {
 | 
					  function typearg(type, value) {
 | 
				
			||||||
    if (type == "variable") return cont(typearg)
 | 
					    if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg)
 | 
				
			||||||
    else if (type == ":") return cont(typeexpr)
 | 
					    if (type == ":") return cont(typeexpr)
 | 
				
			||||||
 | 
					    return pass(typeexpr)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  function afterType(type, value) {
 | 
					  function afterType(type, value) {
 | 
				
			||||||
    if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
 | 
					    if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
 | 
				
			||||||
    if (value == "|" || type == ".") return cont(typeexpr)
 | 
					    if (value == "|" || type == "." || value == "&") return cont(typeexpr)
 | 
				
			||||||
    if (type == "[") return cont(expect("]"), afterType)
 | 
					    if (type == "[") return cont(expect("]"), afterType)
 | 
				
			||||||
    if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) }
 | 
					    if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -608,7 +636,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
  function maybeTypeDefault(_, value) {
 | 
					  function maybeTypeDefault(_, value) {
 | 
				
			||||||
    if (value == "=") return cont(typeexpr)
 | 
					    if (value == "=") return cont(typeexpr)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  function vardef() {
 | 
					  function vardef(_, value) {
 | 
				
			||||||
 | 
					    if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)}
 | 
				
			||||||
    return pass(pattern, maybetype, maybeAssign, vardefCont);
 | 
					    return pass(pattern, maybetype, maybeAssign, vardefCont);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  function pattern(type, value) {
 | 
					  function pattern(type, value) {
 | 
				
			||||||
@@ -637,7 +666,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
  function maybeelse(type, value) {
 | 
					  function maybeelse(type, value) {
 | 
				
			||||||
    if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex);
 | 
					    if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  function forspec(type) {
 | 
					  function forspec(type, value) {
 | 
				
			||||||
 | 
					    if (value == "await") return cont(forspec);
 | 
				
			||||||
    if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex);
 | 
					    if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  function forspec1(type) {
 | 
					  function forspec1(type) {
 | 
				
			||||||
@@ -680,8 +710,10 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
  function classNameAfter(type, value) {
 | 
					  function classNameAfter(type, value) {
 | 
				
			||||||
    if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter)
 | 
					    if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter)
 | 
				
			||||||
    if (value == "extends" || value == "implements" || (isTS && type == ","))
 | 
					    if (value == "extends" || value == "implements" || (isTS && type == ",")) {
 | 
				
			||||||
 | 
					      if (value == "implements") cx.marked = "keyword";
 | 
				
			||||||
      return cont(isTS ? typeexpr : expression, classNameAfter);
 | 
					      return cont(isTS ? typeexpr : expression, classNameAfter);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    if (type == "{") return cont(pushlex("}"), classBody, poplex);
 | 
					    if (type == "{") return cont(pushlex("}"), classBody, poplex);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  function classBody(type, value) {
 | 
					  function classBody(type, value) {
 | 
				
			||||||
@@ -724,6 +756,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
  function afterImport(type) {
 | 
					  function afterImport(type) {
 | 
				
			||||||
    if (type == "string") return cont();
 | 
					    if (type == "string") return cont();
 | 
				
			||||||
 | 
					    if (type == "(") return pass(expression);
 | 
				
			||||||
    return pass(importSpec, maybeMoreImports, maybeFrom);
 | 
					    return pass(importSpec, maybeMoreImports, maybeFrom);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  function importSpec(type, value) {
 | 
					  function importSpec(type, value) {
 | 
				
			||||||
@@ -745,6 +778,12 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
    if (type == "]") return cont();
 | 
					    if (type == "]") return cont();
 | 
				
			||||||
    return pass(commasep(expressionNoComma, "]"));
 | 
					    return pass(commasep(expressionNoComma, "]"));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  function enumdef() {
 | 
				
			||||||
 | 
					    return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  function enummember() {
 | 
				
			||||||
 | 
					    return pass(pattern, maybeAssign);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function isContinuedStatement(state, textAfter) {
 | 
					  function isContinuedStatement(state, textAfter) {
 | 
				
			||||||
    return state.lastType == "operator" || state.lastType == "," ||
 | 
					    return state.lastType == "operator" || state.lastType == "," ||
 | 
				
			||||||
@@ -768,7 +807,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
        cc: [],
 | 
					        cc: [],
 | 
				
			||||||
        lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
 | 
					        lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
 | 
				
			||||||
        localVars: parserConfig.localVars,
 | 
					        localVars: parserConfig.localVars,
 | 
				
			||||||
        context: parserConfig.localVars && {vars: parserConfig.localVars},
 | 
					        context: parserConfig.localVars && new Context(null, null, false),
 | 
				
			||||||
        indented: basecolumn || 0
 | 
					        indented: basecolumn || 0
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
 | 
					      if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
 | 
				
			||||||
@@ -809,7 +848,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 | 
				
			|||||||
        lexical = lexical.prev;
 | 
					        lexical = lexical.prev;
 | 
				
			||||||
      var type = lexical.type, closing = firstChar == type;
 | 
					      var type = lexical.type, closing = firstChar == type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0);
 | 
					      if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0);
 | 
				
			||||||
      else if (type == "form" && firstChar == "{") return lexical.indented;
 | 
					      else if (type == "form" && firstChar == "{") return lexical.indented;
 | 
				
			||||||
      else if (type == "form") return lexical.indented + indentUnit;
 | 
					      else if (type == "form") return lexical.indented + indentUnit;
 | 
				
			||||||
      else if (type == "stat")
 | 
					      else if (type == "stat")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -63,6 +63,12 @@
 | 
				
			|||||||
  MT("import_trailing_comma",
 | 
					  MT("import_trailing_comma",
 | 
				
			||||||
     "[keyword import] {[def foo], [def bar],} [keyword from] [string 'baz']")
 | 
					     "[keyword import] {[def foo], [def bar],} [keyword from] [string 'baz']")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("import_dynamic",
 | 
				
			||||||
 | 
					     "[keyword import]([string 'baz']).[property then]")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("import_dynamic",
 | 
				
			||||||
 | 
					     "[keyword const] [def t] [operator =] [keyword import]([string 'baz']).[property then]")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  MT("const",
 | 
					  MT("const",
 | 
				
			||||||
     "[keyword function] [def f]() {",
 | 
					     "[keyword function] [def f]() {",
 | 
				
			||||||
     "  [keyword const] [[ [def a], [def b] ]] [operator =] [[ [number 1], [number 2] ]];",
 | 
					     "  [keyword const] [[ [def a], [def b] ]] [operator =] [[ [number 1], [number 2] ]];",
 | 
				
			||||||
@@ -71,12 +77,44 @@
 | 
				
			|||||||
  MT("for/of",
 | 
					  MT("for/of",
 | 
				
			||||||
     "[keyword for]([keyword let] [def of] [keyword of] [variable something]) {}");
 | 
					     "[keyword for]([keyword let] [def of] [keyword of] [variable something]) {}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("for await",
 | 
				
			||||||
 | 
					     "[keyword for] [keyword await]([keyword let] [def of] [keyword of] [variable something]) {}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  MT("generator",
 | 
					  MT("generator",
 | 
				
			||||||
     "[keyword function*] [def repeat]([def n]) {",
 | 
					     "[keyword function*] [def repeat]([def n]) {",
 | 
				
			||||||
     "  [keyword for]([keyword var] [def i] [operator =] [number 0]; [variable-2 i] [operator <] [variable-2 n]; [operator ++][variable-2 i])",
 | 
					     "  [keyword for]([keyword var] [def i] [operator =] [number 0]; [variable-2 i] [operator <] [variable-2 n]; [operator ++][variable-2 i])",
 | 
				
			||||||
     "    [keyword yield] [variable-2 i];",
 | 
					     "    [keyword yield] [variable-2 i];",
 | 
				
			||||||
     "}");
 | 
					     "}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("let_scoping",
 | 
				
			||||||
 | 
					     "[keyword function] [def scoped]([def n]) {",
 | 
				
			||||||
 | 
					     "  { [keyword var] [def i]; } [variable-2 i];",
 | 
				
			||||||
 | 
					     "  { [keyword let] [def j]; [variable-2 j]; } [variable j];",
 | 
				
			||||||
 | 
					     "  [keyword if] ([atom true]) { [keyword const] [def k]; [variable-2 k]; } [variable k];",
 | 
				
			||||||
 | 
					     "}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("switch_scoping",
 | 
				
			||||||
 | 
					     "[keyword switch] ([variable x]) {",
 | 
				
			||||||
 | 
					     "  [keyword default]:",
 | 
				
			||||||
 | 
					     "    [keyword let] [def j];",
 | 
				
			||||||
 | 
					     "    [keyword return] [variable-2 j]",
 | 
				
			||||||
 | 
					     "}",
 | 
				
			||||||
 | 
					     "[variable j];")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("leaving_scope",
 | 
				
			||||||
 | 
					     "[keyword function] [def a]() {",
 | 
				
			||||||
 | 
					     "  {",
 | 
				
			||||||
 | 
					     "    [keyword const] [def x] [operator =] [number 1]",
 | 
				
			||||||
 | 
					     "    [keyword if] ([atom true]) {",
 | 
				
			||||||
 | 
					     "      [keyword let] [def y] [operator =] [number 2]",
 | 
				
			||||||
 | 
					     "      [keyword var] [def z] [operator =] [number 3]",
 | 
				
			||||||
 | 
					     "      [variable console].[property log]([variable-2 x], [variable-2 y], [variable-2 z])",
 | 
				
			||||||
 | 
					     "    }",
 | 
				
			||||||
 | 
					     "    [variable console].[property log]([variable-2 x], [variable y], [variable-2 z])",
 | 
				
			||||||
 | 
					     "  }",
 | 
				
			||||||
 | 
					     "  [variable console].[property log]([variable x], [variable y], [variable-2 z])",
 | 
				
			||||||
 | 
					     "}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  MT("quotedStringAddition",
 | 
					  MT("quotedStringAddition",
 | 
				
			||||||
     "[keyword let] [def f] [operator =] [variable a] [operator +] [string 'fatarrow'] [operator +] [variable c];");
 | 
					     "[keyword let] [def f] [operator =] [variable a] [operator +] [string 'fatarrow'] [operator +] [variable c];");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -230,6 +268,8 @@
 | 
				
			|||||||
     "[keyword const] [def async] [operator =] {[property a]: [number 1]};",
 | 
					     "[keyword const] [def async] [operator =] {[property a]: [number 1]};",
 | 
				
			||||||
     "[keyword const] [def foo] [operator =] [string-2 `bar ${][variable async].[property a][string-2 }`];")
 | 
					     "[keyword const] [def foo] [operator =] [string-2 `bar ${][variable async].[property a][string-2 }`];")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("bigint", "[number 1n] [operator +] [number 0x1afn] [operator +] [number 0o064n] [operator +] [number 0b100n];")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  MT("async_comment",
 | 
					  MT("async_comment",
 | 
				
			||||||
     "[keyword async] [comment /**/] [keyword function] [def foo]([def args]) { [keyword return] [atom true]; }");
 | 
					     "[keyword async] [comment /**/] [keyword function] [def foo]([def args]) { [keyword return] [atom true]; }");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -383,6 +423,25 @@
 | 
				
			|||||||
     "  }",
 | 
					     "  }",
 | 
				
			||||||
     "}")
 | 
					     "}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  TS("type as variable",
 | 
				
			||||||
 | 
					     "[variable type] [operator =] [variable x] [keyword as] [type Bar];");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  TS("enum body",
 | 
				
			||||||
 | 
					     "[keyword export] [keyword const] [keyword enum] [def CodeInspectionResultType] {",
 | 
				
			||||||
 | 
					     "  [def ERROR] [operator =] [string 'problem_type_error'],",
 | 
				
			||||||
 | 
					     "  [def WARNING] [operator =] [string 'problem_type_warning'],",
 | 
				
			||||||
 | 
					     "  [def META],",
 | 
				
			||||||
 | 
					     "}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  TS("parenthesized type",
 | 
				
			||||||
 | 
					     "[keyword class] [def Foo] {",
 | 
				
			||||||
 | 
					     "  [property x] [operator =] [keyword new] [variable A][operator <][type B], [type string][operator |](() [operator =>] [type void])[operator >]();",
 | 
				
			||||||
 | 
					     "  [keyword private] [property bar]();",
 | 
				
			||||||
 | 
					     "}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  TS("abstract class",
 | 
				
			||||||
 | 
					     "[keyword export] [keyword abstract] [keyword class] [def Foo] {}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var jsonld_mode = CodeMirror.getMode(
 | 
					  var jsonld_mode = CodeMirror.getMode(
 | 
				
			||||||
    {indentUnit: 2},
 | 
					    {indentUnit: 2},
 | 
				
			||||||
    {name: "javascript", jsonld: true}
 | 
					    {name: "javascript", jsonld: true}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -54,11 +54,13 @@ CodeMirror.defineMode("julia", function(config, parserConf) {
 | 
				
			|||||||
    return inGenerator(state, '[')
 | 
					    return inGenerator(state, '[')
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function inGenerator(state, bracket) {
 | 
					  function inGenerator(state, bracket, depth) {
 | 
				
			||||||
    var curr = currentScope(state),
 | 
					 | 
				
			||||||
        prev = currentScope(state, 1);
 | 
					 | 
				
			||||||
    if (typeof(bracket) === "undefined") { bracket = '('; }
 | 
					    if (typeof(bracket) === "undefined") { bracket = '('; }
 | 
				
			||||||
    if (curr === bracket || (prev === bracket && curr === "for")) {
 | 
					    if (typeof(depth)   === "undefined") { depth   = 0;   }
 | 
				
			||||||
 | 
					    var scope = currentScope(state, depth);
 | 
				
			||||||
 | 
					    if ((depth == 0 && scope === "if" && inGenerator(state, bracket, depth + 1)) ||
 | 
				
			||||||
 | 
					        (scope === "for" && inGenerator(state, bracket, depth + 1)) ||
 | 
				
			||||||
 | 
					        (scope === bracket)) {
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
@@ -119,16 +121,16 @@ CodeMirror.defineMode("julia", function(config, parserConf) {
 | 
				
			|||||||
      state.scopes.push('(');
 | 
					      state.scopes.push('(');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var scope = currentScope(state);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (inArray(state) && ch === ']') {
 | 
					    if (inArray(state) && ch === ']') {
 | 
				
			||||||
      if (scope === "for") { state.scopes.pop(); }
 | 
					      if (currentScope(state) === "if") { state.scopes.pop(); }
 | 
				
			||||||
 | 
					      while (currentScope(state) === "for") { state.scopes.pop(); }
 | 
				
			||||||
      state.scopes.pop();
 | 
					      state.scopes.pop();
 | 
				
			||||||
      state.leavingExpr = true;
 | 
					      state.leavingExpr = true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (inGenerator(state) && ch === ')') {
 | 
					    if (inGenerator(state) && ch === ')') {
 | 
				
			||||||
      if (scope === "for") { state.scopes.pop(); }
 | 
					      if (currentScope(state) === "if") { state.scopes.pop(); }
 | 
				
			||||||
 | 
					      while (currentScope(state) === "for") { state.scopes.pop(); }
 | 
				
			||||||
      state.scopes.pop();
 | 
					      state.scopes.pop();
 | 
				
			||||||
      state.leavingExpr = true;
 | 
					      state.leavingExpr = true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -143,12 +145,14 @@ CodeMirror.defineMode("julia", function(config, parserConf) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var match;
 | 
					    var match;
 | 
				
			||||||
    if (match = stream.match(openers, false)) {
 | 
					    if (match = stream.match(openers)) {
 | 
				
			||||||
      state.scopes.push(match[0]);
 | 
					      state.scopes.push(match[0]);
 | 
				
			||||||
 | 
					      return "keyword";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (stream.match(closers, false)) {
 | 
					    if (stream.match(closers)) {
 | 
				
			||||||
      state.scopes.pop();
 | 
					      state.scopes.pop();
 | 
				
			||||||
 | 
					      return "keyword";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Handle type annotations
 | 
					    // Handle type annotations
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -90,7 +90,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
 | 
				
			|||||||
  ,   setextHeaderRE = /^ *(?:\={1,}|-{1,})\s*$/
 | 
					  ,   setextHeaderRE = /^ *(?:\={1,}|-{1,})\s*$/
 | 
				
			||||||
  ,   textRE = /^[^#!\[\]*_\\<>` "'(~:]+/
 | 
					  ,   textRE = /^[^#!\[\]*_\\<>` "'(~:]+/
 | 
				
			||||||
  ,   fencedCodeRE = /^(~~~+|```+)[ \t]*([\w+#-]*)[^\n`]*$/
 | 
					  ,   fencedCodeRE = /^(~~~+|```+)[ \t]*([\w+#-]*)[^\n`]*$/
 | 
				
			||||||
  ,   linkDefRE = /^\s*\[[^\]]+?\]:\s*\S+(\s*\S*\s*)?$/ // naive link-definition
 | 
					  ,   linkDefRE = /^\s*\[[^\]]+?\]:.*$/ // naive link-definition
 | 
				
			||||||
  ,   punctuation = /[!\"#$%&\'()*+,\-\.\/:;<=>?@\[\\\]^_`{|}~—]/
 | 
					  ,   punctuation = /[!\"#$%&\'()*+,\-\.\/:;<=>?@\[\\\]^_`{|}~—]/
 | 
				
			||||||
  ,   expandedTab = "    " // CommonMark specifies tab as 4 spaces
 | 
					  ,   expandedTab = "    " // CommonMark specifies tab as 4 spaces
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -113,6 +113,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
 | 
				
			|||||||
  function blankLine(state) {
 | 
					  function blankLine(state) {
 | 
				
			||||||
    // Reset linkTitle state
 | 
					    // Reset linkTitle state
 | 
				
			||||||
    state.linkTitle = false;
 | 
					    state.linkTitle = false;
 | 
				
			||||||
 | 
					    state.linkHref = false;
 | 
				
			||||||
 | 
					    state.linkText = false;
 | 
				
			||||||
    // Reset EM state
 | 
					    // Reset EM state
 | 
				
			||||||
    state.em = false;
 | 
					    state.em = false;
 | 
				
			||||||
    // Reset STRONG state
 | 
					    // Reset STRONG state
 | 
				
			||||||
@@ -124,8 +126,17 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
 | 
				
			|||||||
    // Reset state.indentedCode
 | 
					    // Reset state.indentedCode
 | 
				
			||||||
    state.indentedCode = false;
 | 
					    state.indentedCode = false;
 | 
				
			||||||
    if (state.f == htmlBlock) {
 | 
					    if (state.f == htmlBlock) {
 | 
				
			||||||
 | 
					      var exit = htmlModeMissing
 | 
				
			||||||
 | 
					      if (!exit) {
 | 
				
			||||||
 | 
					        var inner = CodeMirror.innerMode(htmlMode, state.htmlState)
 | 
				
			||||||
 | 
					        exit = inner.mode.name == "xml" && inner.state.tagStart === null &&
 | 
				
			||||||
 | 
					          (!inner.state.context && inner.state.tokenize.isInText)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (exit) {
 | 
				
			||||||
        state.f = inlineNormal;
 | 
					        state.f = inlineNormal;
 | 
				
			||||||
        state.block = blockNormal;
 | 
					        state.block = blockNormal;
 | 
				
			||||||
 | 
					        state.htmlState = null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // Reset state.trailingSpace
 | 
					    // Reset state.trailingSpace
 | 
				
			||||||
    state.trailingSpace = 0;
 | 
					    state.trailingSpace = 0;
 | 
				
			||||||
@@ -151,6 +162,12 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
 | 
				
			|||||||
    if (state.indentationDiff === null) {
 | 
					    if (state.indentationDiff === null) {
 | 
				
			||||||
      state.indentationDiff = state.indentation;
 | 
					      state.indentationDiff = state.indentation;
 | 
				
			||||||
      if (prevLineIsList) {
 | 
					      if (prevLineIsList) {
 | 
				
			||||||
 | 
					        // Reset inline styles which shouldn't propagate aross list items
 | 
				
			||||||
 | 
					        state.em = false;
 | 
				
			||||||
 | 
					        state.strong = false;
 | 
				
			||||||
 | 
					        state.code = false;
 | 
				
			||||||
 | 
					        state.strikethrough = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        state.list = null;
 | 
					        state.list = null;
 | 
				
			||||||
        // While this list item's marker's indentation is less than the deepest
 | 
					        // While this list item's marker's indentation is less than the deepest
 | 
				
			||||||
        //  list item's content's indentation,pop the deepest list item
 | 
					        //  list item's content's indentation,pop the deepest list item
 | 
				
			||||||
@@ -489,6 +506,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (ch === '[' && !state.image) {
 | 
					    if (ch === '[' && !state.image) {
 | 
				
			||||||
 | 
					      if (state.linkText && stream.match(/^.*?\]/)) return getType(state)
 | 
				
			||||||
      state.linkText = true;
 | 
					      state.linkText = true;
 | 
				
			||||||
      if (modeCfg.highlightFormatting) state.formatting = "link";
 | 
					      if (modeCfg.highlightFormatting) state.formatting = "link";
 | 
				
			||||||
      return getType(state);
 | 
					      return getType(state);
 | 
				
			||||||
@@ -526,7 +544,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
 | 
				
			|||||||
      return type + tokenTypes.linkEmail;
 | 
					      return type + tokenTypes.linkEmail;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (modeCfg.xml && ch === '<' && stream.match(/^(!--|[a-z]+(?:\s+[a-z_:.\-]+(?:\s*=\s*[^ >]+)?)*\s*>)/i, false)) {
 | 
					    if (modeCfg.xml && ch === '<' && stream.match(/^(!--|\?|!\[CDATA\[|[a-z][a-z0-9-]*(?:\s+[a-z_:.\-]+(?:\s*=\s*[^>]+)?)*\s*(?:>|$))/i, false)) {
 | 
				
			||||||
      var end = stream.string.indexOf(">", stream.pos);
 | 
					      var end = stream.string.indexOf(">", stream.pos);
 | 
				
			||||||
      if (end != -1) {
 | 
					      if (end != -1) {
 | 
				
			||||||
        var atts = stream.string.substring(stream.start, end);
 | 
					        var atts = stream.string.substring(stream.start, end);
 | 
				
			||||||
@@ -611,7 +629,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (ch === ' ') {
 | 
					    if (ch === ' ') {
 | 
				
			||||||
      if (stream.match(/ +$/, false)) {
 | 
					      if (stream.match(/^ +$/, false)) {
 | 
				
			||||||
        state.trailingSpace++;
 | 
					        state.trailingSpace++;
 | 
				
			||||||
      } else if (state.trailingSpace) {
 | 
					      } else if (state.trailingSpace) {
 | 
				
			||||||
        state.trailingSpaceNewLine = true;
 | 
					        state.trailingSpaceNewLine = true;
 | 
				
			||||||
@@ -777,6 +795,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
 | 
				
			|||||||
        formatting: false,
 | 
					        formatting: false,
 | 
				
			||||||
        linkText: s.linkText,
 | 
					        linkText: s.linkText,
 | 
				
			||||||
        linkTitle: s.linkTitle,
 | 
					        linkTitle: s.linkTitle,
 | 
				
			||||||
 | 
					        linkHref: s.linkHref,
 | 
				
			||||||
        code: s.code,
 | 
					        code: s.code,
 | 
				
			||||||
        em: s.em,
 | 
					        em: s.em,
 | 
				
			||||||
        strong: s.strong,
 | 
					        strong: s.strong,
 | 
				
			||||||
@@ -856,6 +875,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
 | 
				
			|||||||
  return mode;
 | 
					  return mode;
 | 
				
			||||||
}, "xml");
 | 
					}, "xml");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CodeMirror.defineMIME("text/markdown", "markdown");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CodeMirror.defineMIME("text/x-markdown", "markdown");
 | 
					CodeMirror.defineMIME("text/x-markdown", "markdown");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1283,6 +1283,25 @@
 | 
				
			|||||||
     "[tag&bracket <][tag div][tag&bracket >]",
 | 
					     "[tag&bracket <][tag div][tag&bracket >]",
 | 
				
			||||||
     "[tag&bracket </][tag div][tag&bracket >]");
 | 
					     "[tag&bracket </][tag div][tag&bracket >]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("xmlModeLineBreakInTags",
 | 
				
			||||||
 | 
					     "[tag&bracket <][tag div] [attribute id]=[string \"1\"]",
 | 
				
			||||||
 | 
					     "     [attribute class]=[string \"sth\"][tag&bracket >]xxx",
 | 
				
			||||||
 | 
					     "[tag&bracket </][tag div][tag&bracket >]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("xmlModeCommentWithBlankLine",
 | 
				
			||||||
 | 
					     "[comment <!-- Hello]",
 | 
				
			||||||
 | 
					     "",
 | 
				
			||||||
 | 
					     "[comment World -->]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("xmlModeCDATA",
 | 
				
			||||||
 | 
					     "[atom <![CDATA[ Hello]",
 | 
				
			||||||
 | 
					     "",
 | 
				
			||||||
 | 
					     "[atom FooBar]",
 | 
				
			||||||
 | 
					     "[atom Test ]]]]>]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("xmlModePreprocessor",
 | 
				
			||||||
 | 
					     "[meta <?php] [meta echo '1234'; ?>]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  MT_noXml("xmlHighlightDisabled",
 | 
					  MT_noXml("xmlHighlightDisabled",
 | 
				
			||||||
     "<div>foo</div>");
 | 
					     "<div>foo</div>");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -71,12 +71,12 @@ CodeMirror.defineMode('mathematica', function(_config, _parserConfig) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // usage
 | 
					    // usage
 | 
				
			||||||
    if (stream.match(/([a-zA-Z\$]+(?:`?[a-zA-Z0-9\$])*::usage)/, true, false)) {
 | 
					    if (stream.match(/([a-zA-Z\$][a-zA-Z0-9\$]*(?:`[a-zA-Z0-9\$]+)*::usage)/, true, false)) {
 | 
				
			||||||
      return 'meta';
 | 
					      return 'meta';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // message
 | 
					    // message
 | 
				
			||||||
    if (stream.match(/([a-zA-Z\$]+(?:`?[a-zA-Z0-9\$])*::[a-zA-Z\$][a-zA-Z0-9\$]*):?/, true, false)) {
 | 
					    if (stream.match(/([a-zA-Z\$][a-zA-Z0-9\$]*(?:`[a-zA-Z0-9\$]+)*::[a-zA-Z\$][a-zA-Z0-9\$]*):?/, true, false)) {
 | 
				
			||||||
      return 'string-2';
 | 
					      return 'string-2';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								src/public/libraries/codemirror/mode/meta.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								src/public/libraries/codemirror/mode/meta.js
									
									
									
									
										vendored
									
									
								
							@@ -17,7 +17,7 @@
 | 
				
			|||||||
    {name: "ASN.1", mime: "text/x-ttcn-asn", mode: "asn.1", ext: ["asn", "asn1"]},
 | 
					    {name: "ASN.1", mime: "text/x-ttcn-asn", mode: "asn.1", ext: ["asn", "asn1"]},
 | 
				
			||||||
    {name: "Asterisk", mime: "text/x-asterisk", mode: "asterisk", file: /^extensions\.conf$/i},
 | 
					    {name: "Asterisk", mime: "text/x-asterisk", mode: "asterisk", file: /^extensions\.conf$/i},
 | 
				
			||||||
    {name: "Brainfuck", mime: "text/x-brainfuck", mode: "brainfuck", ext: ["b", "bf"]},
 | 
					    {name: "Brainfuck", mime: "text/x-brainfuck", mode: "brainfuck", ext: ["b", "bf"]},
 | 
				
			||||||
    {name: "C", mime: "text/x-csrc", mode: "clike", ext: ["c", "h"]},
 | 
					    {name: "C", mime: "text/x-csrc", mode: "clike", ext: ["c", "h", "ino"]},
 | 
				
			||||||
    {name: "C++", mime: "text/x-c++src", mode: "clike", ext: ["cpp", "c++", "cc", "cxx", "hpp", "h++", "hh", "hxx"], alias: ["cpp"]},
 | 
					    {name: "C++", mime: "text/x-c++src", mode: "clike", ext: ["cpp", "c++", "cc", "cxx", "hpp", "h++", "hh", "hxx"], alias: ["cpp"]},
 | 
				
			||||||
    {name: "Cobol", mime: "text/x-cobol", mode: "cobol", ext: ["cob", "cpy"]},
 | 
					    {name: "Cobol", mime: "text/x-cobol", mode: "cobol", ext: ["cob", "cpy"]},
 | 
				
			||||||
    {name: "C#", mime: "text/x-csharp", mode: "clike", ext: ["cs"], alias: ["csharp"]},
 | 
					    {name: "C#", mime: "text/x-csharp", mode: "clike", ext: ["cs"], alias: ["csharp"]},
 | 
				
			||||||
@@ -64,13 +64,13 @@
 | 
				
			|||||||
    {name: "Haxe", mime: "text/x-haxe", mode: "haxe", ext: ["hx"]},
 | 
					    {name: "Haxe", mime: "text/x-haxe", mode: "haxe", ext: ["hx"]},
 | 
				
			||||||
    {name: "HXML", mime: "text/x-hxml", mode: "haxe", ext: ["hxml"]},
 | 
					    {name: "HXML", mime: "text/x-hxml", mode: "haxe", ext: ["hxml"]},
 | 
				
			||||||
    {name: "ASP.NET", mime: "application/x-aspx", mode: "htmlembedded", ext: ["aspx"], alias: ["asp", "aspx"]},
 | 
					    {name: "ASP.NET", mime: "application/x-aspx", mode: "htmlembedded", ext: ["aspx"], alias: ["asp", "aspx"]},
 | 
				
			||||||
    {name: "HTML", mime: "text/html", mode: "htmlmixed", ext: ["html", "htm"], alias: ["xhtml"]},
 | 
					    {name: "HTML", mime: "text/html", mode: "htmlmixed", ext: ["html", "htm", "handlebars", "hbs"], alias: ["xhtml"]},
 | 
				
			||||||
    {name: "HTTP", mime: "message/http", mode: "http"},
 | 
					    {name: "HTTP", mime: "message/http", mode: "http"},
 | 
				
			||||||
    {name: "IDL", mime: "text/x-idl", mode: "idl", ext: ["pro"]},
 | 
					    {name: "IDL", mime: "text/x-idl", mode: "idl", ext: ["pro"]},
 | 
				
			||||||
    {name: "Pug", mime: "text/x-pug", mode: "pug", ext: ["jade", "pug"], alias: ["jade"]},
 | 
					    {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", mime: "text/x-java", mode: "clike", ext: ["java"]},
 | 
				
			||||||
    {name: "Java Server Pages", mime: "application/x-jsp", mode: "htmlembedded", ext: ["jsp"], alias: ["jsp"]},
 | 
					    {name: "Java Server Pages", mime: "application/x-jsp", mode: "htmlembedded", ext: ["jsp"], alias: ["jsp"]},
 | 
				
			||||||
    {name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/javascript;env=frontend", "application/javascript;env=backend", "application/x-javascript", "application/ecmascript"],
 | 
					    {name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/x-javascript", "application/ecmascript", "application/javascript;env=frontend", "application/javascript;env=backend"],
 | 
				
			||||||
     mode: "javascript", ext: ["js"], alias: ["ecmascript", "js", "node"]},
 | 
					     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", 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"]},
 | 
					    {name: "JSON-LD", mime: "application/ld+json", mode: "javascript", ext: ["jsonld"], alias: ["jsonld"]},
 | 
				
			||||||
@@ -101,7 +101,7 @@
 | 
				
			|||||||
    {name: "Pascal", mime: "text/x-pascal", mode: "pascal", ext: ["p", "pas"]},
 | 
					    {name: "Pascal", mime: "text/x-pascal", mode: "pascal", ext: ["p", "pas"]},
 | 
				
			||||||
    {name: "PEG.js", mime: "null", mode: "pegjs", ext: ["jsonld"]},
 | 
					    {name: "PEG.js", mime: "null", mode: "pegjs", ext: ["jsonld"]},
 | 
				
			||||||
    {name: "Perl", mime: "text/x-perl", mode: "perl", ext: ["pl", "pm"]},
 | 
					    {name: "Perl", mime: "text/x-perl", mode: "perl", ext: ["pl", "pm"]},
 | 
				
			||||||
    {name: "PHP", mime: ["application/x-httpd-php", "text/x-php"], mode: "php", ext: ["php", "php3", "php4", "php5", "php7", "phtml"]},
 | 
					    {name: "PHP", mimes: ["text/x-php", "application/x-httpd-php", "application/x-httpd-php-open"], mode: "php", ext: ["php", "php3", "php4", "php5", "php7", "phtml"]},
 | 
				
			||||||
    {name: "Pig", mime: "text/x-pig", mode: "pig", ext: ["pig"]},
 | 
					    {name: "Pig", mime: "text/x-pig", mode: "pig", ext: ["pig"]},
 | 
				
			||||||
    {name: "Plain Text", mime: "text/plain", mode: "null", ext: ["txt", "text", "conf", "def", "list", "log"]},
 | 
					    {name: "Plain Text", mime: "text/plain", mode: "null", ext: ["txt", "text", "conf", "def", "list", "log"]},
 | 
				
			||||||
    {name: "PLSQL", mime: "text/x-plsql", mode: "sql", ext: ["pls"]},
 | 
					    {name: "PLSQL", mime: "text/x-plsql", mode: "sql", ext: ["pls"]},
 | 
				
			||||||
@@ -128,6 +128,7 @@
 | 
				
			|||||||
    {name: "Smalltalk", mime: "text/x-stsrc", mode: "smalltalk", ext: ["st"]},
 | 
					    {name: "Smalltalk", mime: "text/x-stsrc", mode: "smalltalk", ext: ["st"]},
 | 
				
			||||||
    {name: "Smarty", mime: "text/x-smarty", mode: "smarty", ext: ["tpl"]},
 | 
					    {name: "Smarty", mime: "text/x-smarty", mode: "smarty", ext: ["tpl"]},
 | 
				
			||||||
    {name: "Solr", mime: "text/x-solr", mode: "solr"},
 | 
					    {name: "Solr", mime: "text/x-solr", mode: "solr"},
 | 
				
			||||||
 | 
					    {name: "SML", mime: "text/x-sml", mode: "mllike", ext: ["sml", "sig", "fun", "smackspec"]},
 | 
				
			||||||
    {name: "Soy", mime: "text/x-soy", mode: "soy", ext: ["soy"], alias: ["closure template"]},
 | 
					    {name: "Soy", mime: "text/x-soy", mode: "soy", ext: ["soy"], alias: ["closure template"]},
 | 
				
			||||||
    {name: "SPARQL", mime: "application/sparql-query", mode: "sparql", ext: ["rq", "sparql"], alias: ["sparul"]},
 | 
					    {name: "SPARQL", mime: "application/sparql-query", mode: "sparql", ext: ["rq", "sparql"], alias: ["sparul"]},
 | 
				
			||||||
    {name: "Spreadsheet", mime: "text/x-spreadsheet", mode: "spreadsheet", alias: ["excel", "formula"]},
 | 
					    {name: "Spreadsheet", mime: "text/x-spreadsheet", mode: "spreadsheet", alias: ["excel", "formula"]},
 | 
				
			||||||
@@ -137,7 +138,7 @@
 | 
				
			|||||||
    {name: "Stylus", mime: "text/x-styl", mode: "stylus", ext: ["styl"]},
 | 
					    {name: "Stylus", mime: "text/x-styl", mode: "stylus", ext: ["styl"]},
 | 
				
			||||||
    {name: "Swift", mime: "text/x-swift", mode: "swift", ext: ["swift"]},
 | 
					    {name: "Swift", mime: "text/x-swift", mode: "swift", ext: ["swift"]},
 | 
				
			||||||
    {name: "sTeX", mime: "text/x-stex", mode: "stex"},
 | 
					    {name: "sTeX", mime: "text/x-stex", mode: "stex"},
 | 
				
			||||||
    {name: "LaTeX", mime: "text/x-latex", mode: "stex", ext: ["text", "ltx"], alias: ["tex"]},
 | 
					    {name: "LaTeX", mime: "text/x-latex", mode: "stex", ext: ["text", "ltx", "tex"], alias: ["tex"]},
 | 
				
			||||||
    {name: "SystemVerilog", mime: "text/x-systemverilog", mode: "verilog", ext: ["v", "sv", "svh"]},
 | 
					    {name: "SystemVerilog", mime: "text/x-systemverilog", mode: "verilog", ext: ["v", "sv", "svh"]},
 | 
				
			||||||
    {name: "Tcl", mime: "text/x-tcl", mode: "tcl", ext: ["tcl"]},
 | 
					    {name: "Tcl", mime: "text/x-tcl", mode: "tcl", ext: ["tcl"]},
 | 
				
			||||||
    {name: "Textile", mime: "text/x-textile", mode: "textile", ext: ["textile"]},
 | 
					    {name: "Textile", mime: "text/x-textile", mode: "textile", ext: ["textile"]},
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,31 +13,26 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
CodeMirror.defineMode('mllike', function(_config, parserConfig) {
 | 
					CodeMirror.defineMode('mllike', function(_config, parserConfig) {
 | 
				
			||||||
  var words = {
 | 
					  var words = {
 | 
				
			||||||
    'let': 'keyword',
 | 
					    'as': 'keyword',
 | 
				
			||||||
    'rec': 'keyword',
 | 
					 | 
				
			||||||
    'in': 'keyword',
 | 
					 | 
				
			||||||
    'of': 'keyword',
 | 
					 | 
				
			||||||
    'and': 'keyword',
 | 
					 | 
				
			||||||
    'if': 'keyword',
 | 
					 | 
				
			||||||
    'then': 'keyword',
 | 
					 | 
				
			||||||
    'else': 'keyword',
 | 
					 | 
				
			||||||
    'for': 'keyword',
 | 
					 | 
				
			||||||
    'to': 'keyword',
 | 
					 | 
				
			||||||
    'while': 'keyword',
 | 
					 | 
				
			||||||
    'do': 'keyword',
 | 
					    'do': 'keyword',
 | 
				
			||||||
    'done': 'keyword',
 | 
					    'else': 'keyword',
 | 
				
			||||||
 | 
					    'end': 'keyword',
 | 
				
			||||||
 | 
					    'exception': 'keyword',
 | 
				
			||||||
    'fun': 'keyword',
 | 
					    'fun': 'keyword',
 | 
				
			||||||
    'function': 'keyword',
 | 
					    'functor': 'keyword',
 | 
				
			||||||
    'val': 'keyword',
 | 
					    'if': 'keyword',
 | 
				
			||||||
 | 
					    'in': 'keyword',
 | 
				
			||||||
 | 
					    'include': 'keyword',
 | 
				
			||||||
 | 
					    'let': 'keyword',
 | 
				
			||||||
 | 
					    'of': 'keyword',
 | 
				
			||||||
 | 
					    'open': 'keyword',
 | 
				
			||||||
 | 
					    'rec': 'keyword',
 | 
				
			||||||
 | 
					    'struct': 'keyword',
 | 
				
			||||||
 | 
					    'then': 'keyword',
 | 
				
			||||||
    'type': 'keyword',
 | 
					    'type': 'keyword',
 | 
				
			||||||
    'mutable': 'keyword',
 | 
					    'val': 'keyword',
 | 
				
			||||||
    'match': 'keyword',
 | 
					    'while': 'keyword',
 | 
				
			||||||
    'with': 'keyword',
 | 
					    'with': 'keyword'
 | 
				
			||||||
    'try': 'keyword',
 | 
					 | 
				
			||||||
    'open': 'builtin',
 | 
					 | 
				
			||||||
    'ignore': 'builtin',
 | 
					 | 
				
			||||||
    'begin': 'keyword',
 | 
					 | 
				
			||||||
    'end': 'keyword'
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var extraWords = parserConfig.extraWords || {};
 | 
					  var extraWords = parserConfig.extraWords || {};
 | 
				
			||||||
@@ -68,7 +63,7 @@ CodeMirror.defineMode('mllike', function(_config, parserConfig) {
 | 
				
			|||||||
        return state.tokenize(stream, state);
 | 
					        return state.tokenize(stream, state);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (ch === '~') {
 | 
					    if (ch === '~' || ch === '?') {
 | 
				
			||||||
      stream.eatWhile(/\w/);
 | 
					      stream.eatWhile(/\w/);
 | 
				
			||||||
      return 'variable-2';
 | 
					      return 'variable-2';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -98,7 +93,7 @@ CodeMirror.defineMode('mllike', function(_config, parserConfig) {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      return 'number';
 | 
					      return 'number';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if ( /[+\-*&%=<>!?|@]/.test(ch)) {
 | 
					    if ( /[+\-*&%=<>!?|@\.~:]/.test(ch)) {
 | 
				
			||||||
      return 'operator';
 | 
					      return 'operator';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (/[\w\xa1-\uffff]/.test(ch)) {
 | 
					    if (/[\w\xa1-\uffff]/.test(ch)) {
 | 
				
			||||||
@@ -165,16 +160,64 @@ CodeMirror.defineMode('mllike', function(_config, parserConfig) {
 | 
				
			|||||||
CodeMirror.defineMIME('text/x-ocaml', {
 | 
					CodeMirror.defineMIME('text/x-ocaml', {
 | 
				
			||||||
  name: 'mllike',
 | 
					  name: 'mllike',
 | 
				
			||||||
  extraWords: {
 | 
					  extraWords: {
 | 
				
			||||||
    'succ': 'keyword',
 | 
					    'and': 'keyword',
 | 
				
			||||||
 | 
					    'assert': 'keyword',
 | 
				
			||||||
 | 
					    'begin': 'keyword',
 | 
				
			||||||
 | 
					    'class': 'keyword',
 | 
				
			||||||
 | 
					    'constraint': 'keyword',
 | 
				
			||||||
 | 
					    'done': 'keyword',
 | 
				
			||||||
 | 
					    'downto': 'keyword',
 | 
				
			||||||
 | 
					    'external': 'keyword',
 | 
				
			||||||
 | 
					    'function': 'keyword',
 | 
				
			||||||
 | 
					    'initializer': 'keyword',
 | 
				
			||||||
 | 
					    'lazy': 'keyword',
 | 
				
			||||||
 | 
					    'match': 'keyword',
 | 
				
			||||||
 | 
					    'method': 'keyword',
 | 
				
			||||||
 | 
					    'module': 'keyword',
 | 
				
			||||||
 | 
					    'mutable': 'keyword',
 | 
				
			||||||
 | 
					    'new': 'keyword',
 | 
				
			||||||
 | 
					    'nonrec': 'keyword',
 | 
				
			||||||
 | 
					    'object': 'keyword',
 | 
				
			||||||
 | 
					    'private': 'keyword',
 | 
				
			||||||
 | 
					    'sig': 'keyword',
 | 
				
			||||||
 | 
					    'to': 'keyword',
 | 
				
			||||||
 | 
					    'try': 'keyword',
 | 
				
			||||||
 | 
					    'value': 'keyword',
 | 
				
			||||||
 | 
					    'virtual': 'keyword',
 | 
				
			||||||
 | 
					    'when': 'keyword',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // builtins
 | 
				
			||||||
 | 
					    'raise': 'builtin',
 | 
				
			||||||
 | 
					    'failwith': 'builtin',
 | 
				
			||||||
 | 
					    'true': 'builtin',
 | 
				
			||||||
 | 
					    'false': 'builtin',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Pervasives builtins
 | 
				
			||||||
 | 
					    'asr': 'builtin',
 | 
				
			||||||
 | 
					    'land': 'builtin',
 | 
				
			||||||
 | 
					    'lor': 'builtin',
 | 
				
			||||||
 | 
					    'lsl': 'builtin',
 | 
				
			||||||
 | 
					    'lsr': 'builtin',
 | 
				
			||||||
 | 
					    'lxor': 'builtin',
 | 
				
			||||||
 | 
					    'mod': 'builtin',
 | 
				
			||||||
 | 
					    'or': 'builtin',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // More Pervasives
 | 
				
			||||||
 | 
					    'raise_notrace': 'builtin',
 | 
				
			||||||
    'trace': 'builtin',
 | 
					    'trace': 'builtin',
 | 
				
			||||||
    'exit': 'builtin',
 | 
					    'exit': 'builtin',
 | 
				
			||||||
    'print_string': 'builtin',
 | 
					    'print_string': 'builtin',
 | 
				
			||||||
    'print_endline': 'builtin',
 | 
					    'print_endline': 'builtin',
 | 
				
			||||||
    'true': 'atom',
 | 
					
 | 
				
			||||||
    'false': 'atom',
 | 
					     'int': 'type',
 | 
				
			||||||
    'raise': 'keyword',
 | 
					     'float': 'type',
 | 
				
			||||||
    'module': 'keyword',
 | 
					     'bool': 'type',
 | 
				
			||||||
    'sig': 'keyword'
 | 
					     'char': 'type',
 | 
				
			||||||
 | 
					     'string': 'type',
 | 
				
			||||||
 | 
					     'unit': 'type',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					     // Modules
 | 
				
			||||||
 | 
					     'List': 'builtin'
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -182,18 +225,21 @@ CodeMirror.defineMIME('text/x-fsharp', {
 | 
				
			|||||||
  name: 'mllike',
 | 
					  name: 'mllike',
 | 
				
			||||||
  extraWords: {
 | 
					  extraWords: {
 | 
				
			||||||
    'abstract': 'keyword',
 | 
					    'abstract': 'keyword',
 | 
				
			||||||
    'as': 'keyword',
 | 
					 | 
				
			||||||
    'assert': 'keyword',
 | 
					    'assert': 'keyword',
 | 
				
			||||||
    'base': 'keyword',
 | 
					    'base': 'keyword',
 | 
				
			||||||
 | 
					    'begin': 'keyword',
 | 
				
			||||||
    'class': 'keyword',
 | 
					    'class': 'keyword',
 | 
				
			||||||
    'default': 'keyword',
 | 
					    'default': 'keyword',
 | 
				
			||||||
    'delegate': 'keyword',
 | 
					    'delegate': 'keyword',
 | 
				
			||||||
 | 
					    'do!': 'keyword',
 | 
				
			||||||
 | 
					    'done': 'keyword',
 | 
				
			||||||
    'downcast': 'keyword',
 | 
					    'downcast': 'keyword',
 | 
				
			||||||
    'downto': 'keyword',
 | 
					    'downto': 'keyword',
 | 
				
			||||||
    'elif': 'keyword',
 | 
					    'elif': 'keyword',
 | 
				
			||||||
    'exception': 'keyword',
 | 
					 | 
				
			||||||
    'extern': 'keyword',
 | 
					    'extern': 'keyword',
 | 
				
			||||||
    'finally': 'keyword',
 | 
					    'finally': 'keyword',
 | 
				
			||||||
 | 
					    'for': 'keyword',
 | 
				
			||||||
 | 
					    'function': 'keyword',
 | 
				
			||||||
    'global': 'keyword',
 | 
					    'global': 'keyword',
 | 
				
			||||||
    'inherit': 'keyword',
 | 
					    'inherit': 'keyword',
 | 
				
			||||||
    'inline': 'keyword',
 | 
					    'inline': 'keyword',
 | 
				
			||||||
@@ -201,38 +247,108 @@ CodeMirror.defineMIME('text/x-fsharp', {
 | 
				
			|||||||
    'internal': 'keyword',
 | 
					    'internal': 'keyword',
 | 
				
			||||||
    'lazy': 'keyword',
 | 
					    'lazy': 'keyword',
 | 
				
			||||||
    'let!': 'keyword',
 | 
					    'let!': 'keyword',
 | 
				
			||||||
    'member' : 'keyword',
 | 
					    'match': 'keyword',
 | 
				
			||||||
 | 
					    'member': 'keyword',
 | 
				
			||||||
    'module': 'keyword',
 | 
					    'module': 'keyword',
 | 
				
			||||||
 | 
					    'mutable': 'keyword',
 | 
				
			||||||
    'namespace': 'keyword',
 | 
					    'namespace': 'keyword',
 | 
				
			||||||
    'new': 'keyword',
 | 
					    'new': 'keyword',
 | 
				
			||||||
    'null': 'keyword',
 | 
					    'null': 'keyword',
 | 
				
			||||||
    'override': 'keyword',
 | 
					    'override': 'keyword',
 | 
				
			||||||
    'private': 'keyword',
 | 
					    'private': 'keyword',
 | 
				
			||||||
    'public': 'keyword',
 | 
					    'public': 'keyword',
 | 
				
			||||||
    'return': 'keyword',
 | 
					 | 
				
			||||||
    'return!': 'keyword',
 | 
					    'return!': 'keyword',
 | 
				
			||||||
 | 
					    'return': 'keyword',
 | 
				
			||||||
    'select': 'keyword',
 | 
					    'select': 'keyword',
 | 
				
			||||||
    'static': 'keyword',
 | 
					    'static': 'keyword',
 | 
				
			||||||
    'struct': 'keyword',
 | 
					    'to': 'keyword',
 | 
				
			||||||
 | 
					    'try': 'keyword',
 | 
				
			||||||
    'upcast': 'keyword',
 | 
					    'upcast': 'keyword',
 | 
				
			||||||
    'use': 'keyword',
 | 
					 | 
				
			||||||
    'use!': 'keyword',
 | 
					    'use!': 'keyword',
 | 
				
			||||||
    'val': 'keyword',
 | 
					    'use': 'keyword',
 | 
				
			||||||
 | 
					    'void': 'keyword',
 | 
				
			||||||
    'when': 'keyword',
 | 
					    'when': 'keyword',
 | 
				
			||||||
    'yield': 'keyword',
 | 
					 | 
				
			||||||
    'yield!': 'keyword',
 | 
					    'yield!': 'keyword',
 | 
				
			||||||
 | 
					    'yield': 'keyword',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Reserved words
 | 
				
			||||||
 | 
					    'atomic': 'keyword',
 | 
				
			||||||
 | 
					    'break': 'keyword',
 | 
				
			||||||
 | 
					    'checked': 'keyword',
 | 
				
			||||||
 | 
					    'component': 'keyword',
 | 
				
			||||||
 | 
					    'const': 'keyword',
 | 
				
			||||||
 | 
					    'constraint': 'keyword',
 | 
				
			||||||
 | 
					    'constructor': 'keyword',
 | 
				
			||||||
 | 
					    'continue': 'keyword',
 | 
				
			||||||
 | 
					    'eager': 'keyword',
 | 
				
			||||||
 | 
					    'event': 'keyword',
 | 
				
			||||||
 | 
					    'external': 'keyword',
 | 
				
			||||||
 | 
					    'fixed': 'keyword',
 | 
				
			||||||
 | 
					    'method': 'keyword',
 | 
				
			||||||
 | 
					    'mixin': 'keyword',
 | 
				
			||||||
 | 
					    'object': 'keyword',
 | 
				
			||||||
 | 
					    'parallel': 'keyword',
 | 
				
			||||||
 | 
					    'process': 'keyword',
 | 
				
			||||||
 | 
					    'protected': 'keyword',
 | 
				
			||||||
 | 
					    'pure': 'keyword',
 | 
				
			||||||
 | 
					    'sealed': 'keyword',
 | 
				
			||||||
 | 
					    'tailcall': 'keyword',
 | 
				
			||||||
 | 
					    'trait': 'keyword',
 | 
				
			||||||
 | 
					    'virtual': 'keyword',
 | 
				
			||||||
 | 
					    'volatile': 'keyword',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // builtins
 | 
				
			||||||
    'List': 'builtin',
 | 
					    'List': 'builtin',
 | 
				
			||||||
    'Seq': 'builtin',
 | 
					    'Seq': 'builtin',
 | 
				
			||||||
    'Map': 'builtin',
 | 
					    'Map': 'builtin',
 | 
				
			||||||
    'Set': 'builtin',
 | 
					    'Set': 'builtin',
 | 
				
			||||||
 | 
					    'Option': 'builtin',
 | 
				
			||||||
    'int': 'builtin',
 | 
					    'int': 'builtin',
 | 
				
			||||||
    'string': 'builtin',
 | 
					    'string': 'builtin',
 | 
				
			||||||
    'raise': 'builtin',
 | 
					 | 
				
			||||||
    'failwith': 'builtin',
 | 
					 | 
				
			||||||
    'not': 'builtin',
 | 
					    'not': 'builtin',
 | 
				
			||||||
    'true': 'builtin',
 | 
					    'true': 'builtin',
 | 
				
			||||||
    'false': 'builtin'
 | 
					    'false': 'builtin',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    'raise': 'builtin',
 | 
				
			||||||
 | 
					    'failwith': 'builtin'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  slashComments: true
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CodeMirror.defineMIME('text/x-sml', {
 | 
				
			||||||
 | 
					  name: 'mllike',
 | 
				
			||||||
 | 
					  extraWords: {
 | 
				
			||||||
 | 
					    'abstype': 'keyword',
 | 
				
			||||||
 | 
					    'and': 'keyword',
 | 
				
			||||||
 | 
					    'andalso': 'keyword',
 | 
				
			||||||
 | 
					    'case': 'keyword',
 | 
				
			||||||
 | 
					    'datatype': 'keyword',
 | 
				
			||||||
 | 
					    'fn': 'keyword',
 | 
				
			||||||
 | 
					    'handle': 'keyword',
 | 
				
			||||||
 | 
					    'infix': 'keyword',
 | 
				
			||||||
 | 
					    'infixr': 'keyword',
 | 
				
			||||||
 | 
					    'local': 'keyword',
 | 
				
			||||||
 | 
					    'nonfix': 'keyword',
 | 
				
			||||||
 | 
					    'op': 'keyword',
 | 
				
			||||||
 | 
					    'orelse': 'keyword',
 | 
				
			||||||
 | 
					    'raise': 'keyword',
 | 
				
			||||||
 | 
					    'withtype': 'keyword',
 | 
				
			||||||
 | 
					    'eqtype': 'keyword',
 | 
				
			||||||
 | 
					    'sharing': 'keyword',
 | 
				
			||||||
 | 
					    'sig': 'keyword',
 | 
				
			||||||
 | 
					    'signature': 'keyword',
 | 
				
			||||||
 | 
					    'structure': 'keyword',
 | 
				
			||||||
 | 
					    'where': 'keyword',
 | 
				
			||||||
 | 
					    'true': 'keyword',
 | 
				
			||||||
 | 
					    'false': 'keyword',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // types
 | 
				
			||||||
 | 
					    'int': 'builtin',
 | 
				
			||||||
 | 
					    'real': 'builtin',
 | 
				
			||||||
 | 
					    'string': 'builtin',
 | 
				
			||||||
 | 
					    'char': 'builtin',
 | 
				
			||||||
 | 
					    'bool': 'builtin'
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  slashComments: true
 | 
					  slashComments: true
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
<!doctype html>
 | 
					<!doctype html>
 | 
				
			||||||
<head>
 | 
					<head>
 | 
				
			||||||
<title>CodeMirror: NGINX mode</title>
 | 
					<title>CodeMirror: NGINX mode</title>
 | 
				
			||||||
<meta charset="utf-8"/>
 | 
					<meta charset="utf-8"/>
 | 
				
			||||||
@@ -176,6 +176,6 @@ server {
 | 
				
			|||||||
      var editor = CodeMirror.fromTextArea(document.getElementById("code"), {});
 | 
					      var editor = CodeMirror.fromTextArea(document.getElementById("code"), {});
 | 
				
			||||||
    </script>
 | 
					    </script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <p><strong>MIME types defined:</strong> <code>text/nginx</code>.</p>
 | 
					    <p><strong>MIME types defined:</strong> <code>text/x-nginx-conf</code>.</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  </article>
 | 
					  </article>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,14 +24,14 @@ CodeMirror.defineSimpleMode("nsis",{
 | 
				
			|||||||
    { regex: /`(?:[^\\`]|\\.)*`?/, token: "string" },
 | 
					    { regex: /`(?:[^\\`]|\\.)*`?/, token: "string" },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Compile Time Commands
 | 
					    // Compile Time Commands
 | 
				
			||||||
    {regex: /^\s*(?:\!(include|addincludedir|addplugindir|appendfile|cd|delfile|echo|error|execute|packhdr|pragma|finalize|getdllversion|system|tempfile|warning|verbose|define|undef|insertmacro|makensis|searchparse|searchreplace))\b/, token: "keyword"},
 | 
					    {regex: /^\s*(?:\!(include|addincludedir|addplugindir|appendfile|cd|delfile|echo|error|execute|packhdr|pragma|finalize|getdllversion|gettlbversion|system|tempfile|warning|verbose|define|undef|insertmacro|macro|macroend|makensis|searchparse|searchreplace))\b/, token: "keyword"},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Conditional Compilation
 | 
					    // Conditional Compilation
 | 
				
			||||||
    {regex: /^\s*(?:\!(if(?:n?def)?|ifmacron?def|macro))\b/, token: "keyword", indent: true},
 | 
					    {regex: /^\s*(?:\!(if(?:n?def)?|ifmacron?def|macro))\b/, token: "keyword", indent: true},
 | 
				
			||||||
    {regex: /^\s*(?:\!(else|endif|macroend))\b/, token: "keyword", dedent: true},
 | 
					    {regex: /^\s*(?:\!(else|endif|macroend))\b/, token: "keyword", dedent: true},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Runtime Commands
 | 
					    // Runtime Commands
 | 
				
			||||||
    {regex: /^\s*(?:Abort|AddBrandingImage|AddSize|AllowRootDirInstall|AllowSkipFiles|AutoCloseWindow|BGFont|BGGradient|BrandingText|BringToFront|Call|CallInstDLL|Caption|ChangeUI|CheckBitmap|ClearErrors|CompletedText|ComponentText|CopyFiles|CRCCheck|CreateDirectory|CreateFont|CreateShortCut|Delete|DeleteINISec|DeleteINIStr|DeleteRegKey|DeleteRegValue|DetailPrint|DetailsButtonText|DirText|DirVar|DirVerify|EnableWindow|EnumRegKey|EnumRegValue|Exch|Exec|ExecShell|ExecShellWait|ExecWait|ExpandEnvStrings|File|FileBufSize|FileClose|FileErrorText|FileOpen|FileRead|FileReadByte|FileReadUTF16LE|FileReadWord|FileWriteUTF16LE|FileSeek|FileWrite|FileWriteByte|FileWriteWord|FindClose|FindFirst|FindNext|FindWindow|FlushINI|GetCurInstType|GetCurrentAddress|GetDlgItem|GetDLLVersion|GetDLLVersionLocal|GetErrorLevel|GetFileTime|GetFileTimeLocal|GetFullPathName|GetFunctionAddress|GetInstDirError|GetLabelAddress|GetTempFileName|Goto|HideWindow|Icon|IfAbort|IfErrors|IfFileExists|IfRebootFlag|IfSilent|InitPluginsDir|InstallButtonText|InstallColors|InstallDir|InstallDirRegKey|InstProgressFlags|InstType|InstTypeGetText|InstTypeSetText|IntCmp|IntCmpU|IntFmt|IntOp|IsWindow|LangString|LicenseBkColor|LicenseData|LicenseForceSelection|LicenseLangString|LicenseText|LoadLanguageFile|LockWindow|LogSet|LogText|ManifestDPIAware|ManifestSupportedOS|MessageBox|MiscButtonText|Name|Nop|OutFile|Page|PageCallbacks|Pop|Push|Quit|ReadEnvStr|ReadINIStr|ReadRegDWORD|ReadRegStr|Reboot|RegDLL|Rename|RequestExecutionLevel|ReserveFile|Return|RMDir|SearchPath|SectionGetFlags|SectionGetInstTypes|SectionGetSize|SectionGetText|SectionIn|SectionSetFlags|SectionSetInstTypes|SectionSetSize|SectionSetText|SendMessage|SetAutoClose|SetBrandingImage|SetCompress|SetCompressor|SetCompressorDictSize|SetCtlColors|SetCurInstType|SetDatablockOptimize|SetDateSave|SetDetailsPrint|SetDetailsView|SetErrorLevel|SetErrors|SetFileAttributes|SetFont|SetOutPath|SetOverwrite|SetRebootFlag|SetRegView|SetShellVarContext|SetSilent|ShowInstDetails|ShowUninstDetails|ShowWindow|SilentInstall|SilentUnInstall|Sleep|SpaceTexts|StrCmp|StrCmpS|StrCpy|StrLen|SubCaption|Unicode|UninstallButtonText|UninstallCaption|UninstallIcon|UninstallSubCaption|UninstallText|UninstPage|UnRegDLL|Var|VIAddVersionKey|VIFileVersion|VIProductVersion|WindowIcon|WriteINIStr|WriteRegBin|WriteRegDWORD|WriteRegExpandStr|WriteRegMultiStr|WriteRegNone|WriteRegStr|WriteUninstaller|XPStyle)\b/, token: "keyword"},
 | 
					    {regex: /^\s*(?:Abort|AddBrandingImage|AddSize|AllowRootDirInstall|AllowSkipFiles|AutoCloseWindow|BGFont|BGGradient|BrandingText|BringToFront|Call|CallInstDLL|Caption|ChangeUI|CheckBitmap|ClearErrors|CompletedText|ComponentText|CopyFiles|CRCCheck|CreateDirectory|CreateFont|CreateShortCut|Delete|DeleteINISec|DeleteINIStr|DeleteRegKey|DeleteRegValue|DetailPrint|DetailsButtonText|DirText|DirVar|DirVerify|EnableWindow|EnumRegKey|EnumRegValue|Exch|Exec|ExecShell|ExecShellWait|ExecWait|ExpandEnvStrings|File|FileBufSize|FileClose|FileErrorText|FileOpen|FileRead|FileReadByte|FileReadUTF16LE|FileReadWord|FileWriteUTF16LE|FileSeek|FileWrite|FileWriteByte|FileWriteWord|FindClose|FindFirst|FindNext|FindWindow|FlushINI|GetCurInstType|GetCurrentAddress|GetDlgItem|GetDLLVersion|GetDLLVersionLocal|GetErrorLevel|GetFileTime|GetFileTimeLocal|GetFullPathName|GetFunctionAddress|GetInstDirError|GetLabelAddress|GetTempFileName|Goto|HideWindow|Icon|IfAbort|IfErrors|IfFileExists|IfRebootFlag|IfSilent|InitPluginsDir|InstallButtonText|InstallColors|InstallDir|InstallDirRegKey|InstProgressFlags|InstType|InstTypeGetText|InstTypeSetText|Int64Cmp|Int64CmpU|Int64Fmt|IntCmp|IntCmpU|IntFmt|IntOp|IntPtrCmp|IntPtrCmpU|IntPtrOp|IsWindow|LangString|LicenseBkColor|LicenseData|LicenseForceSelection|LicenseLangString|LicenseText|LoadLanguageFile|LockWindow|LogSet|LogText|ManifestDPIAware|ManifestSupportedOS|MessageBox|MiscButtonText|Name|Nop|OutFile|Page|PageCallbacks|PEDllCharacteristics|PESubsysVer|Pop|Push|Quit|ReadEnvStr|ReadINIStr|ReadRegDWORD|ReadRegStr|Reboot|RegDLL|Rename|RequestExecutionLevel|ReserveFile|Return|RMDir|SearchPath|SectionGetFlags|SectionGetInstTypes|SectionGetSize|SectionGetText|SectionIn|SectionSetFlags|SectionSetInstTypes|SectionSetSize|SectionSetText|SendMessage|SetAutoClose|SetBrandingImage|SetCompress|SetCompressor|SetCompressorDictSize|SetCtlColors|SetCurInstType|SetDatablockOptimize|SetDateSave|SetDetailsPrint|SetDetailsView|SetErrorLevel|SetErrors|SetFileAttributes|SetFont|SetOutPath|SetOverwrite|SetRebootFlag|SetRegView|SetShellVarContext|SetSilent|ShowInstDetails|ShowUninstDetails|ShowWindow|SilentInstall|SilentUnInstall|Sleep|SpaceTexts|StrCmp|StrCmpS|StrCpy|StrLen|SubCaption|Unicode|UninstallButtonText|UninstallCaption|UninstallIcon|UninstallSubCaption|UninstallText|UninstPage|UnRegDLL|Var|VIAddVersionKey|VIFileVersion|VIProductVersion|WindowIcon|WriteINIStr|WriteRegBin|WriteRegDWORD|WriteRegExpandStr|WriteRegMultiStr|WriteRegNone|WriteRegStr|WriteUninstaller|XPStyle)\b/, token: "keyword"},
 | 
				
			||||||
    {regex: /^\s*(?:Function|PageEx|Section(?:Group)?)\b/, token: "keyword", indent: true},
 | 
					    {regex: /^\s*(?:Function|PageEx|Section(?:Group)?)\b/, token: "keyword", indent: true},
 | 
				
			||||||
    {regex: /^\s*(?:(Function|PageEx|Section(?:Group)?)End)\b/, token: "keyword", dedent: true},
 | 
					    {regex: /^\s*(?:(Function|PageEx|Section(?:Group)?)End)\b/, token: "keyword", dedent: true},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,9 +17,21 @@ CodeMirror.defineMode("pascal", function() {
 | 
				
			|||||||
    for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
 | 
					    for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
 | 
				
			||||||
    return obj;
 | 
					    return obj;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  var keywords = words("and array begin case const div do downto else end file for forward integer " +
 | 
					  var keywords = words(
 | 
				
			||||||
                       "boolean char function goto if in label mod nil not of or packed procedure " +
 | 
					    "absolute and array asm begin case const constructor destructor div do " +
 | 
				
			||||||
                       "program record repeat set string then to type until var while with");
 | 
					    "downto else end file for function goto if implementation in inherited " +
 | 
				
			||||||
 | 
					    "inline interface label mod nil not object of operator or packed procedure " +
 | 
				
			||||||
 | 
					    "program record reintroduce repeat self set shl shr string then to type " +
 | 
				
			||||||
 | 
					    "unit until uses var while with xor as class dispinterface except exports " +
 | 
				
			||||||
 | 
					    "finalization finally initialization inline is library on out packed " +
 | 
				
			||||||
 | 
					    "property raise resourcestring threadvar try absolute abstract alias " +
 | 
				
			||||||
 | 
					    "assembler bitpacked break cdecl continue cppdecl cvar default deprecated " +
 | 
				
			||||||
 | 
					    "dynamic enumerator experimental export external far far16 forward generic " +
 | 
				
			||||||
 | 
					    "helper implements index interrupt iocheck local message name near " +
 | 
				
			||||||
 | 
					    "nodefault noreturn nostackframe oldfpccall otherwise overload override " +
 | 
				
			||||||
 | 
					    "pascal platform private protected public published read register " +
 | 
				
			||||||
 | 
					    "reintroduce result safecall saveregisters softfloat specialize static " +
 | 
				
			||||||
 | 
					    "stdcall stored strict unaligned unimplemented varargs virtual write");
 | 
				
			||||||
  var atoms = {"null": true};
 | 
					  var atoms = {"null": true};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var isOperatorChar = /[+\-*&%=<>!?|\/]/;
 | 
					  var isOperatorChar = /[+\-*&%=<>!?|\/]/;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -126,6 +126,15 @@ class ExampleClass(ParentClass):
 | 
				
			|||||||
    def __init__(self, mixin = 'Hello'):
 | 
					    def __init__(self, mixin = 'Hello'):
 | 
				
			||||||
        self.mixin = mixin
 | 
					        self.mixin = mixin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Python 3.6 f-strings (https://www.python.org/dev/peps/pep-0498/)
 | 
				
			||||||
 | 
					f'My name is {name}, my age next year is {age+1}, my anniversary is {anniversary:%A, %B %d, %Y}.'
 | 
				
			||||||
 | 
					f'He said his name is {name!r}.'
 | 
				
			||||||
 | 
					f"""He said his name is {name!r}."""
 | 
				
			||||||
 | 
					f'{"quoted string"}'
 | 
				
			||||||
 | 
					f'{{ {4*10} }}'
 | 
				
			||||||
 | 
					f'This is an error }'
 | 
				
			||||||
 | 
					f'This is ok }}'
 | 
				
			||||||
 | 
					fr'x={4*10}\n'
 | 
				
			||||||
</textarea></div>
 | 
					</textarea></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,7 +41,7 @@
 | 
				
			|||||||
  CodeMirror.defineMode("python", function(conf, parserConf) {
 | 
					  CodeMirror.defineMode("python", function(conf, parserConf) {
 | 
				
			||||||
    var ERRORCLASS = "error";
 | 
					    var ERRORCLASS = "error";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var delimiters = parserConf.delimiters || parserConf.singleDelimiters || /^[\(\)\[\]\{\}@,:`=;\.]/;
 | 
					    var delimiters = parserConf.delimiters || parserConf.singleDelimiters || /^[\(\)\[\]\{\}@,:`=;\.\\]/;
 | 
				
			||||||
    //               (Backwards-compatiblity with old, cumbersome config system)
 | 
					    //               (Backwards-compatiblity with old, cumbersome config system)
 | 
				
			||||||
    var operators = [parserConf.singleOperators, parserConf.doubleOperators, parserConf.doubleDelimiters, parserConf.tripleDelimiters,
 | 
					    var operators = [parserConf.singleOperators, parserConf.doubleOperators, parserConf.doubleDelimiters, parserConf.tripleDelimiters,
 | 
				
			||||||
                     parserConf.operators || /^([-+*/%\/&|^]=?|[<>=]+|\/\/=?|\*\*=?|!=|[~!@])/]
 | 
					                     parserConf.operators || /^([-+*/%\/&|^]=?|[<>=]+|\/\/=?|\*\*=?|!=|[~!@])/]
 | 
				
			||||||
@@ -62,7 +62,7 @@
 | 
				
			|||||||
      var identifiers = parserConf.identifiers|| /^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*/;
 | 
					      var identifiers = parserConf.identifiers|| /^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*/;
 | 
				
			||||||
      myKeywords = myKeywords.concat(["nonlocal", "False", "True", "None", "async", "await"]);
 | 
					      myKeywords = myKeywords.concat(["nonlocal", "False", "True", "None", "async", "await"]);
 | 
				
			||||||
      myBuiltins = myBuiltins.concat(["ascii", "bytes", "exec", "print"]);
 | 
					      myBuiltins = myBuiltins.concat(["ascii", "bytes", "exec", "print"]);
 | 
				
			||||||
      var stringPrefixes = new RegExp("^(([rbuf]|(br))?('{3}|\"{3}|['\"]))", "i");
 | 
					      var stringPrefixes = new RegExp("^(([rbuf]|(br)|(fr))?('{3}|\"{3}|['\"]))", "i");
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      var identifiers = parserConf.identifiers|| /^[_A-Za-z][_A-Za-z0-9]*/;
 | 
					      var identifiers = parserConf.identifiers|| /^[_A-Za-z][_A-Za-z0-9]*/;
 | 
				
			||||||
      myKeywords = myKeywords.concat(["exec", "print"]);
 | 
					      myKeywords = myKeywords.concat(["exec", "print"]);
 | 
				
			||||||
@@ -76,9 +76,10 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // tokenizers
 | 
					    // tokenizers
 | 
				
			||||||
    function tokenBase(stream, state) {
 | 
					    function tokenBase(stream, state) {
 | 
				
			||||||
      if (stream.sol()) state.indent = stream.indentation()
 | 
					      var sol = stream.sol() && state.lastToken != "\\"
 | 
				
			||||||
 | 
					      if (sol) state.indent = stream.indentation()
 | 
				
			||||||
      // Handle scope changes
 | 
					      // Handle scope changes
 | 
				
			||||||
      if (stream.sol() && top(state).type == "py") {
 | 
					      if (sol && top(state).type == "py") {
 | 
				
			||||||
        var scopeOffset = top(state).offset;
 | 
					        var scopeOffset = top(state).offset;
 | 
				
			||||||
        if (stream.eatSpace()) {
 | 
					        if (stream.eatSpace()) {
 | 
				
			||||||
          var lineOffset = stream.indentation();
 | 
					          var lineOffset = stream.indentation();
 | 
				
			||||||
@@ -100,13 +101,8 @@
 | 
				
			|||||||
    function tokenBaseInner(stream, state) {
 | 
					    function tokenBaseInner(stream, state) {
 | 
				
			||||||
      if (stream.eatSpace()) return null;
 | 
					      if (stream.eatSpace()) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var ch = stream.peek();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Handle Comments
 | 
					      // Handle Comments
 | 
				
			||||||
      if (ch == "#") {
 | 
					      if (stream.match(/^#.*/)) return "comment";
 | 
				
			||||||
        stream.skipToEnd();
 | 
					 | 
				
			||||||
        return "comment";
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Handle Number Literals
 | 
					      // Handle Number Literals
 | 
				
			||||||
      if (stream.match(/^[0-9\.]/, false)) {
 | 
					      if (stream.match(/^[0-9\.]/, false)) {
 | 
				
			||||||
@@ -146,8 +142,14 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      // Handle Strings
 | 
					      // Handle Strings
 | 
				
			||||||
      if (stream.match(stringPrefixes)) {
 | 
					      if (stream.match(stringPrefixes)) {
 | 
				
			||||||
 | 
					        var isFmtString = stream.current().toLowerCase().indexOf('f') !== -1;
 | 
				
			||||||
 | 
					        if (!isFmtString) {
 | 
				
			||||||
          state.tokenize = tokenStringFactory(stream.current());
 | 
					          state.tokenize = tokenStringFactory(stream.current());
 | 
				
			||||||
          return state.tokenize(stream, state);
 | 
					          return state.tokenize(stream, state);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          state.tokenize = formatStringFactory(stream.current(), state.tokenize);
 | 
				
			||||||
 | 
					          return state.tokenize(stream, state);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (var i = 0; i < operators.length; i++)
 | 
					      for (var i = 0; i < operators.length; i++)
 | 
				
			||||||
@@ -178,6 +180,77 @@
 | 
				
			|||||||
      return ERRORCLASS;
 | 
					      return ERRORCLASS;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function formatStringFactory(delimiter, tokenOuter) {
 | 
				
			||||||
 | 
					      while ("rubf".indexOf(delimiter.charAt(0).toLowerCase()) >= 0)
 | 
				
			||||||
 | 
					        delimiter = delimiter.substr(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var singleline = delimiter.length == 1;
 | 
				
			||||||
 | 
					      var OUTCLASS = "string";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      function tokenFString(stream, state) {
 | 
				
			||||||
 | 
					        // inside f-str Expression
 | 
				
			||||||
 | 
					        if (stream.match(delimiter)) {
 | 
				
			||||||
 | 
					          // expression ends pre-maturally, but very common in editing
 | 
				
			||||||
 | 
					          // Could show error to remind users to close brace here
 | 
				
			||||||
 | 
					          state.tokenize = tokenString
 | 
				
			||||||
 | 
					          return OUTCLASS;
 | 
				
			||||||
 | 
					        } else if (stream.match('{')) {
 | 
				
			||||||
 | 
					          // starting brace, if not eaten below
 | 
				
			||||||
 | 
					          return "punctuation";
 | 
				
			||||||
 | 
					        } else if (stream.match('}')) {
 | 
				
			||||||
 | 
					          // return to regular inside string state
 | 
				
			||||||
 | 
					          state.tokenize = tokenString
 | 
				
			||||||
 | 
					          return "punctuation";
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // use tokenBaseInner to parse the expression
 | 
				
			||||||
 | 
					          return tokenBaseInner(stream, state);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      function tokenString(stream, state) {
 | 
				
			||||||
 | 
					        while (!stream.eol()) {
 | 
				
			||||||
 | 
					          stream.eatWhile(/[^'"\{\}\\]/);
 | 
				
			||||||
 | 
					          if (stream.eat("\\")) {
 | 
				
			||||||
 | 
					            stream.next();
 | 
				
			||||||
 | 
					            if (singleline && stream.eol())
 | 
				
			||||||
 | 
					              return OUTCLASS;
 | 
				
			||||||
 | 
					          } else if (stream.match(delimiter)) {
 | 
				
			||||||
 | 
					            state.tokenize = tokenOuter;
 | 
				
			||||||
 | 
					            return OUTCLASS;
 | 
				
			||||||
 | 
					          } else if (stream.match('{{')) {
 | 
				
			||||||
 | 
					            // ignore {{ in f-str
 | 
				
			||||||
 | 
					            return OUTCLASS;
 | 
				
			||||||
 | 
					          } else if (stream.match('{', false)) {
 | 
				
			||||||
 | 
					            // switch to nested mode
 | 
				
			||||||
 | 
					            state.tokenize = tokenFString
 | 
				
			||||||
 | 
					            if (stream.current()) {
 | 
				
			||||||
 | 
					              return OUTCLASS;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              // need to return something, so eat the starting {
 | 
				
			||||||
 | 
					              stream.next();
 | 
				
			||||||
 | 
					              return "punctuation";
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          } else if (stream.match('}}')) {
 | 
				
			||||||
 | 
					            return OUTCLASS;
 | 
				
			||||||
 | 
					          } else if (stream.match('}')) {
 | 
				
			||||||
 | 
					            // single } in f-string is an error
 | 
				
			||||||
 | 
					            return ERRORCLASS;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            stream.eat(/['"]/);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (singleline) {
 | 
				
			||||||
 | 
					          if (parserConf.singleLineStringErrors)
 | 
				
			||||||
 | 
					            return ERRORCLASS;
 | 
				
			||||||
 | 
					          else
 | 
				
			||||||
 | 
					            state.tokenize = tokenOuter;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return OUTCLASS;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      tokenString.isString = true;
 | 
				
			||||||
 | 
					      return tokenString;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function tokenStringFactory(delimiter) {
 | 
					    function tokenStringFactory(delimiter) {
 | 
				
			||||||
      while ("rubf".indexOf(delimiter.charAt(0).toLowerCase()) >= 0)
 | 
					      while ("rubf".indexOf(delimiter.charAt(0).toLowerCase()) >= 0)
 | 
				
			||||||
        delimiter = delimiter.substr(1);
 | 
					        delimiter = delimiter.substr(1);
 | 
				
			||||||
@@ -258,7 +331,8 @@
 | 
				
			|||||||
      if (current == ":" && !state.lambda && top(state).type == "py")
 | 
					      if (current == ":" && !state.lambda && top(state).type == "py")
 | 
				
			||||||
        pushPyScope(state);
 | 
					        pushPyScope(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var delimiter_index = current.length == 1 ? "[({".indexOf(current) : -1;
 | 
					      if (current.length == 1 && !/string|comment/.test(style)) {
 | 
				
			||||||
 | 
					        var delimiter_index = "[({".indexOf(current);
 | 
				
			||||||
        if (delimiter_index != -1)
 | 
					        if (delimiter_index != -1)
 | 
				
			||||||
          pushBracketScope(stream, state, "])}".slice(delimiter_index, delimiter_index+1));
 | 
					          pushBracketScope(stream, state, "])}".slice(delimiter_index, delimiter_index+1));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -267,6 +341,7 @@
 | 
				
			|||||||
          if (top(state).type == current) state.indent = state.scopes.pop().offset - hangingIndent
 | 
					          if (top(state).type == current) state.indent = state.scopes.pop().offset - hangingIndent
 | 
				
			||||||
          else return ERRORCLASS;
 | 
					          else return ERRORCLASS;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      if (state.dedent > 0 && stream.eol() && top(state).type == "py") {
 | 
					      if (state.dedent > 0 && stream.eol() && top(state).type == "py") {
 | 
				
			||||||
        if (state.scopes.length > 1) state.scopes.pop();
 | 
					        if (state.scopes.length > 1) state.scopes.pop();
 | 
				
			||||||
        state.dedent -= 1;
 | 
					        state.dedent -= 1;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,6 +30,9 @@
 | 
				
			|||||||
    MT("before_equal_sign_" + c, "[variable a] [operator " + c + "=] [variable b]");
 | 
					    MT("before_equal_sign_" + c, "[variable a] [operator " + c + "=] [variable b]");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  MT("fValidStringPrefix", "[string f'this is a {formatted} string']");
 | 
					  MT("fValidStringPrefix", "[string f'this is a]{[variable formatted]}[string string']");
 | 
				
			||||||
 | 
					  MT("fValidExpressioninFString", "[string f'expression ]{[number 100][operator *][number 5]}[string string']");
 | 
				
			||||||
 | 
					  MT("fInvalidFString", "[error f'this is wrong}]");
 | 
				
			||||||
 | 
					  MT("fNestedFString", "[string f'expression ]{[number 100] [operator +] [string f'inner]{[number 5]}[string ']}[string string']");
 | 
				
			||||||
  MT("uValidStringPrefix", "[string u'this is an unicode string']");
 | 
					  MT("uValidStringPrefix", "[string u'this is an unicode string']");
 | 
				
			||||||
})();
 | 
					})();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -84,29 +84,38 @@ CodeMirror.defineMode('shell', function() {
 | 
				
			|||||||
  function tokenString(quote, style) {
 | 
					  function tokenString(quote, style) {
 | 
				
			||||||
    var close = quote == "(" ? ")" : quote == "{" ? "}" : quote
 | 
					    var close = quote == "(" ? ")" : quote == "{" ? "}" : quote
 | 
				
			||||||
    return function(stream, state) {
 | 
					    return function(stream, state) {
 | 
				
			||||||
      var next, end = false, escaped = false;
 | 
					      var next, escaped = false;
 | 
				
			||||||
      while ((next = stream.next()) != null) {
 | 
					      while ((next = stream.next()) != null) {
 | 
				
			||||||
        if (next === close && !escaped) {
 | 
					        if (next === close && !escaped) {
 | 
				
			||||||
          end = true;
 | 
					          state.tokens.shift();
 | 
				
			||||||
          break;
 | 
					          break;
 | 
				
			||||||
        }
 | 
					        } else if (next === '$' && !escaped && quote !== "'" && stream.peek() != close) {
 | 
				
			||||||
        if (next === '$' && !escaped && quote !== "'") {
 | 
					 | 
				
			||||||
          escaped = true;
 | 
					          escaped = true;
 | 
				
			||||||
          stream.backUp(1);
 | 
					          stream.backUp(1);
 | 
				
			||||||
          state.tokens.unshift(tokenDollar);
 | 
					          state.tokens.unshift(tokenDollar);
 | 
				
			||||||
          break;
 | 
					          break;
 | 
				
			||||||
        }
 | 
					        } else if (!escaped && quote !== close && next === quote) {
 | 
				
			||||||
        if (!escaped && next === quote && quote !== close) {
 | 
					 | 
				
			||||||
          state.tokens.unshift(tokenString(quote, style))
 | 
					          state.tokens.unshift(tokenString(quote, style))
 | 
				
			||||||
          return tokenize(stream, state)
 | 
					          return tokenize(stream, state)
 | 
				
			||||||
 | 
					        } else if (!escaped && /['"]/.test(next) && !/['"]/.test(quote)) {
 | 
				
			||||||
 | 
					          state.tokens.unshift(tokenStringStart(next, "string"));
 | 
				
			||||||
 | 
					          stream.backUp(1);
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        escaped = !escaped && next === '\\';
 | 
					        escaped = !escaped && next === '\\';
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (end) state.tokens.shift();
 | 
					 | 
				
			||||||
      return style;
 | 
					      return style;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function tokenStringStart(quote, style) {
 | 
				
			||||||
 | 
					    return function(stream, state) {
 | 
				
			||||||
 | 
					      state.tokens[0] = tokenString(quote, style)
 | 
				
			||||||
 | 
					      stream.next()
 | 
				
			||||||
 | 
					      return tokenize(stream, state)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var tokenDollar = function(stream, state) {
 | 
					  var tokenDollar = function(stream, state) {
 | 
				
			||||||
    if (state.tokens.length > 1) stream.eat('$');
 | 
					    if (state.tokens.length > 1) stream.eat('$');
 | 
				
			||||||
    var ch = stream.next()
 | 
					    var ch = stream.next()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -61,4 +61,13 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  MT("nested braces",
 | 
					  MT("nested braces",
 | 
				
			||||||
     "[builtin echo] [def ${A[${B}]]}]")
 | 
					     "[builtin echo] [def ${A[${B}]]}]")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("strings in parens",
 | 
				
			||||||
 | 
					     "[def FOO][operator =]([quote $(<][string \"][def $MYDIR][string \"][quote /myfile grep ][string 'hello$'][quote )])")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT ("string ending in dollar",
 | 
				
			||||||
 | 
					     '[def a][operator =][string "xyz$"]; [def b][operator =][string "y"]')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT ("quote ending in dollar",
 | 
				
			||||||
 | 
					     "[quote $(echo a$)]")
 | 
				
			||||||
})();
 | 
					})();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,7 @@
 | 
				
			|||||||
      attributes: textMode,
 | 
					      attributes: textMode,
 | 
				
			||||||
      text: textMode,
 | 
					      text: textMode,
 | 
				
			||||||
      uri: textMode,
 | 
					      uri: textMode,
 | 
				
			||||||
 | 
					      trusted_resource_uri: textMode,
 | 
				
			||||||
      css: CodeMirror.getMode(config, "text/css"),
 | 
					      css: CodeMirror.getMode(config, "text/css"),
 | 
				
			||||||
      js: CodeMirror.getMode(config, {name: "text/javascript", statementIndent: 2 * config.indentUnit})
 | 
					      js: CodeMirror.getMode(config, {name: "text/javascript", statementIndent: 2 * config.indentUnit})
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
@@ -148,12 +149,14 @@
 | 
				
			|||||||
            return "string";
 | 
					            return "string";
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!state.soyState.length || last(state.soyState) != "literal") {
 | 
				
			||||||
          if (stream.match(/^\/\*/)) {
 | 
					          if (stream.match(/^\/\*/)) {
 | 
				
			||||||
            state.soyState.push("comment");
 | 
					            state.soyState.push("comment");
 | 
				
			||||||
            return "comment";
 | 
					            return "comment";
 | 
				
			||||||
        } else if (stream.match(stream.sol() || (state.soyState.length && last(state.soyState) != "literal") ? /^\s*\/\/.*/ : /^\s+\/\/.*/)) {
 | 
					          } else if (stream.match(stream.sol() ? /^\s*\/\/.*/ : /^\s+\/\/.*/)) {
 | 
				
			||||||
            return "comment";
 | 
					            return "comment";
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        switch (last(state.soyState)) {
 | 
					        switch (last(state.soyState)) {
 | 
				
			||||||
          case "templ-def":
 | 
					          case "templ-def":
 | 
				
			||||||
@@ -269,7 +272,7 @@
 | 
				
			|||||||
          return "keyword";
 | 
					          return "keyword";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // A tag-keyword must be followed by whitespace, comment or a closing tag.
 | 
					        // A tag-keyword must be followed by whitespace, comment or a closing tag.
 | 
				
			||||||
        } else if (match = stream.match(/^\{([\/@\\]?\w+\??)(?=[\s\}]|\/[/*])/)) {
 | 
					        } else if (match = stream.match(/^\{([/@\\]?\w+\??)(?=$|[\s}]|\/[/*])/)) {
 | 
				
			||||||
          if (match[1] != "/switch")
 | 
					          if (match[1] != "/switch")
 | 
				
			||||||
            state.indent += (/^(\/|(else|elseif|ifempty|case|fallbackmsg|default)$)/.test(match[1]) && state.tag != "switch" ? 1 : 2) * config.indentUnit;
 | 
					            state.indent += (/^(\/|(else|elseif|ifempty|case|fallbackmsg|default)$)/.test(match[1]) && state.tag != "switch" ? 1 : 2) * config.indentUnit;
 | 
				
			||||||
          state.tag = match[1];
 | 
					          state.tag = match[1];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -111,4 +111,11 @@
 | 
				
			|||||||
  MT('single-quote-strings',
 | 
					  MT('single-quote-strings',
 | 
				
			||||||
     '[keyword {][string "foo"] [string \'bar\'][keyword }]',
 | 
					     '[keyword {][string "foo"] [string \'bar\'][keyword }]',
 | 
				
			||||||
     '');
 | 
					     '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT('literal-comments',
 | 
				
			||||||
 | 
					     '[keyword {literal}]/* comment */ // comment[keyword {/literal}]');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT('highlight-command-at-eol',
 | 
				
			||||||
 | 
					     '[keyword {msg]',
 | 
				
			||||||
 | 
					     '    [keyword }]');
 | 
				
			||||||
})();
 | 
					})();
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										38
									
								
								src/public/libraries/codemirror/mode/sql/sql.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										38
									
								
								src/public/libraries/codemirror/mode/sql/sql.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -103,6 +103,12 @@
 | 
				
			|||||||
      var editor = CodeMirror.fromTextArea(document.getElementById("code"), {});
 | 
					      var editor = CodeMirror.fromTextArea(document.getElementById("code"), {});
 | 
				
			||||||
    </script>
 | 
					    </script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>sTeX mode supports this option:</p>
 | 
				
			||||||
 | 
					    <d1>
 | 
				
			||||||
 | 
					      <dt><code>inMathMode: boolean</code></dt>
 | 
				
			||||||
 | 
					      <dd>Whether to start parsing in math mode (default: <code>false</code>).</dd>
 | 
				
			||||||
 | 
					    </d1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <p><strong>MIME types defined:</strong> <code>text/x-stex</code>.</p>
 | 
					    <p><strong>MIME types defined:</strong> <code>text/x-stex</code>.</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <p><strong>Parsing/Highlighting Tests:</strong> <a href="../../test/index.html#stex_*">normal</a>,  <a href="../../test/index.html#verbose,stex_*">verbose</a>.</p>
 | 
					    <p><strong>Parsing/Highlighting Tests:</strong> <a href="../../test/index.html#stex_*">normal</a>,  <a href="../../test/index.html#verbose,stex_*">verbose</a>.</p>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@
 | 
				
			|||||||
})(function(CodeMirror) {
 | 
					})(function(CodeMirror) {
 | 
				
			||||||
  "use strict";
 | 
					  "use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  CodeMirror.defineMode("stex", function() {
 | 
					  CodeMirror.defineMode("stex", function(_config, parserConfig) {
 | 
				
			||||||
    "use strict";
 | 
					    "use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function pushCommand(state, command) {
 | 
					    function pushCommand(state, command) {
 | 
				
			||||||
@@ -78,6 +78,14 @@
 | 
				
			|||||||
    plugins["begin"] = addPluginPattern("begin", "tag", ["atom"]);
 | 
					    plugins["begin"] = addPluginPattern("begin", "tag", ["atom"]);
 | 
				
			||||||
    plugins["end"] = addPluginPattern("end", "tag", ["atom"]);
 | 
					    plugins["end"] = addPluginPattern("end", "tag", ["atom"]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    plugins["label"    ] = addPluginPattern("label"    , "tag", ["atom"]);
 | 
				
			||||||
 | 
					    plugins["ref"      ] = addPluginPattern("ref"      , "tag", ["atom"]);
 | 
				
			||||||
 | 
					    plugins["eqref"    ] = addPluginPattern("eqref"    , "tag", ["atom"]);
 | 
				
			||||||
 | 
					    plugins["cite"     ] = addPluginPattern("cite"     , "tag", ["atom"]);
 | 
				
			||||||
 | 
					    plugins["bibitem"  ] = addPluginPattern("bibitem"  , "tag", ["atom"]);
 | 
				
			||||||
 | 
					    plugins["Bibitem"  ] = addPluginPattern("Bibitem"  , "tag", ["atom"]);
 | 
				
			||||||
 | 
					    plugins["RBibitem" ] = addPluginPattern("RBibitem" , "tag", ["atom"]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    plugins["DEFAULT"] = function () {
 | 
					    plugins["DEFAULT"] = function () {
 | 
				
			||||||
      this.name = "DEFAULT";
 | 
					      this.name = "DEFAULT";
 | 
				
			||||||
      this.style = "tag";
 | 
					      this.style = "tag";
 | 
				
			||||||
@@ -117,6 +125,10 @@
 | 
				
			|||||||
        setState(state, function(source, state){ return inMathMode(source, state, "\\]"); });
 | 
					        setState(state, function(source, state){ return inMathMode(source, state, "\\]"); });
 | 
				
			||||||
        return "keyword";
 | 
					        return "keyword";
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      if (source.match("\\(")) {
 | 
				
			||||||
 | 
					        setState(state, function(source, state){ return inMathMode(source, state, "\\)"); });
 | 
				
			||||||
 | 
					        return "keyword";
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      if (source.match("$$")) {
 | 
					      if (source.match("$$")) {
 | 
				
			||||||
        setState(state, function(source, state){ return inMathMode(source, state, "$$"); });
 | 
					        setState(state, function(source, state){ return inMathMode(source, state, "$$"); });
 | 
				
			||||||
        return "keyword";
 | 
					        return "keyword";
 | 
				
			||||||
@@ -161,7 +173,7 @@
 | 
				
			|||||||
      if (source.eatSpace()) {
 | 
					      if (source.eatSpace()) {
 | 
				
			||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (source.match(endModeSeq)) {
 | 
					      if (endModeSeq && source.match(endModeSeq)) {
 | 
				
			||||||
        setState(state, normal);
 | 
					        setState(state, normal);
 | 
				
			||||||
        return "keyword";
 | 
					        return "keyword";
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -223,9 +235,10 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      startState: function() {
 | 
					      startState: function() {
 | 
				
			||||||
 | 
					        var f = parserConfig.inMathMode ? function(source, state){ return inMathMode(source, state); } : normal;
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
          cmdState: [],
 | 
					          cmdState: [],
 | 
				
			||||||
          f: normal
 | 
					          f: f
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      copyState: function(s) {
 | 
					      copyState: function(s) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -111,9 +111,18 @@
 | 
				
			|||||||
  MT("inlineMath",
 | 
					  MT("inlineMath",
 | 
				
			||||||
     "[keyword $][number 3][variable-2 x][tag ^][number 2.45]-[tag \\sqrt][bracket {][tag \\$\\alpha][bracket }] = [number 2][keyword $] other text");
 | 
					     "[keyword $][number 3][variable-2 x][tag ^][number 2.45]-[tag \\sqrt][bracket {][tag \\$\\alpha][bracket }] = [number 2][keyword $] other text");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("inlineMathLatexStyle",
 | 
				
			||||||
 | 
					     "[keyword \\(][number 3][variable-2 x][tag ^][number 2.45]-[tag \\sqrt][bracket {][tag \\$\\alpha][bracket }] = [number 2][keyword \\)] other text");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  MT("displayMath",
 | 
					  MT("displayMath",
 | 
				
			||||||
     "More [keyword $$]\t[variable-2 S][tag ^][variable-2 n][tag \\sum] [variable-2 i][keyword $$] other text");
 | 
					     "More [keyword $$]\t[variable-2 S][tag ^][variable-2 n][tag \\sum] [variable-2 i][keyword $$] other text");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("displayMath environment",
 | 
				
			||||||
 | 
					     "[tag \\begin][bracket {][atom equation][bracket }] x [tag \\end][bracket {][atom equation][bracket }] other text");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MT("displayMath environment with label",
 | 
				
			||||||
 | 
					     "[tag \\begin][bracket {][atom equation][bracket }][tag \\label][bracket {][atom eq1][bracket }] x [tag \\end][bracket {][atom equation][bracket }] other text~[tag \\ref][bracket {][atom eq1][bracket }]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  MT("mathWithComment",
 | 
					  MT("mathWithComment",
 | 
				
			||||||
     "[keyword $][variable-2 x] [comment % $]",
 | 
					     "[keyword $][variable-2 x] [comment % $]",
 | 
				
			||||||
     "[variable-2 y][keyword $] other text");
 | 
					     "[variable-2 y][keyword $] other text");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -76,7 +76,7 @@
 | 
				
			|||||||
      if (ch == "#") {
 | 
					      if (ch == "#") {
 | 
				
			||||||
        stream.next();
 | 
					        stream.next();
 | 
				
			||||||
        // Hex color
 | 
					        // Hex color
 | 
				
			||||||
        if (stream.match(/^[0-9a-f]{6}|[0-9a-f]{3}/i)) {
 | 
					        if (stream.match(/^[0-9a-f]{3}([0-9a-f]([0-9a-f]{2}){0,2})?\b/i)) {
 | 
				
			||||||
          return ["atom", "atom"];
 | 
					          return ["atom", "atom"];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        // ID selector
 | 
					        // ID selector
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -82,7 +82,7 @@ CodeMirror.defineMode("velocity", function() {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        // variable?
 | 
					        // variable?
 | 
				
			||||||
        else if (ch == "$") {
 | 
					        else if (ch == "$") {
 | 
				
			||||||
            stream.eatWhile(/[\w\d\$_\.{}]/);
 | 
					            stream.eatWhile(/[\w\d\$_\.{}-]/);
 | 
				
			||||||
            // is it one of the specials?
 | 
					            // is it one of the specials?
 | 
				
			||||||
            if (specials && specials.propertyIsEnumerable(stream.current())) {
 | 
					            if (specials && specials.propertyIsEnumerable(stream.current())) {
 | 
				
			||||||
                return "keyword";
 | 
					                return "keyword";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -163,8 +163,9 @@ CodeMirror.defineMode("xml", function(editorConf, config_) {
 | 
				
			|||||||
        stream.next();
 | 
					        stream.next();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return style;
 | 
					      return style;
 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function doctype(depth) {
 | 
					  function doctype(depth) {
 | 
				
			||||||
    return function(stream, state) {
 | 
					    return function(stream, state) {
 | 
				
			||||||
      var ch;
 | 
					      var ch;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -108,7 +108,8 @@ CodeMirror.defineMode("yaml", function() {
 | 
				
			|||||||
        literal: false,
 | 
					        literal: false,
 | 
				
			||||||
        escaped: false
 | 
					        escaped: false
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    lineComment: "#"
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@
 | 
				
			|||||||
    grid-template-areas: "header header"
 | 
					    grid-template-areas: "header header"
 | 
				
			||||||
                         "left-pane title"
 | 
					                         "left-pane title"
 | 
				
			||||||
                         "left-pane note-detail";
 | 
					                         "left-pane note-detail";
 | 
				
			||||||
    grid-template-columns: 29% 70%;
 | 
					    grid-template-columns: 29% 69.5%;
 | 
				
			||||||
    grid-template-rows: auto
 | 
					    grid-template-rows: auto
 | 
				
			||||||
                        auto
 | 
					                        auto
 | 
				
			||||||
                        1fr;
 | 
					                        1fr;
 | 
				
			||||||
@@ -52,8 +52,10 @@
 | 
				
			|||||||
    overflow: auto;
 | 
					    overflow: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#note-detail-component-wrapper.protected, #note-detail-component-wrapper.protected .CodeMirror {
 | 
					#note-detail-wrapper.protected {
 | 
				
			||||||
    background-color: #eee;
 | 
					    background: url('/images/shield.svg') no-repeat;
 | 
				
			||||||
 | 
					    background-size: contain;
 | 
				
			||||||
 | 
					    background-position: center;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#note-detail-text p {
 | 
					#note-detail-text p {
 | 
				
			||||||
@@ -286,6 +288,7 @@ div.ui-tooltip {
 | 
				
			|||||||
.CodeMirror {
 | 
					.CodeMirror {
 | 
				
			||||||
    font-family: "Liberation Mono", "Lucida Console", monospace;
 | 
					    font-family: "Liberation Mono", "Lucida Console", monospace;
 | 
				
			||||||
    height: auto;
 | 
					    height: auto;
 | 
				
			||||||
 | 
					    background: inherit;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.CodeMirror-scroll {
 | 
					.CodeMirror-scroll {
 | 
				
			||||||
@@ -443,3 +446,11 @@ html.theme-dark body {
 | 
				
			|||||||
    background: url('/images/icons/clock-16.png') no-repeat center;
 | 
					    background: url('/images/icons/clock-16.png') no-repeat center;
 | 
				
			||||||
    cursor: pointer;
 | 
					    cursor: pointer;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					table.promoted-attributes-in-tooltip {
 | 
				
			||||||
 | 
					    margin: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th {
 | 
				
			||||||
 | 
					    padding: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -5,11 +5,12 @@ const repository = require('../../services/repository');
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
async function getAutocomplete(req) {
 | 
					async function getAutocomplete(req) {
 | 
				
			||||||
    const query = req.query.query;
 | 
					    const query = req.query.query;
 | 
				
			||||||
 | 
					    const currentNoteId = req.query.currentNoteId || 'none';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let results;
 | 
					    let results;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (query.trim().length === 0) {
 | 
					    if (query.trim().length === 0) {
 | 
				
			||||||
        results = await getRecentNotes();
 | 
					        results = await getRecentNotes(currentNoteId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else {
 | 
					    else {
 | 
				
			||||||
        results = noteCacheService.findNotes(query);
 | 
					        results = noteCacheService.findNotes(query);
 | 
				
			||||||
@@ -23,7 +24,7 @@ async function getAutocomplete(req) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getRecentNotes() {
 | 
					async function getRecentNotes(currentNoteId) {
 | 
				
			||||||
    const recentNotes = await repository.getEntities(`
 | 
					    const recentNotes = await repository.getEntities(`
 | 
				
			||||||
      SELECT 
 | 
					      SELECT 
 | 
				
			||||||
        recent_notes.* 
 | 
					        recent_notes.* 
 | 
				
			||||||
@@ -33,9 +34,10 @@ async function getRecentNotes() {
 | 
				
			|||||||
      WHERE
 | 
					      WHERE
 | 
				
			||||||
        recent_notes.isDeleted = 0
 | 
					        recent_notes.isDeleted = 0
 | 
				
			||||||
        AND branches.isDeleted = 0
 | 
					        AND branches.isDeleted = 0
 | 
				
			||||||
 | 
					        AND branches.noteId != ?
 | 
				
			||||||
      ORDER BY 
 | 
					      ORDER BY 
 | 
				
			||||||
        dateCreated DESC
 | 
					        dateCreated DESC
 | 
				
			||||||
      LIMIT 200`);
 | 
					      LIMIT 200`, [currentNoteId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return recentNotes.map(rn => {
 | 
					    return recentNotes.map(rn => {
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -77,8 +77,25 @@ async function importTar(file, parentNoteId) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // maps from original noteId (in tar file) to newly generated noteId
 | 
					    // maps from original noteId (in tar file) to newly generated noteId
 | 
				
			||||||
    const noteIdMap = {};
 | 
					    const noteIdMap = {};
 | 
				
			||||||
 | 
					    const attributes = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await importNotes(files, parentNoteId, noteIdMap);
 | 
					    await importNotes(files, parentNoteId, noteIdMap, attributes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // we save attributes after importing notes because we need to have all the relation
 | 
				
			||||||
 | 
					    // targets already existing
 | 
				
			||||||
 | 
					    for (const attr of attributes) {
 | 
				
			||||||
 | 
					        if (attr.type === 'relation') {
 | 
				
			||||||
 | 
					            // map to local noteId
 | 
				
			||||||
 | 
					            attr.value = noteIdMap[attr.value];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!attr.value) {
 | 
				
			||||||
 | 
					                // relation is targeting note not present in the import
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await attributeService.createAttribute(attr);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getFileName(name) {
 | 
					function getFileName(name) {
 | 
				
			||||||
@@ -159,7 +176,7 @@ async function parseImportFile(file) {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function importNotes(files, parentNoteId, noteIdMap) {
 | 
					async function importNotes(files, parentNoteId, noteIdMap, attributes) {
 | 
				
			||||||
    for (const file of files) {
 | 
					    for (const file of files) {
 | 
				
			||||||
        if (file.meta.version !== 1) {
 | 
					        if (file.meta.version !== 1) {
 | 
				
			||||||
            throw new Error("Can't read meta data version " + file.meta.version);
 | 
					            throw new Error("Can't read meta data version " + file.meta.version);
 | 
				
			||||||
@@ -188,7 +205,7 @@ async function importNotes(files, parentNoteId, noteIdMap) {
 | 
				
			|||||||
        noteIdMap[file.meta.noteId] = note.noteId;
 | 
					        noteIdMap[file.meta.noteId] = note.noteId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const attribute of file.meta.attributes) {
 | 
					        for (const attribute of file.meta.attributes) {
 | 
				
			||||||
            await attributeService.createAttribute({
 | 
					            attributes.push({
 | 
				
			||||||
                noteId: note.noteId,
 | 
					                noteId: note.noteId,
 | 
				
			||||||
                type: attribute.type,
 | 
					                type: attribute.type,
 | 
				
			||||||
                name: attribute.name,
 | 
					                name: attribute.name,
 | 
				
			||||||
@@ -199,7 +216,7 @@ async function importNotes(files, parentNoteId, noteIdMap) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (file.children.length > 0) {
 | 
					        if (file.children.length > 0) {
 | 
				
			||||||
            await importNotes(file.children, note.noteId, noteIdMap);
 | 
					            await importNotes(file.children, note.noteId, noteIdMap, attributes);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,10 +5,15 @@ const attributeService = require('../../services/attributes');
 | 
				
			|||||||
const repository = require('../../services/repository');
 | 
					const repository = require('../../services/repository');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function exec(req) {
 | 
					async function exec(req) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
        const result = await scriptService.executeScript(req.body.script, req.body.params, req.body.startNoteId,
 | 
					        const result = await scriptService.executeScript(req.body.script, req.body.params, req.body.startNoteId,
 | 
				
			||||||
            req.body.currentNoteId, req.body.originEntityName, req.body.originEntityId);
 | 
					            req.body.currentNoteId, req.body.originEntityName, req.body.originEntityId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return { executionResult: result };
 | 
					        return { success: true, executionResult: result };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    catch (e) {
 | 
				
			||||||
 | 
					        return { success: false, error: e.message };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function run(req) {
 | 
					async function run(req) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,7 +38,7 @@ async function searchNotes(req) {
 | 
				
			|||||||
    let results;
 | 
					    let results;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (labelFiltersNoteIds && searchTextResults) {
 | 
					    if (labelFiltersNoteIds && searchTextResults) {
 | 
				
			||||||
        results = labelFiltersNoteIds.filter(item => searchTextResults.includes(item.noteId));
 | 
					        results = searchTextResults.filter(item => labelFiltersNoteIds.includes(item.noteId));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else if (labelFiltersNoteIds) {
 | 
					    else if (labelFiltersNoteIds) {
 | 
				
			||||||
        results = labelFiltersNoteIds.map(noteCacheService.getNotePath).filter(res => !!res);
 | 
					        results = labelFiltersNoteIds.map(noteCacheService.getNotePath).filter(res => !!res);
 | 
				
			||||||
@@ -64,6 +64,7 @@ async function getFullTextResults(searchText) {
 | 
				
			|||||||
      FROM notes 
 | 
					      FROM notes 
 | 
				
			||||||
      WHERE isDeleted = 0 
 | 
					      WHERE isDeleted = 0 
 | 
				
			||||||
        AND isProtected = 0
 | 
					        AND isProtected = 0
 | 
				
			||||||
 | 
					        AND type IN ('text', 'code')
 | 
				
			||||||
        AND ${tokenSql.join(' AND ')}`);
 | 
					        AND ${tokenSql.join(' AND ')}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return noteIds;
 | 
					    return noteIds;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,7 +64,7 @@ async function getTree() {
 | 
				
			|||||||
    const relations = await getRelations(noteIds);
 | 
					    const relations = await getRelations(noteIds);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
        startNotePath: await optionService.getOption('startNotePath'),
 | 
					        startNotePath: (await optionService.getOption('startNotePath')) || 'root',
 | 
				
			||||||
        branches,
 | 
					        branches,
 | 
				
			||||||
        notes,
 | 
					        notes,
 | 
				
			||||||
        relations
 | 
					        relations
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -176,7 +176,8 @@ function register(app) {
 | 
				
			|||||||
    apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize);
 | 
					    apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    apiRoute(POST, '/api/cleanup/cleanup-unused-images', cleanupRoute.cleanupUnusedImages);
 | 
					    apiRoute(POST, '/api/cleanup/cleanup-unused-images', cleanupRoute.cleanupUnusedImages);
 | 
				
			||||||
    apiRoute(POST, '/api/cleanup/vacuum-database', cleanupRoute.vacuumDatabase);
 | 
					    // VACUUM requires execution outside of transaction
 | 
				
			||||||
 | 
					    route(POST, '/api/cleanup/vacuum-database', [auth.checkApiAuthOrElectron], cleanupRoute.vacuumDatabase, apiResultHandler, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    apiRoute(POST, '/api/script/exec', scriptRoute.exec);
 | 
					    apiRoute(POST, '/api/script/exec', scriptRoute.exec);
 | 
				
			||||||
    apiRoute(POST, '/api/script/run/:noteId', scriptRoute.run);
 | 
					    apiRoute(POST, '/api/script/run/:noteId', scriptRoute.run);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
const build = require('./build');
 | 
					const build = require('./build');
 | 
				
			||||||
const packageJson = require('../../package');
 | 
					const packageJson = require('../../package');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const APP_DB_VERSION = 111;
 | 
					const APP_DB_VERSION = 112;
 | 
				
			||||||
const SYNC_VERSION = 1;
 | 
					const SYNC_VERSION = 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,24 +19,26 @@ const BUILTIN_ATTRIBUTES = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // relation names
 | 
					    // relation names
 | 
				
			||||||
    { type: 'relation', name: 'runOnNoteView' },
 | 
					    { type: 'relation', name: 'runOnNoteView' },
 | 
				
			||||||
 | 
					    { type: 'relation', name: 'runOnNoteCreation' },
 | 
				
			||||||
    { type: 'relation', name: 'runOnNoteTitleChange' },
 | 
					    { type: 'relation', name: 'runOnNoteTitleChange' },
 | 
				
			||||||
 | 
					    { type: 'relation', name: 'runOnNoteChange' },
 | 
				
			||||||
 | 
					    { type: 'relation', name: 'runOnChildNoteCreation' },
 | 
				
			||||||
 | 
					    { type: 'relation', name: 'runOnAttributeCreation' },
 | 
				
			||||||
    { type: 'relation', name: 'runOnAttributeChange' },
 | 
					    { type: 'relation', name: 'runOnAttributeChange' },
 | 
				
			||||||
    { type: 'relation', name: 'inheritAttributes' }
 | 
					    { type: 'relation', name: 'template' }
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getNotesWithLabel(name, value) {
 | 
					async function getNotesWithLabel(name, value) {
 | 
				
			||||||
    let notes;
 | 
					    let valueCondition = "";
 | 
				
			||||||
 | 
					    let params = [name];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (value !== undefined) {
 | 
					    if (value !== undefined) {
 | 
				
			||||||
        notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId) 
 | 
					        valueCondition = " AND attributes.value = ?";
 | 
				
			||||||
          WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]);
 | 
					        params.push(value);
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else {
 | 
					 | 
				
			||||||
        notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId) 
 | 
					 | 
				
			||||||
          WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ?`, [name]);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return notes;
 | 
					    return await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId) 
 | 
				
			||||||
 | 
					          WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? ${valueCondition} ORDER BY position`, params);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getNoteWithLabel(name, value) {
 | 
					async function getNoteWithLabel(name, value) {
 | 
				
			||||||
@@ -59,16 +61,18 @@ async function createAttribute(attribute) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getAttributeNames(type, nameLike) {
 | 
					async function getAttributeNames(type, nameLike) {
 | 
				
			||||||
 | 
					    nameLike = nameLike.toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const names = await sql.getColumn(
 | 
					    const names = await sql.getColumn(
 | 
				
			||||||
        `SELECT DISTINCT name 
 | 
					        `SELECT DISTINCT name 
 | 
				
			||||||
             FROM attributes 
 | 
					             FROM attributes 
 | 
				
			||||||
             WHERE isDeleted = 0
 | 
					             WHERE isDeleted = 0
 | 
				
			||||||
               AND type = ?
 | 
					               AND type = ?
 | 
				
			||||||
           AND name LIKE '%${utils.sanitizeSql(nameLike)}%'`, [ type ]);
 | 
					               AND name LIKE '%${utils.sanitizeSql(nameLike)}%'`, [type]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const attribute of BUILTIN_ATTRIBUTES) {
 | 
					    for (const attr of BUILTIN_ATTRIBUTES) {
 | 
				
			||||||
        if (attribute.type === type && !names.includes(attribute.name)) {
 | 
					        if (attr.type === type && attr.name.toLowerCase().includes(nameLike) && !names.includes(attr.name)) {
 | 
				
			||||||
            names.push(attribute.name);
 | 
					            names.push(attr.name);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										237
									
								
								src/services/backend_script_api.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								src/services/backend_script_api.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,237 @@
 | 
				
			|||||||
 | 
					const log = require('./log');
 | 
				
			||||||
 | 
					const noteService = require('./notes');
 | 
				
			||||||
 | 
					const sql = require('./sql');
 | 
				
			||||||
 | 
					const utils = require('./utils');
 | 
				
			||||||
 | 
					const dateUtils = require('./date_utils');
 | 
				
			||||||
 | 
					const attributeService = require('./attributes');
 | 
				
			||||||
 | 
					const dateNoteService = require('./date_notes');
 | 
				
			||||||
 | 
					const treeService = require('./tree');
 | 
				
			||||||
 | 
					const config = require('./config');
 | 
				
			||||||
 | 
					const repository = require('./repository');
 | 
				
			||||||
 | 
					const axios = require('axios');
 | 
				
			||||||
 | 
					const cloningService = require('./cloning');
 | 
				
			||||||
 | 
					const messagingService = require('./messaging');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This is the main backend API interface for scripts. It's published in the local "api" object.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @constructor
 | 
				
			||||||
 | 
					 * @hideconstructor
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function BackendScriptApi(startNote, currentNote, originEntity) {
 | 
				
			||||||
 | 
					    /** @property {Note} note where script started executing */
 | 
				
			||||||
 | 
					    this.startNote = startNote;
 | 
				
			||||||
 | 
					    /** @property {Note} note where script is currently executing */
 | 
				
			||||||
 | 
					    this.currentNote = currentNote;
 | 
				
			||||||
 | 
					    /** @property {Entity} entity whose event triggered this executions */
 | 
				
			||||||
 | 
					    this.originEntity = originEntity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.axios = axios;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.utils = {
 | 
				
			||||||
 | 
					        unescapeHtml: utils.unescapeHtml,
 | 
				
			||||||
 | 
					        isoDateTimeStr: dateUtils.dateStr,
 | 
				
			||||||
 | 
					        isoDateStr: date => dateUtils.dateStr(date).substr(0, 10)
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Instance name identifies particular Trilium instance. It can be useful for scripts
 | 
				
			||||||
 | 
					     * if some action needs to happen on only one specific instance.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns {string|null}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.getInstanceName = () => config.General ? config.General.instanceName : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} noteId
 | 
				
			||||||
 | 
					     * @returns {Promise<Note|null>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.getNote = repository.getNote;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} branchId
 | 
				
			||||||
 | 
					     * @returns {Promise<Branch|null>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.getBranch = repository.getBranch;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} attributeId
 | 
				
			||||||
 | 
					     * @returns {Promise<Attribute|null>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.getAttribute = repository.getAttribute;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} imageId
 | 
				
			||||||
 | 
					     * @returns {Promise<Image|null>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.getImage = repository.getImage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Retrieves first entity from the SQL's result set.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} SQL query
 | 
				
			||||||
 | 
					     * @param {Array.<?>} array of params
 | 
				
			||||||
 | 
					     * @returns {Promise<Entity|null>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.getEntity = repository.getEntity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} SQL query
 | 
				
			||||||
 | 
					     * @param {Array.<?>} array of params
 | 
				
			||||||
 | 
					     * @returns {Promise<Entity[]>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.getEntities = repository.getEntities;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Retrieves notes with given label name & value
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} name - attribute name
 | 
				
			||||||
 | 
					     * @param {string} [value] - attribute value
 | 
				
			||||||
 | 
					     * @returns {Promise<Note[]>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.getNotesWithLabel = attributeService.getNotesWithLabel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Retrieves first note with given label name & value
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} name - attribute name
 | 
				
			||||||
 | 
					     * @param {string} [value] - attribute value
 | 
				
			||||||
 | 
					     * @returns {Promise<Note|null>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.getNoteWithLabel = attributeService.getNoteWithLabel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * If there's no branch between note and parent note, create one. Otherwise do nothing.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} noteId
 | 
				
			||||||
 | 
					     * @param {string} parentNoteId
 | 
				
			||||||
 | 
					     * @param {string} prefix - if branch will be create between note and parent note, set this prefix
 | 
				
			||||||
 | 
					     * @returns {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.ensureNoteIsPresentInParent = cloningService.ensureNoteIsPresentInParent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * If there's a branch between note and parent note, remove it. Otherwise do nothing.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} noteId
 | 
				
			||||||
 | 
					     * @param {string} parentNoteId
 | 
				
			||||||
 | 
					     * @returns {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.ensureNoteIsAbsentFromParent = cloningService.ensureNoteIsAbsentFromParent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Based on the value, either create or remove branch between note and parent note.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {boolean} present - true if we want the branch to exist, false if we want it gone
 | 
				
			||||||
 | 
					     * @param {string} noteId
 | 
				
			||||||
 | 
					     * @param {string} parentNoteId
 | 
				
			||||||
 | 
					     * @param {string} prefix - if branch will be create between note and parent note, set this prefix
 | 
				
			||||||
 | 
					     * @returns {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.toggleNoteInParent = cloningService.toggleNoteInParent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @typedef {object} CreateNoteAttribute
 | 
				
			||||||
 | 
					     * @property {string} type - attribute type - label, relation etc.
 | 
				
			||||||
 | 
					     * @property {string} name - attribute name
 | 
				
			||||||
 | 
					     * @property {string} [value] - attribute value
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @typedef {object} CreateNoteExtraOptions
 | 
				
			||||||
 | 
					     * @property {boolean} [json=false] - should the note be JSON
 | 
				
			||||||
 | 
					     * @property {boolean} [isProtected=false] - should the note be protected
 | 
				
			||||||
 | 
					     * @property {string} [type='text'] - note type
 | 
				
			||||||
 | 
					     * @property {string} [mime='text/html'] - MIME type of the note
 | 
				
			||||||
 | 
					     * @property {CreateNoteAttribute[]} [attributes=[]] - attributes to be created for this note
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param {string} parentNoteId - create new note under this parent
 | 
				
			||||||
 | 
					     * @param {string} title
 | 
				
			||||||
 | 
					     * @param {string} [content=""]
 | 
				
			||||||
 | 
					     * @param {CreateNoteExtraOptions} [extraOptions={}]
 | 
				
			||||||
 | 
					     * @returns {Promise<{note: Note, branch: Branch}>} object contains newly created entities note and branch
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.createNote = noteService.createNote;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Log given message to trilium logs.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param message
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.log = message => log.info(`Script "${currentNote.title}" (${currentNote.noteId}): ${message}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns root note of the calendar.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @returns {Promise<Note|null>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.getRootCalendarNote = dateNoteService.getRootCalendarNote;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns day note for given date (YYYY-MM-DD format). If such note doesn't exist, it is created.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} date
 | 
				
			||||||
 | 
					     * @returns {Promise<Note|null>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.getDateNote = dateNoteService.getDateNote;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} parentNoteId - this note's child notes will be sorted
 | 
				
			||||||
 | 
					     * @returns Promise<void>
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.sortNotesAlphabetically = treeService.sortNotesAlphabetically;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * This method finds note by its noteId and prefix and either sets it to the given parentNoteId
 | 
				
			||||||
 | 
					     * or removes the branch (if parentNoteId is not given).
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * This method looks similar to toggleNoteInParent() but differs because we're looking up branch by prefix.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {string} noteId
 | 
				
			||||||
 | 
					     * @param {string} prefix
 | 
				
			||||||
 | 
					     * @param {string} [parentNoteId]
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.setNoteToParent = treeService.setNoteToParent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * This functions wraps code which is supposed to be running in transaction. If transaction already
 | 
				
			||||||
 | 
					     * exists, then we'll use that transaction.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * This method is required only when script has label manualTransactionHandling, all other scripts are
 | 
				
			||||||
 | 
					     * transactional by default.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @method
 | 
				
			||||||
 | 
					     * @param {function} func
 | 
				
			||||||
 | 
					     * @returns {Promise<?>} result of func callback
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.transactional = sql.transactional;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Trigger tree refresh in all connected clients. This is required when some tree change happens in
 | 
				
			||||||
 | 
					     * the backend.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns {Promise<void>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    this.refreshTree = () => messagingService.sendMessageToAllClients({ type: 'refresh-tree' });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = BackendScriptApi;
 | 
				
			||||||
@@ -1 +1 @@
 | 
				
			|||||||
module.exports = { buildDate:"2018-08-14T14:19:37+02:00", buildRevision: "fec157444787ad3dbe01a5052cb01e5374bdcb79" };
 | 
					module.exports = { buildDate:"2018-08-27T18:59:54+02:00", buildRevision: "4bc44605fbbbc1a975456db229bcd5557b20d045" };
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,7 +68,7 @@ async function runSyncRowChecks(table, key, errorList) {
 | 
				
			|||||||
          ${table} 
 | 
					          ${table} 
 | 
				
			||||||
          LEFT JOIN sync ON sync.entityName = '${table}' AND entityId = ${key} 
 | 
					          LEFT JOIN sync ON sync.entityName = '${table}' AND entityId = ${key} 
 | 
				
			||||||
        WHERE 
 | 
					        WHERE 
 | 
				
			||||||
          sync.id IS NULL`,
 | 
					          sync.id IS NULL AND ` + (table === 'options' ? 'isSynced = 1' : '1'),
 | 
				
			||||||
        `Missing sync records for ${key} in table ${table}`, errorList);
 | 
					        `Missing sync records for ${key} in table ${table}`, errorList);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await runCheck(`
 | 
					    await runCheck(`
 | 
				
			||||||
@@ -224,6 +224,7 @@ async function runAllChecks() {
 | 
				
			|||||||
    await runSyncRowChecks("note_images", "noteImageId", errorList);
 | 
					    await runSyncRowChecks("note_images", "noteImageId", errorList);
 | 
				
			||||||
    await runSyncRowChecks("attributes", "attributeId", errorList);
 | 
					    await runSyncRowChecks("attributes", "attributeId", errorList);
 | 
				
			||||||
    await runSyncRowChecks("api_tokens", "apiTokenId", errorList);
 | 
					    await runSyncRowChecks("api_tokens", "apiTokenId", errorList);
 | 
				
			||||||
 | 
					    await runSyncRowChecks("options", "name", errorList);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (errorList.length === 0) {
 | 
					    if (errorList.length === 0) {
 | 
				
			||||||
        // we run this only if basic checks passed since this assumes basic data consistency
 | 
					        // we run this only if basic checks passed since this assumes basic data consistency
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,7 @@ const Option = require('../entities/option');
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
async function getHash(entityConstructor, whereBranch) {
 | 
					async function getHash(entityConstructor, whereBranch) {
 | 
				
			||||||
    // subselect is necessary to have correct ordering in GROUP_CONCAT
 | 
					    // subselect is necessary to have correct ordering in GROUP_CONCAT
 | 
				
			||||||
    const query = `SELECT GROUP_CONCAT(hash) FROM (SELECT hash FROM ${entityConstructor.tableName} `
 | 
					    const query = `SELECT GROUP_CONCAT(hash) FROM (SELECT hash FROM ${entityConstructor.entityName} `
 | 
				
			||||||
        + (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${entityConstructor.primaryKeyName})`;
 | 
					        + (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${entityConstructor.primaryKeyName})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let contentToHash = await sql.getValue(query);
 | 
					    let contentToHash = await sql.getValue(query);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,9 @@ const log = require('./log');
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const NOTE_TITLE_CHANGED = "NOTE_TITLE_CHANGED";
 | 
					const NOTE_TITLE_CHANGED = "NOTE_TITLE_CHANGED";
 | 
				
			||||||
const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION";
 | 
					const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION";
 | 
				
			||||||
 | 
					const ENTITY_CREATED = "ENTITY_CREATED";
 | 
				
			||||||
const ENTITY_CHANGED = "ENTITY_CHANGED";
 | 
					const ENTITY_CHANGED = "ENTITY_CHANGED";
 | 
				
			||||||
 | 
					const CHILD_NOTE_CREATED = "CHILD_NOTE_CREATED";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const eventListeners = {};
 | 
					const eventListeners = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -33,5 +35,7 @@ module.exports = {
 | 
				
			|||||||
    // event types:
 | 
					    // event types:
 | 
				
			||||||
    NOTE_TITLE_CHANGED,
 | 
					    NOTE_TITLE_CHANGED,
 | 
				
			||||||
    ENTER_PROTECTED_SESSION,
 | 
					    ENTER_PROTECTED_SESSION,
 | 
				
			||||||
    ENTITY_CHANGED
 | 
					    ENTITY_CREATED,
 | 
				
			||||||
 | 
					    ENTITY_CHANGED,
 | 
				
			||||||
 | 
					    CHILD_NOTE_CREATED
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -2,17 +2,21 @@ const eventService = require('./events');
 | 
				
			|||||||
const scriptService = require('./script');
 | 
					const scriptService = require('./script');
 | 
				
			||||||
const treeService = require('./tree');
 | 
					const treeService = require('./tree');
 | 
				
			||||||
const messagingService = require('./messaging');
 | 
					const messagingService = require('./messaging');
 | 
				
			||||||
const repository = require('./repository');
 | 
					const log = require('./log');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function runAttachedRelations(note, relationName, originEntity) {
 | 
					async function runAttachedRelations(note, relationName, originEntity) {
 | 
				
			||||||
    const attributes = await note.getAttributes();
 | 
					    const runRelations = (await note.getRelations()).filter(relation => relation.name === relationName);
 | 
				
			||||||
    const runRelations = attributes.filter(relation => relation.type === 'relation' && relation.name === relationName);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const relation of runRelations) {
 | 
					    for (const relation of runRelations) {
 | 
				
			||||||
        const scriptNote = await relation.getTargetNote();
 | 
					        const scriptNote = await relation.getTargetNote();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (scriptNote) {
 | 
				
			||||||
            await scriptService.executeNote(scriptNote, originEntity);
 | 
					            await scriptService.executeNote(scriptNote, originEntity);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        else {
 | 
				
			||||||
 | 
					            log.error(`Target note ${relation.value} of atttribute ${relation.attributeId} has not been found.`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
eventService.subscribe(eventService.NOTE_TITLE_CHANGED, async note => {
 | 
					eventService.subscribe(eventService.NOTE_TITLE_CHANGED, async note => {
 | 
				
			||||||
@@ -31,10 +35,24 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, async note => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityId, entityName }) => {
 | 
					eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity }) => {
 | 
				
			||||||
    if (entityName === 'attributes') {
 | 
					    if (entityName === 'attributes') {
 | 
				
			||||||
        const attribute = await repository.getEntityFromName(entityName, entityId);
 | 
					        await runAttachedRelations(await entity.getNote(), 'runOnAttributeChange', entity);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
        await runAttachedRelations(await attribute.getNote(), 'runOnAttributeChange', attribute);
 | 
					    else if (entityName === 'notes') {
 | 
				
			||||||
 | 
					        await runAttachedRelations(entity, 'runOnNoteChange', entity);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					eventService.subscribe(eventService.ENTITY_CREATED, async ({ entityName, entity }) => {
 | 
				
			||||||
 | 
					    if (entityName === 'attributes') {
 | 
				
			||||||
 | 
					        await runAttachedRelations(await entity.getNote(), 'runOnAttributeCreation', entity);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else if (entityName === 'notes') {
 | 
				
			||||||
 | 
					        await runAttachedRelations(entity, 'runOnNoteCreation', entity);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					eventService.subscribe(eventService.CHILD_NOTE_CREATED, async ({ parentNote, childNote }) => {
 | 
				
			||||||
 | 
					    await runAttachedRelations(parentNote, 'runOnChildNoteCreation', childNote);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -50,6 +50,12 @@ function findNotes(query) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const noteId of noteIds) {
 | 
					    for (const noteId of noteIds) {
 | 
				
			||||||
 | 
					        // autocomplete should be able to find notes by their noteIds as well (only leafs)
 | 
				
			||||||
 | 
					        if (noteId === query) {
 | 
				
			||||||
 | 
					            search(noteId, [], [], results);
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // for leaf note it doesn't matter if "archived" label inheritable or not
 | 
					        // for leaf note it doesn't matter if "archived" label inheritable or not
 | 
				
			||||||
        if (noteId in archived) {
 | 
					        if (noteId in archived) {
 | 
				
			||||||
            continue;
 | 
					            continue;
 | 
				
			||||||
@@ -228,13 +234,13 @@ function getNotePath(noteId) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId}) => {
 | 
					eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entity}) => {
 | 
				
			||||||
    if (!loaded) {
 | 
					    if (!loaded) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (entityName === 'notes') {
 | 
					    if (entityName === 'notes') {
 | 
				
			||||||
        const note = await repository.getNote(entityId);
 | 
					        const note = entity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (note.isDeleted) {
 | 
					        if (note.isDeleted) {
 | 
				
			||||||
            delete noteTitles[note.noteId];
 | 
					            delete noteTitles[note.noteId];
 | 
				
			||||||
@@ -245,7 +251,7 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else if (entityName === 'branches') {
 | 
					    else if (entityName === 'branches') {
 | 
				
			||||||
        const branch = await repository.getBranch(entityId);
 | 
					        const branch = entity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (childToParent[branch.noteId]) {
 | 
					        if (childToParent[branch.noteId]) {
 | 
				
			||||||
            childToParent[branch.noteId] = childToParent[branch.noteId].filter(noteId => noteId !== branch.parentNoteId)
 | 
					            childToParent[branch.noteId] = childToParent[branch.noteId].filter(noteId => noteId !== branch.parentNoteId)
 | 
				
			||||||
@@ -266,7 +272,7 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else if (entityName === 'attributes') {
 | 
					    else if (entityName === 'attributes') {
 | 
				
			||||||
        const attribute = await repository.getAttribute(entityId);
 | 
					        const attribute = entity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (attribute.type === 'label' && attribute.name === 'archived') {
 | 
					        if (attribute.type === 'label' && attribute.name === 'archived') {
 | 
				
			||||||
            // we're not using label object directly, since there might be other non-deleted archived label
 | 
					            // we're not using label object directly, since there might be other non-deleted archived label
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user