Compare commits

..

49 Commits

Author SHA1 Message Date
azivner
2d1bc46c04 release 0.20.0 2018-08-27 18:59:54 +02:00
azivner
4bc44605fb don't short child: promoted attributes 2018-08-27 13:35:45 +02:00
azivner
b868990fba using exact versions of packages from now on 2018-08-23 23:42:47 +02:00
azivner
26c06c9826 more API docs 2018-08-23 15:33:19 +02:00
azivner
f5b89432a6 frontend script API documentation 2018-08-23 12:55:45 +02:00
azivner
0e7372adbf backend script API documentation 2018-08-23 10:10:04 +02:00
azivner
d4fbe28517 jsdoc comments on entities 2018-08-22 23:37:06 +02:00
azivner
668528d5eb promoted attributes are visible in tooltip preview, fixes #158 2018-08-22 15:31:36 +02:00
azivner
17348a9cfe fix some issues 2018-08-22 14:40:49 +02:00
azivner
09b610701d removed not unique warning from attributes dialog as it's more common to have multi value 2018-08-21 13:51:35 +02:00
azivner
71e687ad8e child: prefix now copies attributes on new note creation 2018-08-21 13:49:45 +02:00
azivner
171877ce08 renamed inheritAttributes to template 2018-08-21 12:52:11 +02:00
azivner
4f1e6ec70f note API additions 2018-08-21 12:50:43 +02:00
azivner
1938c317c3 fix relation definition in dialog 2018-08-20 10:04:26 +02:00
azivner
99d81059d0 better common JS compatibility 2018-08-19 22:28:32 +02:00
azivner
59d5a86110 fix attachment attributes 2018-08-19 21:42:03 +02:00
azivner
a5e56ea839 display a message if there's no recent change 2018-08-18 15:21:44 +02:00
azivner
44f85224e7 added new label type URL with open button, fixes #156 2018-08-18 15:00:52 +02:00
azivner
0aa08b1c1e relation promoted attribute has clickable button, fixes #155 2018-08-18 14:55:27 +02:00
azivner
406d74c4d7 initial focus on attribute type instead of name 2018-08-18 14:49:25 +02:00
azivner
7f9a8a55ca fix init of synced options in new database 2018-08-17 18:11:03 +02:00
azivner
a42bbba0e5 unprotecting note outside of protected session is not forbidden because it could overwrite previous note 2018-08-17 15:21:59 +02:00
azivner
145efe67c3 better logging and notifications on script errors for easier debugging 2018-08-17 11:31:42 +02:00
azivner
513748836e note autocomplete and full text search should be able to find notes by noteId 2018-08-17 10:06:52 +02:00
azivner
427ce3972e protected notes detail is now marked with shield background instead of just grey background 2018-08-17 09:32:07 +02:00
azivner
02c0f9a6cd shrinkable note title so the design crumbles with smaller width 2018-08-16 23:16:17 +02:00
azivner
208771216e fix in passing originEntity from frontend to backend, some refactorings 2018-08-16 23:00:04 +02:00
azivner
385d97a9b3 recent notes now don't display current note, unification of autocomplete source handling 2018-08-16 21:02:42 +02:00
azivner
e39d1d08ac easier API to add button to toolbar 2018-08-16 20:26:40 +02:00
azivner
0f106fb96f more relation events, events are now not triggered on sync changes 2018-08-15 22:06:49 +02:00
azivner
df9acd0504 relation target noteIds need to be translated into local noteIds 2018-08-15 18:32:06 +02:00
azivner
dbe0eb3f3a fix attribute name autocomplete, no tooltip preview on path selection 2018-08-15 18:22:02 +02:00
azivner
4513651e12 delete attributes when deleting note 2018-08-15 15:27:22 +02:00
azivner
3204291463 update codemirror to 5.39.2 2018-08-15 11:25:30 +02:00
azivner
510704a074 help buttons and existing custom HTML attribute refactoring to data-* 2018-08-15 10:14:14 +02:00
azivner
f440493e45 use ISO dateformat on the frontend instead of european formatting 2018-08-15 08:48:16 +02:00
azivner
b897c6de13 fix note revision saving 2018-08-15 08:44:54 +02:00
azivner
acbd18e8fc links to documentation for attributes, links and search + fix for opening external links 2018-08-14 23:07:50 +02:00
azivner
ff5b84db10 search (note) fixes 2018-08-14 22:50:05 +02:00
azivner
16535f6a73 small changes to attribute dialog 2018-08-14 21:02:42 +02:00
azivner
5b657ad961 minor package upgrades 2018-08-14 20:25:37 +02:00
azivner
bbbc3e9dc4 one more fix for release of pkg 2018-08-14 19:52:17 +02:00
azivner
f43f0e10a1 release 0.19.1 2018-08-14 18:06:31 +02:00
azivner
6d842a65a2 fix DB vacuum, fixes #154 2018-08-14 18:03:36 +02:00
azivner
50c4de021c fix for mysterious 404 not found notes like "appearance" 2018-08-14 18:00:11 +02:00
azivner
936d8449f6 fix & unify "show recent notes" buttons 2018-08-14 17:36:39 +02:00
azivner
462bc0edd5 attribute sync fix 2018-08-14 17:32:15 +02:00
azivner
35ef3c8470 fix migration 2018-08-14 16:09:30 +02:00
azivner
5117d43e29 fix pkg upload to github 2018-08-14 15:32:12 +02:00
111 changed files with 3923 additions and 2533 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ yarn-error.log
config.ini config.ini
cert.key cert.key
cert.crt cert.crt
docs/

View File

@@ -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"

View File

@@ -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=

View File

@@ -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;

View File

@@ -0,0 +1 @@
UPDATE attributes SET name = 'template' WHERE name = 'inheritAttributes';

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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": {

View File

@@ -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"]; }

View File

@@ -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"]; }

View File

@@ -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"]; }

View File

@@ -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];

View File

@@ -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);

View File

@@ -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"]; }

View File

@@ -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.*

View File

@@ -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"]; }

View File

@@ -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"]; }

View File

@@ -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"]; }

View File

@@ -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"]; }

View 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

View File

@@ -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) {

View File

@@ -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
}); });
} }

View File

@@ -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();

View File

@@ -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');
} }

View File

@@ -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);

View File

@@ -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;

View File

@@ -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';
} }

View File

@@ -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) {}

View File

@@ -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());
} }

View File

@@ -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();

View File

@@ -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() {

View File

@@ -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 {

View 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;

View File

@@ -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
} }

View File

@@ -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);

View File

@@ -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") + '&currentNoteId=' + 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
} }

View File

@@ -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);

View File

@@ -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);
} }

View File

@@ -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();
} }

View File

@@ -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;

View File

@@ -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;

View File

@@ -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));

View File

@@ -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 => {

View File

@@ -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
} }

View File

@@ -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,

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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
} }
} }
}); });

View File

@@ -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"});
} }

View File

@@ -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;

View File

@@ -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: '"'}}

View File

@@ -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
{ {

View 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 ;]");
})();

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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>

View File

@@ -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")

View File

@@ -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}

View File

@@ -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

View File

@@ -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");
}); });

View File

@@ -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>");

View File

@@ -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';
} }

View File

@@ -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"]},

View File

@@ -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
}); });

View File

@@ -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>

View File

@@ -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},

View File

@@ -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 = /[+\-*&%=<>!?|\/]/;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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']");
})(); })();

View File

@@ -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()

View File

@@ -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$)]")
})(); })();

View File

@@ -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];

View File

@@ -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 }]');
})(); })();

File diff suppressed because one or more lines are too long

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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");

View File

@@ -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

View File

@@ -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";

View File

@@ -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;

View File

@@ -108,7 +108,8 @@ CodeMirror.defineMode("yaml", function() {
literal: false, literal: false,
escaped: false escaped: false
}; };
} },
lineComment: "#"
}; };
}); });

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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);
} }
} }
} }

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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

View File

@@ -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);

View File

@@ -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 = {

View File

@@ -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);
} }
} }

View 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;

View File

@@ -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" };

View File

@@ -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

View File

@@ -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);

View File

@@ -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
}; };

View File

@@ -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);
});

View File

@@ -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