|
@ -0,0 +1,14 @@
|
|||
.idea
|
||||
coverage
|
||||
node_modules/
|
||||
|
||||
# ignore config files
|
||||
config.json
|
||||
.sequelizerc
|
||||
|
||||
# ignore webpack build
|
||||
public/build
|
||||
public/views/build
|
||||
|
||||
.nyc_output
|
||||
coverage/
|
|
@ -1,3 +0,0 @@
|
|||
lib/ot
|
||||
public/vendor
|
||||
public/build
|
21
.eslintrc.js
|
@ -1,21 +0,0 @@
|
|||
module.exports = {
|
||||
"root": true,
|
||||
"extends": "standard",
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"rules": {
|
||||
// at some point all of these should return to their default "error" state
|
||||
// but right now, this is not a good choice, because too many places are
|
||||
// wrong.
|
||||
"import/first": ["warn"],
|
||||
"indent": ["warn"],
|
||||
"no-multiple-empty-lines": ["warn"],
|
||||
"no-multi-spaces": ["warn"],
|
||||
"object-curly-spacing": ["warn"],
|
||||
"one-var": ["warn"],
|
||||
"quotes": ["warn"],
|
||||
"semi": ["warn"],
|
||||
"space-infix-ops": ["warn"]
|
||||
}
|
||||
};
|
|
@ -27,3 +27,5 @@ public/views/build
|
|||
|
||||
public/uploads/*
|
||||
!public/uploads/.gitkeep
|
||||
/.nyc_output
|
||||
/coverage/
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
var path = require('path');
|
||||
const path = require('path')
|
||||
const config = require('./lib/config')
|
||||
|
||||
module.exports = {
|
||||
'config': path.resolve('config.json'),
|
||||
config: path.resolve('config.json'),
|
||||
'migrations-path': path.resolve('lib', 'migrations'),
|
||||
'models-path': path.resolve('lib', 'models'),
|
||||
'url': 'change this'
|
||||
url: process.env['CMD_DB_URL'] || config.dbURL
|
||||
}
|
57
.travis.yml
|
@ -1,41 +1,34 @@
|
|||
language: node_js
|
||||
dist: trusty
|
||||
|
||||
node_js:
|
||||
- "lts/carbon"
|
||||
- "lts/dubnium"
|
||||
- "11"
|
||||
- "12"
|
||||
|
||||
dist: xenial
|
||||
cache: yarn
|
||||
env:
|
||||
global:
|
||||
- CXX=g++-4.8
|
||||
- YARN_VERSION=1.3.2
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- node_js: lts/carbon
|
||||
- node_js: lts/dubnium
|
||||
allow_failures:
|
||||
- node_js: "11"
|
||||
- node_js: "12"
|
||||
|
||||
script:
|
||||
- yarn test:ci
|
||||
- yarn build
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- env: task=npm-test
|
||||
node_js:
|
||||
- 8
|
||||
before_install:
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version "$YARN_VERSION"
|
||||
- export PATH="$HOME/.yarn/bin:$PATH"
|
||||
- env: task=npm-test
|
||||
node_js:
|
||||
- 10
|
||||
before_install:
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version "$YARN_VERSION"
|
||||
- export PATH="$HOME/.yarn/bin:$PATH"
|
||||
- env: task=ShellCheck
|
||||
script:
|
||||
- shellcheck bin/heroku bin/setup
|
||||
language: generic
|
||||
- env: task=doctoc
|
||||
install: npm install doctoc
|
||||
- stage: doctoc-check
|
||||
install: npm install -g doctoc
|
||||
if: type = pull_request OR branch = master
|
||||
script:
|
||||
- cp README.md README.md.orig
|
||||
- npm run doctoc
|
||||
- diff -q README.md README.md.orig
|
||||
language: generic
|
||||
- env: task=json-lint
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- jq
|
||||
script:
|
||||
- npm run jsonlint
|
||||
language: generic
|
||||
node_js: lts/carbon
|
||||
|
|
21
README.md
|
@ -9,6 +9,8 @@ CodiMD
|
|||
CodiMD lets you collaborate in real-time with markdown.
|
||||
Built on [HackMD](https://hackmd.io) source code, CodiMD lets you host and control your team's content with speed and ease.
|
||||
|
||||
![screenshot](https://raw.githubusercontent.com/hackmdio/codimd/develop/public/screenshot.png)
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
# Table of Contents
|
||||
|
@ -71,20 +73,11 @@ All contributions are welcome! Even asking a question helps.
|
|||
## Browser Support
|
||||
|
||||
CodiMD is a service that runs on Node.js, while users use the service through browsers. We support your users using the following browsers:
|
||||
- ![Chrome](http://browserbadge.com/chrome/47/18px)
|
||||
- Chrome >= 47
|
||||
- Chrome for Android >= 47
|
||||
- ![Safari](http://browserbadge.com/safari/9/18px)
|
||||
- Safari >= 9
|
||||
- iOS Safari >= 8.4
|
||||
- ![Firefox](http://browserbadge.com/firefox/44/18px)
|
||||
- Firefox >= 44
|
||||
- ![IE](http://browserbadge.com/ie/9/18px)
|
||||
- IE >= 9
|
||||
- Edge >= 12
|
||||
- ![Opera](http://browserbadge.com/opera/34/18px)
|
||||
- Opera >= 34
|
||||
- Opera Mini not supported
|
||||
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" /> Chrome >= 47, Chrome for Android >= 47
|
||||
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" /> Safari >= 9, iOS Safari >= 8.4
|
||||
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" /> Firefox >= 44
|
||||
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" /> IE >= 9, Edge >= 12
|
||||
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" /> Opera >= 34, Opera Mini not supported
|
||||
- Android Browser >= 4.4
|
||||
|
||||
To stay up to date with your installation it's recommended to subscribe the [release feed][github-release-feed].
|
||||
|
|
36
app.js
|
@ -7,7 +7,6 @@ var ejs = require('ejs')
|
|||
var passport = require('passport')
|
||||
var methodOverride = require('method-override')
|
||||
var cookieParser = require('cookie-parser')
|
||||
var compression = require('compression')
|
||||
var session = require('express-session')
|
||||
var SequelizeStore = require('connect-session-sequelize')(session.Store)
|
||||
var fs = require('fs')
|
||||
|
@ -26,19 +25,17 @@ var response = require('./lib/response')
|
|||
var models = require('./lib/models')
|
||||
var csp = require('./lib/csp')
|
||||
|
||||
// server setup
|
||||
var app = express()
|
||||
var server = null
|
||||
if (config.useSSL) {
|
||||
var ca = (function () {
|
||||
var i, len, results
|
||||
results = []
|
||||
function createHttpServer () {
|
||||
if (config.useSSL) {
|
||||
const ca = (function () {
|
||||
let i, len
|
||||
const results = []
|
||||
for (i = 0, len = config.sslCAPath.length; i < len; i++) {
|
||||
results.push(fs.readFileSync(config.sslCAPath[i], 'utf8'))
|
||||
}
|
||||
return results
|
||||
})()
|
||||
var options = {
|
||||
const options = {
|
||||
key: fs.readFileSync(config.sslKeyPath, 'utf8'),
|
||||
cert: fs.readFileSync(config.sslCertPath, 'utf8'),
|
||||
ca: ca,
|
||||
|
@ -46,14 +43,19 @@ if (config.useSSL) {
|
|||
requestCert: false,
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
server = require('https').createServer(options, app)
|
||||
} else {
|
||||
server = require('http').createServer(app)
|
||||
return require('https').createServer(options, app)
|
||||
} else {
|
||||
return require('http').createServer(app)
|
||||
}
|
||||
}
|
||||
|
||||
// server setup
|
||||
var app = express()
|
||||
var server = createHttpServer()
|
||||
|
||||
// logger
|
||||
app.use(morgan('combined', {
|
||||
'stream': logger.stream
|
||||
stream: logger.stream
|
||||
}))
|
||||
|
||||
// socket io
|
||||
|
@ -77,9 +79,6 @@ var sessionStore = new SequelizeStore({
|
|||
db: models.sequelize
|
||||
})
|
||||
|
||||
// compression
|
||||
app.use(compression())
|
||||
|
||||
// use hsts to tell https users stick to this
|
||||
if (config.hsts.enable) {
|
||||
app.use(helmet.hsts({
|
||||
|
@ -181,6 +180,7 @@ app.locals.serverURL = config.serverURL
|
|||
app.locals.sourceURL = config.sourceURL
|
||||
app.locals.allowAnonymous = config.allowAnonymous
|
||||
app.locals.allowAnonymousEdits = config.allowAnonymousEdits
|
||||
app.locals.permission = config.permission
|
||||
app.locals.allowPDFExport = config.allowPDFExport
|
||||
app.locals.authProviders = {
|
||||
facebook: config.isFacebookEnable,
|
||||
|
@ -279,6 +279,7 @@ process.on('uncaughtException', function (err) {
|
|||
function handleTermSignals () {
|
||||
logger.info('CodiMD has been killed by signal, try to exit gracefully...')
|
||||
realtime.maintenance = true
|
||||
realtime.terminate()
|
||||
// disconnect all socket.io clients
|
||||
Object.keys(io.sockets.sockets).forEach(function (key) {
|
||||
var socket = io.sockets.sockets[key]
|
||||
|
@ -299,6 +300,9 @@ function handleTermSignals () {
|
|||
})
|
||||
}
|
||||
}, 100)
|
||||
setTimeout(() => {
|
||||
process.exit(1)
|
||||
}, 5000)
|
||||
}
|
||||
process.on('SIGINT', handleTermSignals)
|
||||
process.on('SIGTERM', handleTermSignals)
|
||||
|
|
8
app.json
|
@ -11,18 +11,10 @@
|
|||
"logo": "https://github.com/hackmdio/codimd/raw/master/public/codimd-icon-1024.png",
|
||||
"success_url": "/",
|
||||
"env": {
|
||||
"BUILD_ASSETS": {
|
||||
"description": "Our build script variable",
|
||||
"value": "true"
|
||||
},
|
||||
"NPM_CONFIG_PRODUCTION": {
|
||||
"description": "Let npm also install development build tool",
|
||||
"value": "false"
|
||||
},
|
||||
"DB_TYPE": {
|
||||
"description": "Specify database type. See sequelize available databases. Default using postgres",
|
||||
"value": "postgres"
|
||||
},
|
||||
"HMD_SESSION_SECRET": {
|
||||
"description": "Secret used to secure session cookies.",
|
||||
"required": false
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
set -e
|
||||
|
||||
if [ "$BUILD_ASSETS" = true ]; then
|
||||
if [ ! -z "$DYNO" ]; then
|
||||
# setup config files
|
||||
cat << EOF > .sequelizerc
|
||||
var path = require('path');
|
||||
|
@ -11,8 +11,7 @@ module.exports = {
|
|||
'config': path.resolve('config.json'),
|
||||
'migrations-path': path.resolve('lib', 'migrations'),
|
||||
'models-path': path.resolve('lib', 'models'),
|
||||
'url': process.env.DATABASE_URL,
|
||||
'dialect': process.env.DB_TYPE
|
||||
'url': process.env.DATABASE_URL
|
||||
}
|
||||
|
||||
EOF
|
||||
|
@ -26,6 +25,4 @@ EOF
|
|||
|
||||
EOF
|
||||
|
||||
# build app
|
||||
npm run build
|
||||
fi
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
"db": {
|
||||
"dialect": "sqlite",
|
||||
"storage": ":memory:"
|
||||
}
|
||||
},
|
||||
"linkifyHeaderStyle": "gfm"
|
||||
},
|
||||
"development": {
|
||||
"loglevel": "debug",
|
||||
|
@ -13,7 +14,8 @@
|
|||
"db": {
|
||||
"dialect": "sqlite",
|
||||
"storage": "./db.codimd.sqlite"
|
||||
}
|
||||
},
|
||||
"linkifyHeaderStyle": "gfm"
|
||||
},
|
||||
"production": {
|
||||
"domain": "localhost",
|
||||
|
@ -123,6 +125,11 @@
|
|||
{
|
||||
"connectionString": "change this",
|
||||
"container": "change this"
|
||||
}
|
||||
},
|
||||
"plantuml":
|
||||
{
|
||||
"server": "https://www.plantuml.com/plantuml"
|
||||
},
|
||||
"linkifyHeaderStyle": "gfm"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
FROM hackmdio/buildpack:1.0.4 as BUILD
|
||||
|
||||
COPY --chown=hackmd:hackmd . .
|
||||
|
||||
RUN set -xe && \
|
||||
git reset --hard && \
|
||||
git clean -fx && \
|
||||
yarn install && \
|
||||
yarn build && \
|
||||
yarn install --production=true && \
|
||||
cp ./deployments/docker-entrypoint.sh ./ && \
|
||||
cp .sequelizerc.example .sequelizerc && \
|
||||
rm -rf .git .gitignore .travis.yml .dockerignore .editorconfig .babelrc .mailmap .sequelizerc.example \
|
||||
test docs contribute \
|
||||
yarn.lock webpack.prod.js webpack.htmlexport.js webpack.dev.js webpack.common.js \
|
||||
config.json.example README.md CONTRIBUTING.md AUTHORS
|
||||
|
||||
FROM hackmdio/runtime:1.0.4
|
||||
USER hackmd
|
||||
WORKDIR /home/hackmd/app
|
||||
COPY --chown=1500:1500 --from=BUILD /home/hackmd/app .
|
||||
EXPOSE 3000
|
||||
ENTRYPOINT ["/home/hackmd/app/docker-entrypoint.sh"]
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
CURRENT_DIR=$(dirname "$BASH_SOURCE")
|
||||
|
||||
docker build -t hackmdio/codimd -f "$CURRENT_DIR/Dockerfile" "$CURRENT_DIR/.."
|
|
@ -0,0 +1,28 @@
|
|||
version: "3"
|
||||
services:
|
||||
database:
|
||||
image: postgres:11.5
|
||||
environment:
|
||||
- POSTGRES_USER=codimd
|
||||
- POSTGRES_PASSWORD=change_password
|
||||
- POSTGRES_DB=codimd
|
||||
volumes:
|
||||
- "database-data:/var/lib/postgresql/data"
|
||||
restart: always
|
||||
codimd:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ./deployments/Dockerfile
|
||||
environment:
|
||||
- CMD_DB_URL=postgres://codimd:change_password@database/codimd
|
||||
- CMD_USECDN=false
|
||||
depends_on:
|
||||
- database
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- upload-data:/home/hackmd/app/public/uploads
|
||||
restart: always
|
||||
volumes:
|
||||
database-data: {}
|
||||
upload-data: {}
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$#" -gt 0 ]]; then
|
||||
exec "$@"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# check database and redis is ready
|
||||
pcheck -constr "$CMD_DB_URL"
|
||||
|
||||
# run DB migrate
|
||||
NEED_MIGRATE=${CMD_AUTO_MIGRATE:=true}
|
||||
|
||||
if [[ "$NEED_MIGRATE" = "true" ]] && [[ -f .sequelizerc ]] ; then
|
||||
npx sequelize db:migrate
|
||||
fi
|
||||
|
||||
# start application
|
||||
node app.js
|
|
@ -31,6 +31,7 @@ module.exports = {
|
|||
useCDN: true,
|
||||
allowAnonymous: true,
|
||||
allowAnonymousEdits: false,
|
||||
allowAnonymousViews: true,
|
||||
allowFreeURL: false,
|
||||
forbiddenNoteIDs: ['robots.txt', 'favicon.ico', 'api'],
|
||||
defaultPermission: 'editable',
|
||||
|
@ -60,8 +61,11 @@ module.exports = {
|
|||
responseMaxLag: 70,
|
||||
// document
|
||||
documentMaxLength: 100000,
|
||||
// image upload setting, available options are imgur/s3/filesystem/azure
|
||||
// image upload setting, available options are imgur/s3/filesystem/azure/lutim
|
||||
imageUploadType: 'filesystem',
|
||||
lutim: {
|
||||
url: 'https://framapic.org/'
|
||||
},
|
||||
imgur: {
|
||||
clientID: undefined
|
||||
},
|
||||
|
@ -100,6 +104,7 @@ module.exports = {
|
|||
consumerSecret: undefined
|
||||
},
|
||||
github: {
|
||||
enterpriseURL: undefined, // if you use github.com, not need to specify
|
||||
clientID: undefined,
|
||||
clientSecret: undefined
|
||||
},
|
||||
|
@ -151,9 +156,27 @@ module.exports = {
|
|||
email: undefined
|
||||
}
|
||||
},
|
||||
plantuml: {
|
||||
server: 'https://www.plantuml.com/plantuml'
|
||||
},
|
||||
email: true,
|
||||
allowEmailRegister: true,
|
||||
allowGravatar: true,
|
||||
allowPDFExport: true,
|
||||
openID: false
|
||||
openID: false,
|
||||
defaultUseHardbreak: true,
|
||||
// linkifyHeaderStyle - How is a header text converted into a link id.
|
||||
// Header Example: "3.1. Good Morning my Friend! - Do you have 5$?"
|
||||
// * 'keep-case' is the legacy CodiMD value.
|
||||
// Generated id: "31-Good-Morning-my-Friend---Do-you-have-5"
|
||||
// * 'lower-case' is the same like legacy (see above), but converted to lower-case.
|
||||
// Generated id: "#31-good-morning-my-friend---do-you-have-5"
|
||||
// * 'gfm' _GitHub-Flavored Markdown_ style as described here:
|
||||
// https://gist.github.com/asabaylus/3071099#gistcomment-1593627
|
||||
// It works like 'lower-case', but making sure the ID is unique.
|
||||
// This is What GitHub, GitLab and (hopefully) most other tools use.
|
||||
// Generated id: "31-good-morning-my-friend---do-you-have-5"
|
||||
// 2nd appearance: "31-good-morning-my-friend---do-you-have-5-1"
|
||||
// 3rd appearance: "31-good-morning-my-friend---do-you-have-5-2"
|
||||
linkifyHeaderStyle: 'keep-case'
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ module.exports = {
|
|||
useCDN: toBooleanConfig(process.env.CMD_USECDN),
|
||||
allowAnonymous: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS),
|
||||
allowAnonymousEdits: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS_EDITS),
|
||||
allowAnonymousViews: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS_VIEWS),
|
||||
allowFreeURL: toBooleanConfig(process.env.CMD_ALLOW_FREEURL),
|
||||
forbiddenNoteIDs: toArrayConfig(process.env.CMD_FORBIDDEN_NOTE_IDS),
|
||||
defaultPermission: process.env.CMD_DEFAULT_PERMISSION,
|
||||
|
@ -65,6 +66,7 @@ module.exports = {
|
|||
consumerSecret: process.env.CMD_TWITTER_CONSUMERSECRET
|
||||
},
|
||||
github: {
|
||||
enterpriseURL: process.env.CMD_GITHUB_ENTERPRISE_URL,
|
||||
clientID: process.env.CMD_GITHUB_CLIENTID,
|
||||
clientSecret: process.env.CMD_GITHUB_CLIENTSECRET
|
||||
},
|
||||
|
@ -127,9 +129,14 @@ module.exports = {
|
|||
email: process.env.CMD_SAML_ATTRIBUTE_EMAIL
|
||||
}
|
||||
},
|
||||
plantuml: {
|
||||
server: process.env.CMD_PLANTUML_SERVER
|
||||
},
|
||||
email: toBooleanConfig(process.env.CMD_EMAIL),
|
||||
allowEmailRegister: toBooleanConfig(process.env.CMD_ALLOW_EMAIL_REGISTER),
|
||||
allowGravatar: toBooleanConfig(process.env.CMD_ALLOW_GRAVATAR),
|
||||
allowPDFExport: toBooleanConfig(process.env.CMD_ALLOW_PDF_EXPORT),
|
||||
openID: toBooleanConfig(process.env.CMD_OPENID)
|
||||
openID: toBooleanConfig(process.env.CMD_OPENID),
|
||||
defaultUseHardbreak: toBooleanConfig(process.env.CMD_DEFAULT_USE_HARD_BREAK),
|
||||
linkifyHeaderStyle: process.env.CMD_LINKIFY_HEADER_STYLE
|
||||
}
|
||||
|
|
|
@ -53,14 +53,14 @@ if (['debug', 'verbose', 'info', 'warn', 'error'].includes(config.loglevel)) {
|
|||
|
||||
// load LDAP CA
|
||||
if (config.ldap.tlsca) {
|
||||
let ca = config.ldap.tlsca.split(',')
|
||||
let caContent = []
|
||||
for (let i of ca) {
|
||||
if (fs.existsSync(i)) {
|
||||
caContent.push(fs.readFileSync(i, 'utf8'))
|
||||
const certificateAuthorities = config.ldap.tlsca.split(',')
|
||||
const caContent = []
|
||||
for (const ca of certificateAuthorities) {
|
||||
if (fs.existsSync(ca)) {
|
||||
caContent.push(fs.readFileSync(ca, 'utf8'))
|
||||
}
|
||||
}
|
||||
let tlsOptions = {
|
||||
const tlsOptions = {
|
||||
ca: caContent
|
||||
}
|
||||
config.ldap.tlsOptions = config.ldap.tlsOptions ? Object.assign(config.ldap.tlsOptions, tlsOptions) : tlsOptions
|
||||
|
@ -68,11 +68,17 @@ if (config.ldap.tlsca) {
|
|||
|
||||
// Permission
|
||||
config.permission = Permission
|
||||
if (!config.allowAnonymous && !config.allowAnonymousEdits) {
|
||||
let defaultPermission = config.permission.editable
|
||||
if (!config.allowAnonymous && !config.allowAnonymousViews) {
|
||||
delete config.permission.freely
|
||||
delete config.permission.editable
|
||||
delete config.permission.locked
|
||||
defaultPermission = config.permission.limited
|
||||
} else if (!config.allowAnonymous && !config.allowAnonymousEdits) {
|
||||
delete config.permission.freely
|
||||
}
|
||||
if (!(config.defaultPermission in config.permission)) {
|
||||
config.defaultPermission = config.permission.editable
|
||||
config.defaultPermission = defaultPermission
|
||||
}
|
||||
|
||||
// cache result, cannot change config in runtime!!!
|
||||
|
@ -134,10 +140,10 @@ config.isGitlabSnippetsEnable = (!config.gitlab.scope || config.gitlab.scope ===
|
|||
config.updateI18nFiles = (env === Environment.development)
|
||||
|
||||
// merge legacy values
|
||||
let keys = Object.keys(config)
|
||||
const keys = Object.keys(config)
|
||||
const uppercase = /[A-Z]/
|
||||
for (let i = keys.length; i--;) {
|
||||
let lowercaseKey = keys[i].toLowerCase()
|
||||
const lowercaseKey = keys[i].toLowerCase()
|
||||
// if the config contains uppercase letters
|
||||
// and a lowercase version of this setting exists
|
||||
// and the config with uppercase is not set
|
||||
|
@ -164,8 +170,8 @@ if (config.sessionSecret === 'secret') {
|
|||
}
|
||||
|
||||
// Validate upload upload providers
|
||||
if (['filesystem', 's3', 'minio', 'imgur', 'azure'].indexOf(config.imageUploadType) === -1) {
|
||||
logger.error('"imageuploadtype" is not correctly set. Please use "filesystem", "s3", "minio", "azure" or "imgur". Defaulting to "filesystem"')
|
||||
if (['filesystem', 's3', 'minio', 'imgur', 'azure', 'lutim'].indexOf(config.imageUploadType) === -1) {
|
||||
logger.error('"imageuploadtype" is not correctly set. Please use "filesystem", "s3", "minio", "azure", "lutim" or "imgur". Defaulting to "filesystem"')
|
||||
config.imageUploadType = 'filesystem'
|
||||
}
|
||||
|
||||
|
@ -194,6 +200,7 @@ config.sslCAPath.forEach(function (capath, i, array) {
|
|||
array[i] = path.resolve(appRootPath, capath)
|
||||
})
|
||||
|
||||
config.appRootPath = appRootPath
|
||||
config.sslCertPath = path.resolve(appRootPath, config.sslCertPath)
|
||||
config.sslKeyPath = path.resolve(appRootPath, config.sslKeyPath)
|
||||
config.dhParamPath = path.resolve(appRootPath, config.dhParamPath)
|
||||
|
|
|
@ -9,14 +9,6 @@ var logger = require('./logger')
|
|||
var response = require('./response')
|
||||
var models = require('./models')
|
||||
|
||||
// public
|
||||
var History = {
|
||||
historyGet: historyGet,
|
||||
historyPost: historyPost,
|
||||
historyDelete: historyDelete,
|
||||
updateHistory: updateHistory
|
||||
}
|
||||
|
||||
function getHistory (userid, callback) {
|
||||
models.User.findOne({
|
||||
where: {
|
||||
|
@ -41,7 +33,7 @@ function getHistory (userid, callback) {
|
|||
continue
|
||||
}
|
||||
try {
|
||||
let id = LZString.decompressFromBase64(history[i].id)
|
||||
const id = LZString.decompressFromBase64(history[i].id)
|
||||
if (id && models.Note.checkNoteIdValid(id)) {
|
||||
history[i].id = models.Note.encodeNoteId(id)
|
||||
}
|
||||
|
@ -200,4 +192,8 @@ function historyDelete (req, res) {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = History
|
||||
// public
|
||||
exports.historyGet = historyGet
|
||||
exports.historyPost = historyPost
|
||||
exports.historyDelete = historyDelete
|
||||
exports.updateHistory = updateHistory
|
||||
|
|
|
@ -32,9 +32,9 @@ exports.generateAvatarURL = function (name, email = '', big = true) {
|
|||
}
|
||||
name = encodeURIComponent(name)
|
||||
|
||||
let hash = crypto.createHash('md5')
|
||||
const hash = crypto.createHash('md5')
|
||||
hash.update(email.toLowerCase())
|
||||
let hexDigest = hash.digest('hex')
|
||||
const hexDigest = hash.digest('hex')
|
||||
|
||||
if (email !== '' && config.allowGravatar) {
|
||||
photo = 'https://www.gravatar.com/avatar/' + hexDigest
|
||||
|
|
|
@ -165,15 +165,15 @@ module.exports = function (sequelize, DataTypes) {
|
|||
}
|
||||
Note.encodeNoteId = function (id) {
|
||||
// remove dashes in UUID and encode in url-safe base64
|
||||
let str = id.replace(/-/g, '')
|
||||
let hexStr = Buffer.from(str, 'hex')
|
||||
const str = id.replace(/-/g, '')
|
||||
const hexStr = Buffer.from(str, 'hex')
|
||||
return base64url.encode(hexStr)
|
||||
}
|
||||
Note.decodeNoteId = function (encodedId) {
|
||||
// decode from url-safe base64
|
||||
let id = base64url.toBuffer(encodedId).toString('hex')
|
||||
const id = base64url.toBuffer(encodedId).toString('hex')
|
||||
// add dashes between the UUID string parts
|
||||
let idParts = []
|
||||
const idParts = []
|
||||
idParts.push(id.substr(0, 8))
|
||||
idParts.push(id.substr(8, 4))
|
||||
idParts.push(id.substr(12, 4))
|
||||
|
@ -196,7 +196,7 @@ module.exports = function (sequelize, DataTypes) {
|
|||
}
|
||||
}).then(function (note) {
|
||||
if (note) {
|
||||
let filePath = path.join(config.docsPath, noteId + '.md')
|
||||
const filePath = path.join(config.docsPath, noteId + '.md')
|
||||
if (Note.checkFileExist(filePath)) {
|
||||
// if doc in filesystem have newer modified time than last change time
|
||||
// then will update the doc in db
|
||||
|
@ -421,20 +421,20 @@ module.exports = function (sequelize, DataTypes) {
|
|||
if (ot.TextOperation.isRetain(op)) {
|
||||
index += op
|
||||
} else if (ot.TextOperation.isInsert(op)) {
|
||||
let opStart = index
|
||||
let opEnd = index + op.length
|
||||
var inserted = false
|
||||
const opStart = index
|
||||
const opEnd = index + op.length
|
||||
let inserted = false
|
||||
// authorship format: [userId, startPos, endPos, createdAt, updatedAt]
|
||||
if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp])
|
||||
else {
|
||||
for (let j = 0; j < authorships.length; j++) {
|
||||
let authorship = authorships[j]
|
||||
const authorship = authorships[j]
|
||||
if (!inserted) {
|
||||
let nextAuthorship = authorships[j + 1] || -1
|
||||
const nextAuthorship = authorships[j + 1] || -1
|
||||
if ((nextAuthorship !== -1 && nextAuthorship[1] >= opEnd) || j >= authorships.length - 1) {
|
||||
if (authorship[1] < opStart && authorship[2] > opStart) {
|
||||
// divide
|
||||
let postLength = authorship[2] - opStart
|
||||
const postLength = authorship[2] - opStart
|
||||
authorship[2] = opStart
|
||||
authorship[4] = timestamp
|
||||
authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp])
|
||||
|
@ -460,13 +460,13 @@ module.exports = function (sequelize, DataTypes) {
|
|||
}
|
||||
index += op.length
|
||||
} else if (ot.TextOperation.isDelete(op)) {
|
||||
let opStart = index
|
||||
let opEnd = index - op
|
||||
const opStart = index
|
||||
const opEnd = index - op
|
||||
if (operation.length === 1) {
|
||||
authorships = []
|
||||
} else if (authorships.length > 0) {
|
||||
for (let j = 0; j < authorships.length; j++) {
|
||||
let authorship = authorships[j]
|
||||
const authorship = authorships[j]
|
||||
if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) {
|
||||
authorships.splice(j, 1)
|
||||
j -= 1
|
||||
|
@ -491,12 +491,12 @@ module.exports = function (sequelize, DataTypes) {
|
|||
}
|
||||
// merge
|
||||
for (let j = 0; j < authorships.length; j++) {
|
||||
let authorship = authorships[j]
|
||||
const authorship = authorships[j]
|
||||
for (let k = j + 1; k < authorships.length; k++) {
|
||||
let nextAuthorship = authorships[k]
|
||||
const nextAuthorship = authorships[k]
|
||||
if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) {
|
||||
let minTimestamp = Math.min(authorship[3], nextAuthorship[3])
|
||||
let maxTimestamp = Math.max(authorship[3], nextAuthorship[3])
|
||||
const minTimestamp = Math.min(authorship[3], nextAuthorship[3])
|
||||
const maxTimestamp = Math.max(authorship[3], nextAuthorship[3])
|
||||
authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp])
|
||||
authorships.splice(k, 1)
|
||||
j -= 1
|
||||
|
@ -506,7 +506,7 @@ module.exports = function (sequelize, DataTypes) {
|
|||
}
|
||||
// clear
|
||||
for (let j = 0; j < authorships.length; j++) {
|
||||
let authorship = authorships[j]
|
||||
const authorship = authorships[j]
|
||||
if (!authorship[0]) {
|
||||
authorships.splice(j, 1)
|
||||
j -= 1
|
||||
|
@ -537,11 +537,11 @@ module.exports = function (sequelize, DataTypes) {
|
|||
var lengthBias = 0
|
||||
for (let j = 0; j < patch.length; j++) {
|
||||
var operation = []
|
||||
let p = patch[j]
|
||||
const p = patch[j]
|
||||
var currIndex = p.start1
|
||||
var currLength = contentLength - bias
|
||||
for (let i = 0; i < p.diffs.length; i++) {
|
||||
let diff = p.diffs[i]
|
||||
const diff = p.diffs[i]
|
||||
switch (diff[0]) {
|
||||
case 0: // retain
|
||||
if (i === 0) {
|
||||
|
|
|
@ -103,7 +103,8 @@ module.exports = function (sequelize, DataTypes) {
|
|||
else photo += '?size=bigger'
|
||||
break
|
||||
case 'github':
|
||||
photo = 'https://avatars.githubusercontent.com/u/' + profile.id
|
||||
if (profile.photos && profile.photos[0]) photo = profile.photos[0].value.replace('?', '')
|
||||
else photo = 'https://avatars.githubusercontent.com/u/' + profile.id
|
||||
if (bigger) photo += '?s=400'
|
||||
else photo += '?s=96'
|
||||
break
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
'use strict'
|
||||
|
||||
const EventEmitter = require('events').EventEmitter
|
||||
|
||||
/**
|
||||
* Queuing Class for connection queuing
|
||||
*/
|
||||
|
||||
const QueueEvent = {
|
||||
Tick: 'Tick',
|
||||
Push: 'Push',
|
||||
Finish: 'Finish'
|
||||
}
|
||||
|
||||
class ProcessQueue extends EventEmitter {
|
||||
constructor ({
|
||||
maximumLength = 500,
|
||||
triggerTimeInterval = 5000,
|
||||
// execute on push
|
||||
proactiveMode = true,
|
||||
// execute next work on finish
|
||||
continuousMode = true
|
||||
}) {
|
||||
super()
|
||||
this.max = maximumLength
|
||||
this.triggerTime = triggerTimeInterval
|
||||
this.taskMap = new Map()
|
||||
this.queue = []
|
||||
this.lock = false
|
||||
|
||||
this.on(QueueEvent.Tick, this.onEventProcessFunc.bind(this))
|
||||
if (proactiveMode) {
|
||||
this.on(QueueEvent.Push, this.onEventProcessFunc.bind(this))
|
||||
}
|
||||
if (continuousMode) {
|
||||
this.on(QueueEvent.Finish, this.onEventProcessFunc.bind(this))
|
||||
}
|
||||
}
|
||||
|
||||
onEventProcessFunc () {
|
||||
if (this.lock) return
|
||||
this.lock = true
|
||||
setImmediate(() => {
|
||||
this.process()
|
||||
})
|
||||
}
|
||||
|
||||
start () {
|
||||
if (this.eventTrigger) return
|
||||
this.eventTrigger = setInterval(() => {
|
||||
this.emit(QueueEvent.Tick)
|
||||
}, this.triggerTime)
|
||||
}
|
||||
|
||||
stop () {
|
||||
if (this.eventTrigger) {
|
||||
clearInterval(this.eventTrigger)
|
||||
this.eventTrigger = null
|
||||
}
|
||||
}
|
||||
|
||||
checkTaskIsInQueue (id) {
|
||||
return this.taskMap.has(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* pushWithKey a promisify-task to queue
|
||||
* @param id {string}
|
||||
* @param processingFunc {Function<Promise>}
|
||||
* @returns {boolean} if success return true, otherwise false
|
||||
*/
|
||||
push (id, processingFunc) {
|
||||
if (this.queue.length >= this.max) return false
|
||||
if (this.checkTaskIsInQueue(id)) return false
|
||||
const task = {
|
||||
id: id,
|
||||
processingFunc: processingFunc
|
||||
}
|
||||
this.taskMap.set(id, true)
|
||||
this.queue.push(task)
|
||||
this.start()
|
||||
this.emit(QueueEvent.Push)
|
||||
return true
|
||||
}
|
||||
|
||||
process () {
|
||||
if (this.queue.length <= 0) {
|
||||
this.stop()
|
||||
this.lock = false
|
||||
return
|
||||
}
|
||||
|
||||
const task = this.queue.shift()
|
||||
this.taskMap.delete(task.id)
|
||||
|
||||
const finishTask = () => {
|
||||
this.lock = false
|
||||
setImmediate(() => {
|
||||
this.emit(QueueEvent.Finish)
|
||||
})
|
||||
}
|
||||
task.processingFunc().then(finishTask).catch(finishTask)
|
||||
}
|
||||
}
|
||||
|
||||
exports.ProcessQueue = ProcessQueue
|
940
lib/realtime.js
|
@ -0,0 +1,49 @@
|
|||
'use strict'
|
||||
|
||||
const async = require('async')
|
||||
const config = require('./config')
|
||||
const logger = require('./logger')
|
||||
|
||||
/**
|
||||
* clean when user not in any rooms or user not in connected list
|
||||
*/
|
||||
class CleanDanglingUserJob {
|
||||
constructor (realtime) {
|
||||
this.realtime = realtime
|
||||
}
|
||||
|
||||
start () {
|
||||
if (this.timer) return
|
||||
this.timer = setInterval(this.cleanDanglingUser.bind(this), 60000)
|
||||
}
|
||||
|
||||
stop () {
|
||||
if (!this.timer) return
|
||||
clearInterval(this.timer)
|
||||
this.timer = undefined
|
||||
}
|
||||
|
||||
cleanDanglingUser () {
|
||||
const users = this.realtime.getUserPool()
|
||||
async.each(Object.keys(users), (key, callback) => {
|
||||
const socket = this.realtime.io.sockets.connected[key]
|
||||
if ((!socket && users[key]) ||
|
||||
(socket && (!socket.rooms || socket.rooms.length <= 0))) {
|
||||
if (config.debug) {
|
||||
logger.info('cleaner found redundant user: ' + key)
|
||||
}
|
||||
if (!socket) {
|
||||
return callback(null, null)
|
||||
}
|
||||
if (!this.realtime.disconnectProcessQueue.checkTaskIsInQueue(socket.id)) {
|
||||
this.realtime.queueForDisconnect(socket)
|
||||
}
|
||||
}
|
||||
return callback(null, null)
|
||||
}, function (err) {
|
||||
if (err) return logger.error('cleaner error', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.CleanDanglingUserJob = CleanDanglingUserJob
|
|
@ -0,0 +1,245 @@
|
|||
'use strict'
|
||||
|
||||
const get = require('lodash/get')
|
||||
|
||||
const config = require('./config')
|
||||
const models = require('./models')
|
||||
const logger = require('./logger')
|
||||
|
||||
class RealtimeClientConnection {
|
||||
constructor (socket) {
|
||||
this.socket = socket
|
||||
this.realtime = require('./realtime')
|
||||
}
|
||||
|
||||
registerEventHandler () {
|
||||
// received client refresh request
|
||||
this.socket.on('refresh', this.refreshEventHandler.bind(this))
|
||||
// received user status
|
||||
this.socket.on('user status', this.userStatusEventHandler.bind(this))
|
||||
// when a new client disconnect
|
||||
this.socket.on('disconnect', this.disconnectEventHandler.bind(this))
|
||||
// received cursor focus
|
||||
this.socket.on('cursor focus', this.cursorFocusEventHandler.bind(this))
|
||||
// received cursor activity
|
||||
this.socket.on('cursor activity', this.cursorActivityEventHandler.bind(this))
|
||||
// received cursor blur
|
||||
this.socket.on('cursor blur', this.cursorBlurEventHandler.bind(this))
|
||||
// check version
|
||||
this.socket.on('version', this.checkVersionEventHandler.bind(this))
|
||||
// received sync of online users request
|
||||
this.socket.on('online users', this.onlineUsersEventHandler.bind(this))
|
||||
// reveiced when user logout or changed
|
||||
this.socket.on('user changed', this.userChangedEventHandler.bind(this))
|
||||
// delete a note
|
||||
this.socket.on('delete', this.deleteNoteEventHandler.bind(this))
|
||||
// received note permission change request
|
||||
this.socket.on('permission', this.permissionChangeEventHandler.bind(this))
|
||||
}
|
||||
|
||||
isUserLoggedIn () {
|
||||
return this.socket.request.user && this.socket.request.user.logged_in
|
||||
}
|
||||
|
||||
isNoteAndUserExists () {
|
||||
const note = this.realtime.getNoteFromNotePool(this.socket.noteId)
|
||||
const user = this.realtime.getUserFromUserPool(this.socket.id)
|
||||
return note && user
|
||||
}
|
||||
|
||||
isNoteOwner () {
|
||||
const note = this.getCurrentNote()
|
||||
return get(note, 'owner') === this.getCurrentLoggedInUserId()
|
||||
}
|
||||
|
||||
isAnonymousEnable () {
|
||||
// TODO: move this method to config module
|
||||
return config.allowAnonymous || config.allowAnonymousEdits
|
||||
}
|
||||
|
||||
getAvailablePermissions () {
|
||||
// TODO: move this method to config module
|
||||
const availablePermission = Object.assign({}, config.permission)
|
||||
if (!config.allowAnonymous && !config.allowAnonymousViews) {
|
||||
delete availablePermission.freely
|
||||
delete availablePermission.editable
|
||||
delete availablePermission.locked
|
||||
} else if (!config.allowAnonymous && !config.allowAnonymousEdits) {
|
||||
delete availablePermission.freely
|
||||
}
|
||||
return availablePermission
|
||||
}
|
||||
|
||||
getCurrentUser () {
|
||||
if (!this.socket.id) return
|
||||
return this.realtime.getUserFromUserPool(this.socket.id)
|
||||
}
|
||||
|
||||
getCurrentLoggedInUserId () {
|
||||
return get(this.socket, 'request.user.id')
|
||||
}
|
||||
|
||||
getCurrentNote () {
|
||||
if (!this.socket.noteId) return
|
||||
return this.realtime.getNoteFromNotePool(this.socket.noteId)
|
||||
}
|
||||
|
||||
getNoteChannel () {
|
||||
return this.socket.broadcast.to(this.socket.noteId)
|
||||
}
|
||||
|
||||
async destroyNote (id) {
|
||||
return models.Note.destroy({
|
||||
where: { id: id }
|
||||
})
|
||||
}
|
||||
|
||||
async changeNotePermission (newPermission) {
|
||||
const [changedRows] = await models.Note.update({
|
||||
permission: newPermission
|
||||
}, {
|
||||
where: {
|
||||
id: this.getCurrentNote().id
|
||||
}
|
||||
})
|
||||
if (changedRows !== 1) {
|
||||
throw new Error(`updated permission failed, cannot set permission ${newPermission} to note ${this.getCurrentNote().id}`)
|
||||
}
|
||||
}
|
||||
|
||||
notifyPermissionChanged () {
|
||||
this.realtime.io.to(this.getCurrentNote().id).emit('permission', {
|
||||
permission: this.getCurrentNote().permission
|
||||
})
|
||||
this.getCurrentNote().socks.forEach((sock) => {
|
||||
if (sock) {
|
||||
if (!this.realtime.checkViewPermission(sock.request, this.getCurrentNote())) {
|
||||
sock.emit('info', {
|
||||
code: 403
|
||||
})
|
||||
setTimeout(function () {
|
||||
sock.disconnect(true)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
refreshEventHandler () {
|
||||
this.realtime.emitRefresh(this.socket)
|
||||
}
|
||||
|
||||
checkVersionEventHandler () {
|
||||
this.socket.emit('version', {
|
||||
version: config.fullversion,
|
||||
minimumCompatibleVersion: config.minimumCompatibleVersion
|
||||
})
|
||||
}
|
||||
|
||||
userStatusEventHandler (data) {
|
||||
if (!this.isNoteAndUserExists()) return
|
||||
const user = this.getCurrentUser()
|
||||
if (config.debug) {
|
||||
logger.info('SERVER received [' + this.socket.noteId + '] user status from [' + this.socket.id + ']: ' + JSON.stringify(data))
|
||||
}
|
||||
if (data) {
|
||||
user.idle = data.idle
|
||||
user.type = data.type
|
||||
}
|
||||
this.realtime.emitUserStatus(this.socket)
|
||||
}
|
||||
|
||||
userChangedEventHandler () {
|
||||
logger.info('user changed')
|
||||
|
||||
const note = this.getCurrentNote()
|
||||
if (!note) return
|
||||
const user = note.users[this.socket.id]
|
||||
if (!user) return
|
||||
|
||||
this.realtime.updateUserData(this.socket, user)
|
||||
this.realtime.emitOnlineUsers(this.socket)
|
||||
}
|
||||
|
||||
onlineUsersEventHandler () {
|
||||
if (!this.isNoteAndUserExists()) return
|
||||
|
||||
const currentNote = this.getCurrentNote()
|
||||
|
||||
const currentNoteOnlineUserList = Object.keys(currentNote.users)
|
||||
.map(key => this.realtime.buildUserOutData(currentNote.users[key]))
|
||||
|
||||
this.socket.emit('online users', {
|
||||
users: currentNoteOnlineUserList
|
||||
})
|
||||
}
|
||||
|
||||
cursorFocusEventHandler (data) {
|
||||
if (!this.isNoteAndUserExists()) return
|
||||
const user = this.getCurrentUser()
|
||||
user.cursor = data
|
||||
const out = this.realtime.buildUserOutData(user)
|
||||
this.getNoteChannel().emit('cursor focus', out)
|
||||
}
|
||||
|
||||
cursorActivityEventHandler (data) {
|
||||
if (!this.isNoteAndUserExists()) return
|
||||
const user = this.getCurrentUser()
|
||||
user.cursor = data
|
||||
const out = this.realtime.buildUserOutData(user)
|
||||
this.getNoteChannel().emit('cursor activity', out)
|
||||
}
|
||||
|
||||
cursorBlurEventHandler () {
|
||||
if (!this.isNoteAndUserExists()) return
|
||||
const user = this.getCurrentUser()
|
||||
user.cursor = null
|
||||
this.getNoteChannel().emit('cursor blur', {
|
||||
id: this.socket.id
|
||||
})
|
||||
}
|
||||
|
||||
deleteNoteEventHandler () {
|
||||
// need login to do more actions
|
||||
if (this.isUserLoggedIn() && this.isNoteAndUserExists()) {
|
||||
const note = this.getCurrentNote()
|
||||
// Only owner can delete note
|
||||
if (note.owner && note.owner === this.getCurrentLoggedInUserId()) {
|
||||
this.destroyNote(note.id)
|
||||
.then((successRows) => {
|
||||
if (!successRows) return
|
||||
this.realtime.disconnectSocketOnNote(note)
|
||||
})
|
||||
.catch(function (err) {
|
||||
return logger.error('delete note failed: ' + err)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
permissionChangeEventHandler (permission) {
|
||||
if (!this.isUserLoggedIn()) return
|
||||
if (!this.isNoteAndUserExists()) return
|
||||
|
||||
const note = this.getCurrentNote()
|
||||
// Only owner can change permission
|
||||
if (!this.isNoteOwner()) return
|
||||
if (!(permission in this.getAvailablePermissions())) return
|
||||
|
||||
this.changeNotePermission(permission)
|
||||
.then(() => {
|
||||
note.permission = permission
|
||||
this.notifyPermissionChanged()
|
||||
})
|
||||
.catch(err => logger.error('update note permission failed: ' + err))
|
||||
}
|
||||
|
||||
disconnectEventHandler () {
|
||||
if (this.realtime.disconnectProcessQueue.checkTaskIsInQueue(this.socket.id)) {
|
||||
return
|
||||
}
|
||||
this.realtime.queueForDisconnect(this.socket)
|
||||
}
|
||||
}
|
||||
|
||||
exports.RealtimeClientConnection = RealtimeClientConnection
|
|
@ -0,0 +1,45 @@
|
|||
'use strict'
|
||||
|
||||
const models = require('./models')
|
||||
const logger = require('./logger')
|
||||
|
||||
/**
|
||||
* clean when user not in any rooms or user not in connected list
|
||||
*/
|
||||
class SaveRevisionJob {
|
||||
constructor (realtime) {
|
||||
this.realtime = realtime
|
||||
this.saverSleep = false
|
||||
}
|
||||
|
||||
start () {
|
||||
if (this.timer) return
|
||||
this.timer = setInterval(this.saveRevision.bind(this), 5 * 60 * 1000)
|
||||
}
|
||||
|
||||
stop () {
|
||||
if (!this.timer) return
|
||||
clearInterval(this.timer)
|
||||
this.timer = undefined
|
||||
}
|
||||
|
||||
saveRevision () {
|
||||
if (this.getSaverSleep()) return
|
||||
models.Revision.saveAllNotesRevision((err, notes) => {
|
||||
if (err) return logger.error('revision saver failed: ' + err)
|
||||
if (notes && notes.length <= 0) {
|
||||
this.setSaverSleep(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getSaverSleep () {
|
||||
return this.saverSleep
|
||||
}
|
||||
|
||||
setSaverSleep (val) {
|
||||
this.saverSleep = val
|
||||
}
|
||||
}
|
||||
|
||||
exports.SaveRevisionJob = SaveRevisionJob
|
|
@ -0,0 +1,78 @@
|
|||
'use strict'
|
||||
|
||||
const config = require('./config')
|
||||
const logger = require('./logger')
|
||||
const moment = require('moment')
|
||||
|
||||
class UpdateDirtyNoteJob {
|
||||
constructor (realtime) {
|
||||
this.realtime = realtime
|
||||
}
|
||||
|
||||
start () {
|
||||
if (this.timer) return
|
||||
this.timer = setInterval(this.updateDirtyNotes.bind(this), 1000)
|
||||
}
|
||||
|
||||
stop () {
|
||||
if (!this.timer) return
|
||||
clearInterval(this.timer)
|
||||
this.timer = undefined
|
||||
}
|
||||
|
||||
updateDirtyNotes () {
|
||||
const notes = this.realtime.getNotePool()
|
||||
Object.keys(notes).forEach((key) => {
|
||||
const note = notes[key]
|
||||
this.updateDirtyNote(note)
|
||||
.catch((err) => {
|
||||
logger.error('updateDirtyNote: updater error', err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async updateDirtyNote (note) {
|
||||
const notes = this.realtime.getNotePool()
|
||||
if (!note.server.isDirty) return
|
||||
|
||||
if (config.debug) logger.info('updateDirtyNote: updater found dirty note: ' + note.id)
|
||||
note.server.isDirty = false
|
||||
|
||||
try {
|
||||
const _note = await this.updateNoteAsync(note)
|
||||
// handle when note already been clean up
|
||||
if (!notes[note.id] || !notes[note.id].server) return
|
||||
|
||||
if (!_note) {
|
||||
this.realtime.io.to(note.id).emit('info', {
|
||||
code: 404
|
||||
})
|
||||
logger.error('updateDirtyNote: note not found: ', note.id)
|
||||
this.realtime.disconnectSocketOnNote(note)
|
||||
}
|
||||
|
||||
note.updatetime = moment(_note.lastchangeAt).valueOf()
|
||||
this.realtime.emitCheck(note)
|
||||
} catch (err) {
|
||||
logger.error('updateDirtyNote: note not found: ', note.id)
|
||||
this.realtime.io.to(note.id).emit('info', {
|
||||
code: 404
|
||||
})
|
||||
this.realtime.disconnectSocketOnNote(note)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
updateNoteAsync (note) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.realtime.updateNote(note, (err, _note) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
return resolve(_note)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.UpdateDirtyNoteJob = UpdateDirtyNoteJob
|
148
lib/response.js
|
@ -1,24 +1,40 @@
|
|||
'use strict'
|
||||
// response
|
||||
// external modules
|
||||
var fs = require('fs')
|
||||
var path = require('path')
|
||||
var markdownpdf = require('markdown-pdf')
|
||||
var shortId = require('shortid')
|
||||
var querystring = require('querystring')
|
||||
var request = require('request')
|
||||
var moment = require('moment')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const markdownpdf = require('markdown-pdf')
|
||||
const shortId = require('shortid')
|
||||
const querystring = require('querystring')
|
||||
const request = require('request')
|
||||
const moment = require('moment')
|
||||
|
||||
// core
|
||||
var config = require('./config')
|
||||
var logger = require('./logger')
|
||||
var models = require('./models')
|
||||
var utils = require('./utils')
|
||||
var history = require('./history')
|
||||
const config = require('./config')
|
||||
const logger = require('./logger')
|
||||
const models = require('./models')
|
||||
const utils = require('./utils')
|
||||
const history = require('./history')
|
||||
|
||||
// public
|
||||
var response = {
|
||||
errorForbidden: function (res) {
|
||||
exports.errorForbidden = errorForbidden
|
||||
exports.errorNotFound = errorNotFound
|
||||
exports.errorBadRequest = errorBadRequest
|
||||
exports.errorTooLong = errorTooLong
|
||||
exports.errorInternalError = errorInternalError
|
||||
exports.errorServiceUnavailable = errorServiceUnavailable
|
||||
exports.newNote = newNote
|
||||
exports.showNote = showNote
|
||||
exports.showPublishNote = showPublishNote
|
||||
exports.showPublishSlide = showPublishSlide
|
||||
exports.showIndex = showIndex
|
||||
exports.noteActions = noteActions
|
||||
exports.publishNoteActions = publishNoteActions
|
||||
exports.publishSlideActions = publishSlideActions
|
||||
exports.githubActions = githubActions
|
||||
exports.gitlabActions = gitlabActions
|
||||
|
||||
function errorForbidden (res) {
|
||||
const { req } = res
|
||||
if (req.user) {
|
||||
responseError(res, '403', 'Forbidden', 'oh no.')
|
||||
|
@ -26,32 +42,21 @@ var response = {
|
|||
req.flash('error', 'You are not allowed to access this page. Maybe try logging in?')
|
||||
res.redirect(config.serverURL + '/')
|
||||
}
|
||||
},
|
||||
errorNotFound: function (res) {
|
||||
}
|
||||
function errorNotFound (res) {
|
||||
responseError(res, '404', 'Not Found', 'oops.')
|
||||
},
|
||||
errorBadRequest: function (res) {
|
||||
}
|
||||
function errorBadRequest (res) {
|
||||
responseError(res, '400', 'Bad Request', 'something not right.')
|
||||
},
|
||||
errorTooLong: function (res) {
|
||||
}
|
||||
function errorTooLong (res) {
|
||||
responseError(res, '413', 'Payload Too Large', 'Shorten your note!')
|
||||
},
|
||||
errorInternalError: function (res) {
|
||||
}
|
||||
function errorInternalError (res) {
|
||||
responseError(res, '500', 'Internal Error', 'wtf.')
|
||||
},
|
||||
errorServiceUnavailable: function (res) {
|
||||
}
|
||||
function errorServiceUnavailable (res) {
|
||||
res.status(503).send("I'm busy right now, try again later.")
|
||||
},
|
||||
newNote: newNote,
|
||||
showNote: showNote,
|
||||
showPublishNote: showPublishNote,
|
||||
showPublishSlide: showPublishSlide,
|
||||
showIndex: showIndex,
|
||||
noteActions: noteActions,
|
||||
publishNoteActions: publishNoteActions,
|
||||
publishSlideActions: publishSlideActions,
|
||||
githubActions: githubActions,
|
||||
gitlabActions: gitlabActions
|
||||
}
|
||||
|
||||
function responseError (res, code, detail, msg) {
|
||||
|
@ -117,7 +122,7 @@ function newNote (req, res, next) {
|
|||
var owner = null
|
||||
var body = ''
|
||||
if (req.body && req.body.length > config.documentMaxLength) {
|
||||
return response.errorTooLong(res)
|
||||
return errorTooLong(res)
|
||||
} else if (req.body) {
|
||||
body = req.body
|
||||
}
|
||||
|
@ -125,7 +130,7 @@ function newNote (req, res, next) {
|
|||
if (req.isAuthenticated()) {
|
||||
owner = req.user.id
|
||||
} else if (!config.allowAnonymous) {
|
||||
return response.errorForbidden(res)
|
||||
return errorForbidden(res)
|
||||
}
|
||||
models.Note.create({
|
||||
ownerId: owner,
|
||||
|
@ -139,7 +144,7 @@ function newNote (req, res, next) {
|
|||
return res.redirect(config.serverURL + '/' + models.Note.encodeNoteId(note.id))
|
||||
}).catch(function (err) {
|
||||
logger.error(err)
|
||||
return response.errorInternalError(res)
|
||||
return errorInternalError(res)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -159,7 +164,7 @@ function findNote (req, res, callback, include) {
|
|||
models.Note.parseNoteId(id, function (err, _id) {
|
||||
if (err) {
|
||||
logger.error(err)
|
||||
return response.errorInternalError(res)
|
||||
return errorInternalError(res)
|
||||
}
|
||||
models.Note.findOne({
|
||||
where: {
|
||||
|
@ -172,17 +177,17 @@ function findNote (req, res, callback, include) {
|
|||
req.alias = noteId
|
||||
return newNote(req, res)
|
||||
} else {
|
||||
return response.errorNotFound(res)
|
||||
return errorNotFound(res)
|
||||
}
|
||||
}
|
||||
if (!checkViewPermission(req, note)) {
|
||||
return response.errorForbidden(res)
|
||||
return errorForbidden(res)
|
||||
} else {
|
||||
return callback(note)
|
||||
}
|
||||
}).catch(function (err) {
|
||||
logger.error(err)
|
||||
return response.errorInternalError(res)
|
||||
return errorInternalError(res)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -213,7 +218,7 @@ function showPublishNote (req, res, next) {
|
|||
}
|
||||
note.increment('viewcount').then(function (note) {
|
||||
if (!note) {
|
||||
return response.errorNotFound(res)
|
||||
return errorNotFound(res)
|
||||
}
|
||||
var body = note.content
|
||||
var extracted = models.Note.extractMeta(body)
|
||||
|
@ -242,7 +247,7 @@ function showPublishNote (req, res, next) {
|
|||
return renderPublish(data, res)
|
||||
}).catch(function (err) {
|
||||
logger.error(err)
|
||||
return response.errorInternalError(res)
|
||||
return errorInternalError(res)
|
||||
})
|
||||
}, include)
|
||||
}
|
||||
|
@ -311,17 +316,22 @@ function actionPDF (req, res, note) {
|
|||
var content = extracted.markdown
|
||||
var title = models.Note.decodeTitle(note.title)
|
||||
|
||||
var highlightCssPath = path.join(config.appRootPath, '/node_modules/highlight.js/styles/github-gist.css')
|
||||
|
||||
if (!fs.existsSync(config.tmpPath)) {
|
||||
fs.mkdirSync(config.tmpPath)
|
||||
}
|
||||
var path = config.tmpPath + '/' + Date.now() + '.pdf'
|
||||
var pdfPath = config.tmpPath + '/' + Date.now() + '.pdf'
|
||||
content = content.replace(/\]\(\//g, '](' + url + '/')
|
||||
markdownpdf().from.string(content).to(path, function () {
|
||||
if (!fs.existsSync(path)) {
|
||||
logger.error('PDF seems to not be generated as expected. File doesn\'t exist: ' + path)
|
||||
return response.errorInternalError(res)
|
||||
var markdownpdfOptions = {
|
||||
highlightCssPath: highlightCssPath
|
||||
}
|
||||
var stream = fs.createReadStream(path)
|
||||
markdownpdf(markdownpdfOptions).from.string(content).to(pdfPath, function () {
|
||||
if (!fs.existsSync(pdfPath)) {
|
||||
logger.error('PDF seems to not be generated as expected. File doesn\'t exist: ' + pdfPath)
|
||||
return errorInternalError(res)
|
||||
}
|
||||
var stream = fs.createReadStream(pdfPath)
|
||||
var filename = title
|
||||
// Be careful of special characters
|
||||
filename = encodeURIComponent(filename)
|
||||
|
@ -331,7 +341,7 @@ function actionPDF (req, res, note) {
|
|||
res.setHeader('Content-Type', 'application/pdf; charset=UTF-8')
|
||||
res.setHeader('X-Robots-Tag', 'noindex, nofollow') // prevent crawling
|
||||
stream.pipe(res)
|
||||
fs.unlinkSync(path)
|
||||
fs.unlinkSync(pdfPath)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -354,10 +364,10 @@ function actionRevision (req, res, note) {
|
|||
models.Revision.getPatchedNoteRevisionByTime(note, time, function (err, content) {
|
||||
if (err) {
|
||||
logger.error(err)
|
||||
return response.errorInternalError(res)
|
||||
return errorInternalError(res)
|
||||
}
|
||||
if (!content) {
|
||||
return response.errorNotFound(res)
|
||||
return errorNotFound(res)
|
||||
}
|
||||
res.set({
|
||||
'Access-Control-Allow-Origin': '*', // allow CORS as API
|
||||
|
@ -369,13 +379,13 @@ function actionRevision (req, res, note) {
|
|||
res.send(content)
|
||||
})
|
||||
} else {
|
||||
return response.errorNotFound(res)
|
||||
return errorNotFound(res)
|
||||
}
|
||||
} else {
|
||||
models.Revision.getNoteRevisions(note, function (err, data) {
|
||||
if (err) {
|
||||
logger.error(err)
|
||||
return response.errorInternalError(res)
|
||||
return errorInternalError(res)
|
||||
}
|
||||
var out = {
|
||||
revision: data
|
||||
|
@ -415,7 +425,7 @@ function noteActions (req, res, next) {
|
|||
actionPDF(req, res, note)
|
||||
} else {
|
||||
logger.error('PDF export failed: Disabled by config. Set "allowPDFExport: true" to enable. Check the documentation for details')
|
||||
response.errorForbidden(res)
|
||||
errorForbidden(res)
|
||||
}
|
||||
break
|
||||
case 'gist':
|
||||
|
@ -480,7 +490,7 @@ function githubActionGist (req, res, note) {
|
|||
var code = req.query.code
|
||||
var state = req.query.state
|
||||
if (!code || !state) {
|
||||
return response.errorForbidden(res)
|
||||
return errorForbidden(res)
|
||||
} else {
|
||||
var data = {
|
||||
client_id: config.github.clientID,
|
||||
|
@ -501,17 +511,17 @@ function githubActionGist (req, res, note) {
|
|||
var title = models.Note.decodeTitle(note.title)
|
||||
var filename = title.replace('/', ' ') + '.md'
|
||||
var gist = {
|
||||
'files': {}
|
||||
files: {}
|
||||
}
|
||||
gist.files[filename] = {
|
||||
'content': content
|
||||
content: content
|
||||
}
|
||||
var gistUrl = 'https://api.github.com/gists'
|
||||
request({
|
||||
url: gistUrl,
|
||||
headers: {
|
||||
'User-Agent': 'CodiMD',
|
||||
'Authorization': 'token ' + accessToken
|
||||
Authorization: 'token ' + accessToken
|
||||
},
|
||||
method: 'POST',
|
||||
json: gist
|
||||
|
@ -520,14 +530,14 @@ function githubActionGist (req, res, note) {
|
|||
res.setHeader('referer', '')
|
||||
res.redirect(body.html_url)
|
||||
} else {
|
||||
return response.errorForbidden(res)
|
||||
return errorForbidden(res)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return response.errorForbidden(res)
|
||||
return errorForbidden(res)
|
||||
}
|
||||
} else {
|
||||
return response.errorForbidden(res)
|
||||
return errorForbidden(res)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -555,7 +565,7 @@ function gitlabActionProjects (req, res, note) {
|
|||
id: req.user.id
|
||||
}
|
||||
}).then(function (user) {
|
||||
if (!user) { return response.errorNotFound(res) }
|
||||
if (!user) { return errorNotFound(res) }
|
||||
var ret = { baseURL: config.gitlab.baseURL, version: config.gitlab.version }
|
||||
ret.accesstoken = user.accessToken
|
||||
ret.profileid = user.profileid
|
||||
|
@ -572,10 +582,10 @@ function gitlabActionProjects (req, res, note) {
|
|||
)
|
||||
}).catch(function (err) {
|
||||
logger.error('gitlab action projects failed: ' + err)
|
||||
return response.errorInternalError(res)
|
||||
return errorInternalError(res)
|
||||
})
|
||||
} else {
|
||||
return response.errorForbidden(res)
|
||||
return errorForbidden(res)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -593,7 +603,7 @@ function showPublishSlide (req, res, next) {
|
|||
if ((note.alias && shortid !== note.alias) || (!note.alias && shortid !== note.shortid)) { return res.redirect(config.serverURL + '/p/' + (note.alias || note.shortid)) }
|
||||
note.increment('viewcount').then(function (note) {
|
||||
if (!note) {
|
||||
return response.errorNotFound(res)
|
||||
return errorNotFound(res)
|
||||
}
|
||||
var body = note.content
|
||||
var extracted = models.Note.extractMeta(body)
|
||||
|
@ -624,7 +634,7 @@ function showPublishSlide (req, res, next) {
|
|||
return renderPublishSlide(data, res)
|
||||
}).catch(function (err) {
|
||||
logger.error(err)
|
||||
return response.errorInternalError(res)
|
||||
return errorInternalError(res)
|
||||
})
|
||||
}, include)
|
||||
}
|
||||
|
@ -635,5 +645,3 @@ function renderPublishSlide (data, res) {
|
|||
})
|
||||
res.render('slide.ejs', data)
|
||||
}
|
||||
|
||||
module.exports = response
|
||||
|
|
|
@ -7,7 +7,7 @@ exports.isSQLite = function isSQLite (sequelize) {
|
|||
}
|
||||
|
||||
exports.getImageMimeType = function getImageMimeType (imagePath) {
|
||||
var fileExtension = /[^.]+$/.exec(imagePath)
|
||||
const fileExtension = /[^.]+$/.exec(imagePath)
|
||||
|
||||
switch (fileExtension[0]) {
|
||||
case 'bmp':
|
||||
|
|
|
@ -6,7 +6,7 @@ const DropboxStrategy = require('passport-dropbox-oauth2').Strategy
|
|||
const config = require('../../../config')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let dropboxAuth = module.exports = Router()
|
||||
const dropboxAuth = module.exports = Router()
|
||||
|
||||
passport.use(new DropboxStrategy({
|
||||
apiVersion: '2',
|
||||
|
|
|
@ -11,7 +11,7 @@ const { setReturnToFromReferer } = require('../utils')
|
|||
const { urlencodedParser } = require('../../utils')
|
||||
const response = require('../../../response')
|
||||
|
||||
let emailAuth = module.exports = Router()
|
||||
const emailAuth = module.exports = Router()
|
||||
|
||||
passport.use(new LocalStrategy({
|
||||
usernameField: 'email'
|
||||
|
|
|
@ -7,7 +7,7 @@ const FacebookStrategy = require('passport-facebook').Strategy
|
|||
const config = require('../../../config')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let facebookAuth = module.exports = Router()
|
||||
const facebookAuth = module.exports = Router()
|
||||
|
||||
passport.use(new FacebookStrategy({
|
||||
clientID: config.facebook.clientID,
|
||||
|
|
|
@ -6,13 +6,21 @@ const GithubStrategy = require('passport-github').Strategy
|
|||
const config = require('../../../config')
|
||||
const response = require('../../../response')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
const { URL } = require('url')
|
||||
|
||||
let githubAuth = module.exports = Router()
|
||||
const githubAuth = module.exports = Router()
|
||||
|
||||
function githubUrl (path) {
|
||||
return config.github.enterpriseURL && new URL(path, config.github.enterpriseURL).toString()
|
||||
}
|
||||
|
||||
passport.use(new GithubStrategy({
|
||||
clientID: config.github.clientID,
|
||||
clientSecret: config.github.clientSecret,
|
||||
callbackURL: config.serverURL + '/auth/github/callback'
|
||||
callbackURL: config.serverURL + '/auth/github/callback',
|
||||
authorizationURL: githubUrl('login/oauth/authorize'),
|
||||
tokenURL: githubUrl('login/oauth/access_token'),
|
||||
userProfileURL: githubUrl('api/v3/user')
|
||||
}, passportGeneralCallback))
|
||||
|
||||
githubAuth.get('/auth/github', function (req, res, next) {
|
||||
|
|
|
@ -6,16 +6,24 @@ const GitlabStrategy = require('passport-gitlab2').Strategy
|
|||
const config = require('../../../config')
|
||||
const response = require('../../../response')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
const HttpsProxyAgent = require('https-proxy-agent')
|
||||
|
||||
let gitlabAuth = module.exports = Router()
|
||||
const gitlabAuth = module.exports = Router()
|
||||
|
||||
passport.use(new GitlabStrategy({
|
||||
const gitlabAuthStrategy = new GitlabStrategy({
|
||||
baseURL: config.gitlab.baseURL,
|
||||
clientID: config.gitlab.clientID,
|
||||
clientSecret: config.gitlab.clientSecret,
|
||||
scope: config.gitlab.scope,
|
||||
callbackURL: config.serverURL + '/auth/gitlab/callback'
|
||||
}, passportGeneralCallback))
|
||||
}, passportGeneralCallback)
|
||||
|
||||
if (process.env['https_proxy']) {
|
||||
const httpsProxyAgent = new HttpsProxyAgent(process.env['https_proxy'])
|
||||
gitlabAuthStrategy._oauth2.setAgent(httpsProxyAgent)
|
||||
}
|
||||
|
||||
passport.use(gitlabAuthStrategy)
|
||||
|
||||
gitlabAuth.get('/auth/gitlab', function (req, res, next) {
|
||||
setReturnToFromReferer(req)
|
||||
|
|
|
@ -6,7 +6,7 @@ var GoogleStrategy = require('passport-google-oauth20').Strategy
|
|||
const config = require('../../../config')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let googleAuth = module.exports = Router()
|
||||
const googleAuth = module.exports = Router()
|
||||
|
||||
passport.use(new GoogleStrategy({
|
||||
clientID: config.google.clientID,
|
||||
|
|
|
@ -10,7 +10,7 @@ const { setReturnToFromReferer } = require('../utils')
|
|||
const { urlencodedParser } = require('../../utils')
|
||||
const response = require('../../../response')
|
||||
|
||||
let ldapAuth = module.exports = Router()
|
||||
const ldapAuth = module.exports = Router()
|
||||
|
||||
passport.use(new LDAPStrategy({
|
||||
server: {
|
||||
|
|
|
@ -8,11 +8,11 @@ const OAuthStrategy = require('passport-oauth2').Strategy
|
|||
const config = require('../../../config')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let mattermostAuth = module.exports = Router()
|
||||
const mattermostAuth = module.exports = Router()
|
||||
|
||||
const mattermostClient = new MattermostClient()
|
||||
|
||||
let mattermostStrategy = new OAuthStrategy({
|
||||
const mattermostStrategy = new OAuthStrategy({
|
||||
authorizationURL: config.mattermost.baseURL + '/oauth/authorize',
|
||||
tokenURL: config.mattermost.baseURL + '/oauth/access_token',
|
||||
clientID: config.mattermost.clientID,
|
||||
|
|
|
@ -6,7 +6,7 @@ const { Strategy, InternalOAuthError } = require('passport-oauth2')
|
|||
const config = require('../../../config')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let oauth2Auth = module.exports = Router()
|
||||
const oauth2Auth = module.exports = Router()
|
||||
|
||||
class OAuth2CustomStrategy extends Strategy {
|
||||
constructor (options, verify) {
|
||||
|
@ -31,7 +31,7 @@ class OAuth2CustomStrategy extends Strategy {
|
|||
return done(new Error('Failed to parse user profile'))
|
||||
}
|
||||
|
||||
let profile = parseProfile(json)
|
||||
const profile = parseProfile(json)
|
||||
profile.provider = 'oauth2'
|
||||
|
||||
done(null, profile)
|
||||
|
@ -76,7 +76,7 @@ OAuth2CustomStrategy.prototype.userProfile = function (accessToken, done) {
|
|||
return done(new Error('Failed to parse user profile'))
|
||||
}
|
||||
|
||||
let profile = parseProfile(json)
|
||||
const profile = parseProfile(json)
|
||||
profile.provider = 'oauth2'
|
||||
|
||||
done(null, profile)
|
||||
|
|
|
@ -9,7 +9,7 @@ const logger = require('../../../logger')
|
|||
const { urlencodedParser } = require('../../utils')
|
||||
const { setReturnToFromReferer } = require('../utils')
|
||||
|
||||
let openIDAuth = module.exports = Router()
|
||||
const openIDAuth = module.exports = Router()
|
||||
|
||||
passport.use(new OpenIDStrategy({
|
||||
returnURL: config.serverURL + '/auth/openid/callback',
|
||||
|
|
|
@ -10,7 +10,7 @@ const { urlencodedParser } = require('../../utils')
|
|||
const fs = require('fs')
|
||||
const intersection = function (array1, array2) { return array1.filter((n) => array2.includes(n)) }
|
||||
|
||||
let samlAuth = module.exports = Router()
|
||||
const samlAuth = module.exports = Router()
|
||||
|
||||
passport.use(new SamlStrategy({
|
||||
callbackUrl: config.serverURL + '/auth/saml/callback',
|
||||
|
|
|
@ -7,7 +7,7 @@ const TwitterStrategy = require('passport-twitter').Strategy
|
|||
const config = require('../../../config')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let twitterAuth = module.exports = Router()
|
||||
const twitterAuth = module.exports = Router()
|
||||
|
||||
passport.use(new TwitterStrategy({
|
||||
consumerKey: config.twitter.consumerKey,
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
'use strict'
|
||||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
|
||||
const lutim = require('lutim')
|
||||
|
||||
exports.uploadImage = function (imagePath, callback) {
|
||||
if (!imagePath || typeof imagePath !== 'string') {
|
||||
callback(new Error('Image path is missing or wrong'), null)
|
||||
return
|
||||
}
|
||||
|
||||
if (!callback || typeof callback !== 'function') {
|
||||
logger.error('Callback has to be a function')
|
||||
return
|
||||
}
|
||||
|
||||
if (config.lutim && config.lutim.url) {
|
||||
lutim.setAPIUrl(config.lutim.url)
|
||||
}
|
||||
|
||||
lutim.uploadImage(imagePath)
|
||||
.then(function (json) {
|
||||
if (config.debug) {
|
||||
logger.info('SERVER uploadimage success: ' + JSON.stringify(json))
|
||||
}
|
||||
callback(null, lutim.getAPIUrl() + json.msg.short)
|
||||
}).catch(function (err) {
|
||||
callback(new Error(err), null)
|
||||
})
|
||||
}
|
|
@ -32,16 +32,16 @@ exports.uploadImage = function (imagePath, callback) {
|
|||
return
|
||||
}
|
||||
|
||||
let key = path.join('uploads', path.basename(imagePath))
|
||||
let protocol = config.minio.secure ? 'https' : 'http'
|
||||
const key = path.join('uploads', path.basename(imagePath))
|
||||
const protocol = config.minio.secure ? 'https' : 'http'
|
||||
|
||||
minioClient.putObject(config.s3bucket, key, buffer, buffer.size, getImageMimeType(imagePath), function (err, data) {
|
||||
if (err) {
|
||||
callback(new Error(err), null)
|
||||
return
|
||||
}
|
||||
let hidePort = [80, 443].includes(config.minio.port)
|
||||
let urlPort = hidePort ? '' : `:${config.minio.port}`
|
||||
const hidePort = [80, 443].includes(config.minio.port)
|
||||
const urlPort = hidePort ? '' : `:${config.minio.port}`
|
||||
callback(null, `${protocol}://${config.minio.endPoint}${urlPort}/${config.s3bucket}/${key}`)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -26,7 +26,7 @@ exports.uploadImage = function (imagePath, callback) {
|
|||
callback(new Error(err), null)
|
||||
return
|
||||
}
|
||||
let params = {
|
||||
const params = {
|
||||
Bucket: config.s3bucket,
|
||||
Key: path.join('uploads', path.basename(imagePath)),
|
||||
Body: buffer
|
||||
|
|
|
@ -5,7 +5,7 @@ const toobusy = require('toobusy-js')
|
|||
const config = require('../../config')
|
||||
const response = require('../../response')
|
||||
|
||||
toobusy.maxLag(config.responseMaxLag);
|
||||
toobusy.maxLag(config.responseMaxLag)
|
||||
|
||||
module.exports = function (req, res, next) {
|
||||
if (toobusy()) {
|
||||
|
|
|
@ -97,8 +97,11 @@ statusRouter.get('/config', function (req, res) {
|
|||
urlpath: config.urlPath,
|
||||
debug: config.debug,
|
||||
version: config.fullversion,
|
||||
plantumlServer: config.plantuml.server,
|
||||
DROPBOX_APP_KEY: config.dropbox.appKey,
|
||||
allowedUploadMimeTypes: config.allowedUploadMimeTypes
|
||||
allowedUploadMimeTypes: config.allowedUploadMimeTypes,
|
||||
defaultUseHardbreak: config.defaultUseHardbreak,
|
||||
linkifyHeaderStyle: config.linkifyHeaderStyle
|
||||
}
|
||||
res.set({
|
||||
'Cache-Control': 'private', // only cache by client
|
||||
|
|
|
@ -69,8 +69,7 @@ UserRouter.get('/me/delete/:token?', function (req, res) {
|
|||
// export the data of the authenticated user
|
||||
UserRouter.get('/me/export', function (req, res) {
|
||||
if (req.isAuthenticated()) {
|
||||
// let output = fs.createWriteStream(__dirname + '/example.zip');
|
||||
let archive = archiver('zip', {
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 3 } // Sets the compression level.
|
||||
})
|
||||
res.setHeader('Content-Type', 'application/zip')
|
||||
|
@ -90,14 +89,14 @@ UserRouter.get('/me/export', function (req, res) {
|
|||
ownerId: user.id
|
||||
}
|
||||
}).then(function (notes) {
|
||||
let filenames = {}
|
||||
const filenames = {}
|
||||
async.each(notes, function (note, callback) {
|
||||
let basename = note.title.replace(/\//g, '-') // Prevent subdirectories
|
||||
const basename = note.title.replace(/\//g, '-') // Prevent subdirectories
|
||||
let filename
|
||||
let suffix = ''
|
||||
let suffix = 0
|
||||
do {
|
||||
let seperator = typeof suffix === 'number' ? '-' : ''
|
||||
filename = basename + seperator + suffix + '.md'
|
||||
const separator = suffix === 0 ? '' : '-'
|
||||
filename = basename + separator + suffix + '.md'
|
||||
suffix++
|
||||
} while (filenames[filename])
|
||||
filenames[filename] = true
|
||||
|
|
|
@ -13,7 +13,7 @@ process.on('message', function (data) {
|
|||
}
|
||||
switch (data.msg) {
|
||||
case 'create patch':
|
||||
if (!data.hasOwnProperty('lastDoc') || !data.hasOwnProperty('currDoc')) {
|
||||
if (!Object.hasOwnProperty.call(data, 'lastDoc') || !Object.hasOwnProperty.call(data, 'currDoc')) {
|
||||
return logger.error('dmp worker error: not enough data on create patch')
|
||||
}
|
||||
try {
|
||||
|
@ -33,7 +33,7 @@ process.on('message', function (data) {
|
|||
}
|
||||
break
|
||||
case 'get revision':
|
||||
if (!data.hasOwnProperty('revisions') || !data.hasOwnProperty('count')) {
|
||||
if (!Object.hasOwnProperty.call(data, 'revisions') || !Object.hasOwnProperty.call(data, 'count')) {
|
||||
return logger.error('dmp worker error: not enough data on get revision')
|
||||
}
|
||||
try {
|
||||
|
@ -77,12 +77,12 @@ function getRevision (revisions, count) {
|
|||
if (count <= Math.round(revisions.length / 2)) {
|
||||
// start from top to target
|
||||
for (let i = 0; i < count; i++) {
|
||||
let revision = revisions[i]
|
||||
const revision = revisions[i]
|
||||
if (i === 0) {
|
||||
startContent = revision.content || revision.lastContent
|
||||
}
|
||||
if (i !== count - 1) {
|
||||
let patch = dmp.patch_fromText(revision.patch)
|
||||
const patch = dmp.patch_fromText(revision.patch)
|
||||
applyPatches = applyPatches.concat(patch)
|
||||
}
|
||||
lastPatch = revision.patch
|
||||
|
@ -99,13 +99,13 @@ function getRevision (revisions, count) {
|
|||
// start from bottom to target
|
||||
var l = revisions.length - 1
|
||||
for (var i = l; i >= count - 1; i--) {
|
||||
let revision = revisions[i]
|
||||
const revision = revisions[i]
|
||||
if (i === l) {
|
||||
startContent = revision.lastContent
|
||||
authorship = revision.authorship
|
||||
}
|
||||
if (revision.patch) {
|
||||
let patch = dmp.patch_fromText(revision.patch)
|
||||
const patch = dmp.patch_fromText(revision.patch)
|
||||
applyPatches = applyPatches.concat(patch)
|
||||
}
|
||||
lastPatch = revision.patch
|
||||
|
|
177
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "codimd",
|
||||
"version": "1.3.1",
|
||||
"version": "1.4.0",
|
||||
"description": "Realtime collaborative markdown notes on all platforms.",
|
||||
"keywords": [
|
||||
"Collaborative",
|
||||
|
@ -15,72 +15,78 @@
|
|||
"license": "AGPL-3.0",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.prod.js --progress --colors --bail",
|
||||
"build": "webpack --config webpack.prod.js --display errors-only -p",
|
||||
"dev": "webpack --config webpack.dev.js --progress --colors --watch",
|
||||
"doctoc": "doctoc --title='# Table of Contents' README.md",
|
||||
"eslint": "eslint lib public test app.js",
|
||||
"postinstall": "bin/heroku",
|
||||
"jsonlint": "find . -not -path './node_modules/*' -type f -name '*.json' -o -type f -name '*.json.example' | while read json; do echo $json ; jq . $json; done",
|
||||
"standard": "echo 'standard is no longer being used, use `npm run eslint` instead!' && exit 1",
|
||||
"lint": "standard",
|
||||
"jsonlint": "find . -type f -not -ipath \"./node_modules/*\" \\( -name \"*.json\" -o -name \"*.json.*\" \\) | xargs -n 1 -I{} -- bash -c 'echo {}; jq . {} > /dev/null;'",
|
||||
"start": "sequelize db:migrate && node app.js",
|
||||
"test": "npm run-script eslint && npm run-script jsonlint && mocha"
|
||||
"mocha": "mocha --require intelli-espower-loader --exit ./test --recursive",
|
||||
"mocha:ci": "mocha --no-color -R dot --require intelli-espower-loader --exit ./test --recursive",
|
||||
"coverage": "nyc mocha --require intelli-espower-loader --exit --recursive ./test",
|
||||
"coverage:ci": "nyc mocha --no-color -R dot --require intelli-espower-loader --exit --recursive ./test",
|
||||
"test": "npm run-script lint && npm run-script jsonlint && npm run-script coverage",
|
||||
"test:ci": "npm run-script lint && npm run-script jsonlint && npm run-script coverage:ci",
|
||||
"postinstall": "bin/heroku"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hackmd/codemirror": "^5.46.2",
|
||||
"@hackmd/diff-match-patch": "~1.1.1",
|
||||
"@hackmd/codemirror": "~5.46.2",
|
||||
"@hackmd/diff-match-patch": "~1.1.3",
|
||||
"@hackmd/idle-js": "~1.0.1",
|
||||
"@hackmd/imgur": "~0.4.1",
|
||||
"@hackmd/js-sequence-diagrams": "~0.0.1-alpha.3",
|
||||
"@hackmd/lz-string": "~1.4.4",
|
||||
"@hackmd/meta-marked": "~0.4.4",
|
||||
"@passport-next/passport-openid": "~1.0.0",
|
||||
"archiver": "~2.1.1",
|
||||
"async": "~2.1.4",
|
||||
"aws-sdk": "~2.345.0",
|
||||
"azure-storage": "~2.10.2",
|
||||
"base64url": "~3.0.0",
|
||||
"body-parser": "~1.18.3",
|
||||
"@susisu/mte-kernel": "^2.1.0",
|
||||
"archiver": "~3.1.1",
|
||||
"async": "~3.1.0",
|
||||
"aws-sdk": "~2.503.0",
|
||||
"azure-storage": "~2.10.3",
|
||||
"babel-polyfill": "~6.26.0",
|
||||
"base64url": "~3.0.1",
|
||||
"body-parser": "~1.19.0",
|
||||
"bootstrap": "~3.4.0",
|
||||
"bootstrap-validator": "~0.11.8",
|
||||
"chance": "~1.0.4",
|
||||
"chance": "~1.0.18",
|
||||
"cheerio": "~0.22.0",
|
||||
"compression": "~1.7.4",
|
||||
"connect-flash": "~0.1.1",
|
||||
"connect-session-sequelize": "~6.0.0",
|
||||
"cookie": "~0.3.1",
|
||||
"cookie-parser": "~1.4.3",
|
||||
"cookie": "~0.4.0",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"deep-freeze": "~0.0.1",
|
||||
"ejs": "~2.5.5",
|
||||
"emojify.js": "~1.1.0",
|
||||
"express": "~4.16.4",
|
||||
"express-session": "~1.16.1",
|
||||
"file-saver": "~1.3.3",
|
||||
"flowchart.js": "~1.12.0",
|
||||
"fork-awesome": "~1.1.3",
|
||||
"ejs": "~2.6.2",
|
||||
"express": "~4.17.1",
|
||||
"express-session": "~1.16.2",
|
||||
"file-saver": "~2.0.2",
|
||||
"flowchart.js": "~1.12.2",
|
||||
"fork-awesome": "~1.1.7",
|
||||
"formidable": "~1.2.1",
|
||||
"gist-embed": "~2.6.0",
|
||||
"graceful-fs": "~4.1.11",
|
||||
"handlebars": "~4.0.13",
|
||||
"helmet": "~3.13.0",
|
||||
"highlight.js": "~9.12.0",
|
||||
"graceful-fs": "~4.2.1",
|
||||
"handlebars": "~4.1.2",
|
||||
"helmet": "~3.20.0",
|
||||
"highlight.js": "~9.15.9",
|
||||
"https-proxy-agent": "^3.0.1",
|
||||
"i18n": "~0.8.3",
|
||||
"ionicons": "~2.0.1",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"jquery": "~3.1.1",
|
||||
"isomorphic-fetch": "~2.2.1",
|
||||
"jquery": "~3.4.1",
|
||||
"jquery-mousewheel": "~3.1.13",
|
||||
"jquery-ui": "~1.12.1",
|
||||
"js-cookie": "~2.1.3",
|
||||
"js-cookie": "~2.2.0",
|
||||
"js-yaml": "~3.13.1",
|
||||
"jsdom-nogyp": "~0.8.3",
|
||||
"keymaster": "~1.6.2",
|
||||
"list.js": "~1.5.0",
|
||||
"lodash": "~4.17.11",
|
||||
"markdown-it": "~8.2.2",
|
||||
"lodash": "~4.17.15",
|
||||
"lutim": "~1.0.2",
|
||||
"markdown-it": "~9.0.1",
|
||||
"markdown-it-abbr": "~1.0.4",
|
||||
"markdown-it-container": "~2.0.0",
|
||||
"markdown-it-deflist": "~2.0.1",
|
||||
"markdown-it-emoji": "~1.3.0",
|
||||
"markdown-it-footnote": "~3.0.1",
|
||||
"markdown-it-deflist": "~2.0.3",
|
||||
"markdown-it-emoji": "~1.4.0",
|
||||
"markdown-it-footnote": "~3.0.2",
|
||||
"markdown-it-imsize": "~2.0.1",
|
||||
"markdown-it-ins": "~2.0.0",
|
||||
"markdown-it-mark": "~2.0.0",
|
||||
|
@ -89,15 +95,16 @@
|
|||
"markdown-it-sub": "~1.0.0",
|
||||
"markdown-it-sup": "~1.0.0",
|
||||
"markdown-pdf": "~9.0.0",
|
||||
"mathjax": "~2.7.0",
|
||||
"mattermost-redux": "^5.9.0",
|
||||
"mermaid": "~7.1.0",
|
||||
"method-override": "~2.3.7",
|
||||
"markdownlint": "^0.17.0",
|
||||
"mathjax": "~2.7.5",
|
||||
"mattermost-redux": "~5.13.0",
|
||||
"mermaid": "~8.2.3",
|
||||
"method-override": "~3.0.0",
|
||||
"minimist": "~1.2.0",
|
||||
"minio": "~6.0.0",
|
||||
"minio": "~7.0.10",
|
||||
"moment": "~2.24.0",
|
||||
"morgan": "~1.9.1",
|
||||
"mysql": "~2.16.0",
|
||||
"mysql": "~2.17.1",
|
||||
"passport": "~0.4.0",
|
||||
"passport-dropbox-oauth2": "~1.1.0",
|
||||
"passport-facebook": "~2.1.1",
|
||||
|
@ -110,11 +117,12 @@
|
|||
"passport-saml": "~1.0.0",
|
||||
"passport-twitter": "~1.0.4",
|
||||
"passport.socketio": "~3.7.0",
|
||||
"pdfobject": "~2.0.201604172",
|
||||
"pdfobject": "~2.1.1",
|
||||
"pg": "~6.1.2",
|
||||
"pg-hstore": "~2.3.2",
|
||||
"prismjs": "~1.6.0",
|
||||
"randomcolor": "~0.5.3",
|
||||
"plantuml-encoder": "^1.2.5",
|
||||
"prismjs": "~1.17.1",
|
||||
"randomcolor": "~0.5.4",
|
||||
"raphael": "~2.2.8",
|
||||
"readline-sync": "~1.4.7",
|
||||
"request": "~2.88.0",
|
||||
|
@ -122,64 +130,67 @@
|
|||
"scrypt": "~6.0.3",
|
||||
"select2": "~3.5.2-browserify",
|
||||
"sequelize": "5.3.5",
|
||||
"shortid": "~2.2.8",
|
||||
"socket.io": "~2.1.1",
|
||||
"socket.io-client": "~2.1.1",
|
||||
"spin.js": "~2.3.2",
|
||||
"sqlite3": "~4.0.1",
|
||||
"sequelize-cli": "~5.4.0",
|
||||
"shortid": "~2.2.14",
|
||||
"socket.io": "~2.2.0",
|
||||
"socket.io-client": "~2.2.0",
|
||||
"spin.js": "~4.0.0",
|
||||
"sqlite3": "~4.0.9",
|
||||
"store": "~2.0.12",
|
||||
"tedious": "~6.1.0",
|
||||
"tedious": "~6.2.0",
|
||||
"toobusy-js": "~0.5.1",
|
||||
"turndown": "~5.0.1",
|
||||
"uuid": "~3.1.0",
|
||||
"validator": "~10.4.0",
|
||||
"velocity-animate": "~1.4.0",
|
||||
"visibilityjs": "~1.2.4",
|
||||
"viz.js": "~1.7.0",
|
||||
"winston": "~3.1.0",
|
||||
"ws": "~6.0.0",
|
||||
"turndown": "~5.0.3",
|
||||
"uuid": "~3.3.2",
|
||||
"validator": "~11.1.0",
|
||||
"vega": "~5.4.0",
|
||||
"vega-embed": "~4.2.2",
|
||||
"vega-lite": "~3.4.0",
|
||||
"velocity-animate": "~1.5.2",
|
||||
"visibilityjs": "~2.0.2",
|
||||
"viz.js": "~2.1.2",
|
||||
"winston": "~3.2.1",
|
||||
"ws": "~7.1.1",
|
||||
"wurl": "~2.5.3",
|
||||
"xss": "~1.0.3"
|
||||
"xss": "~1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hackmd/emojify.js": "^2.1.0",
|
||||
"acorn": "~6.1.1",
|
||||
"babel-core": "~6.26.3",
|
||||
"babel-loader": "~7.1.4",
|
||||
"babel-plugin-transform-runtime": "~6.23.0",
|
||||
"babel-polyfill": "~6.26.0",
|
||||
"babel-preset-env": "~1.7.0",
|
||||
"babel-runtime": "~6.26.0",
|
||||
"copy-webpack-plugin": "~4.5.2",
|
||||
"css-loader": "~1.0.0",
|
||||
"doctoc": "~1.4.0",
|
||||
"ejs-loader": "~0.3.1",
|
||||
"eslint": "~5.16.0",
|
||||
"eslint-config-standard": "~12.0.0",
|
||||
"eslint-plugin-import": "~2.17.1",
|
||||
"eslint-plugin-node": "~8.0.1",
|
||||
"eslint-plugin-promise": "~4.1.1",
|
||||
"eslint-plugin-standard": "~4.0.0",
|
||||
"exports-loader": "~0.7.0",
|
||||
"expose-loader": "~0.7.5",
|
||||
"file-loader": "~2.0.0",
|
||||
"html-webpack-plugin": "~4.0.0-beta.2",
|
||||
"imports-loader": "~0.8.0",
|
||||
"intelli-espower-loader": "~1.0.1",
|
||||
"jsonlint": "~1.6.2",
|
||||
"less": "~3.9.0",
|
||||
"less-loader": "~4.1.0",
|
||||
"markdown-it-ruby": "^0.1.1",
|
||||
"mini-css-extract-plugin": "~0.4.1",
|
||||
"mocha": "~5.2.0",
|
||||
"mock-require": "~3.0.3",
|
||||
"nyc": "~14.0.0",
|
||||
"optimize-css-assets-webpack-plugin": "~5.0.0",
|
||||
"power-assert": "~1.6.1",
|
||||
"script-loader": "~0.7.2",
|
||||
"sequelize-cli": "~5.4.0",
|
||||
"sinon": "~7.3.2",
|
||||
"standard": "~13.1.0",
|
||||
"string-loader": "~0.0.1",
|
||||
"style-loader": "~0.21.0",
|
||||
"style-loader": "~0.23.1",
|
||||
"uglifyjs-webpack-plugin": "~1.2.7",
|
||||
"url-loader": "~1.0.1",
|
||||
"webpack": "~4.30.0",
|
||||
"webpack-cli": "~3.3.0",
|
||||
"webpack-merge": "~4.1.4",
|
||||
"webpack": "~4.39.0",
|
||||
"webpack-cli": "~3.3.6",
|
||||
"webpack-merge": "~4.2.1",
|
||||
"webpack-parallel-uglify-plugin": "~1.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
@ -187,7 +198,7 @@
|
|||
"utf-8-validate": "~5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
"node": ">=8.0.0 <12.0.0"
|
||||
},
|
||||
"maintainers": [
|
||||
{
|
||||
|
@ -198,5 +209,21 @@
|
|||
"name": "Christoph (Sheogorath) Kern",
|
||||
"email": "codimd@sheogorath.shivering-isles.com"
|
||||
}
|
||||
],
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"/public/build",
|
||||
"/public/vendor",
|
||||
"/lib/ot",
|
||||
"webpack.*"
|
||||
]
|
||||
},
|
||||
"nyc": {
|
||||
"all": true,
|
||||
"include": [
|
||||
"app.js",
|
||||
"lib/**/*.js"
|
||||
],
|
||||
"reporter": "lcov"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ module.exports = {
|
|||
"moment": false,
|
||||
"editor": false,
|
||||
"ui": false,
|
||||
"Spinner": false,
|
||||
"modeType": false,
|
||||
"serverurl": false,
|
||||
"key": false,
|
||||
|
|
|
@ -25,6 +25,32 @@ body.night{
|
|||
border: 1px solid #343434;
|
||||
}
|
||||
|
||||
.toolbar > .btn-toolbar {
|
||||
white-space: nowrap;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.toolbar > .btn-toolbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toolbar > .btn-toolbar > .btn-group {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.toolbar > .btn-toolbar > .btn-group > span {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
color: #fff;
|
||||
padding: 5px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.toolbar > .btn-toolbar > .btn-group > span.separator {
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
.toolbar > .btn-toolbar > .btn-group > .btn {
|
||||
background-color: #1c1c1e;
|
||||
padding: 5px;
|
||||
|
@ -513,6 +539,7 @@ div[contenteditable]:empty:not(:focus):before{
|
|||
.status-bar .status-indicators .status-keymap > a,
|
||||
.status-bar .status-indicators .status-theme > a,
|
||||
.status-bar .status-indicators .status-spellcheck > a,
|
||||
.status-bar .status-indicators .status-linter > a,
|
||||
.status-bar .status-indicators .status-preferences > a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
@ -520,6 +547,7 @@ div[contenteditable]:empty:not(:focus):before{
|
|||
|
||||
.status-bar .status-indicators .status-theme,
|
||||
.status-bar .status-indicators .status-spellcheck,
|
||||
.status-bar .status-indicators .status-linter,
|
||||
.status-bar .status-indicators .status-preferences {
|
||||
padding: 0 4.3px;
|
||||
}
|
||||
|
@ -540,17 +568,20 @@ div[contenteditable]:empty:not(:focus):before{
|
|||
}
|
||||
|
||||
.ui-theme-toggle,
|
||||
.ui-linter-toggle,
|
||||
.ui-spellcheck-toggle {
|
||||
opacity: 0.2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui-theme-toggle.active,
|
||||
.ui-linter-toggle.active,
|
||||
.ui-spellcheck-toggle.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ui-theme-toggle:hover,
|
||||
.ui-linter-toggle:hover,
|
||||
.ui-spellcheck-toggle:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
|
|
@ -126,7 +126,8 @@
|
|||
.markdown-body pre.sequence-diagram,
|
||||
.markdown-body pre.graphviz,
|
||||
.markdown-body pre.mermaid,
|
||||
.markdown-body pre.abc {
|
||||
.markdown-body pre.abc,
|
||||
.markdown-body pre.vega {
|
||||
text-align: center;
|
||||
background-color: inherit;
|
||||
border-radius: 0;
|
||||
|
@ -147,7 +148,8 @@
|
|||
.markdown-body pre.sequence-diagram > code,
|
||||
.markdown-body pre.graphviz > code,
|
||||
.markdown-body pre.mermaid > code,
|
||||
.markdown-body pre.abc > code {
|
||||
.markdown-body pre.abc > code,
|
||||
.markdown-body pre.vega > code {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
@ -155,7 +157,8 @@
|
|||
.markdown-body pre.sequence-diagram > svg,
|
||||
.markdown-body pre.graphviz > svg,
|
||||
.markdown-body pre.mermaid > svg,
|
||||
.markdown-body pre.abc > svg {
|
||||
.markdown-body pre.abc > svg,
|
||||
.markdown-body pre.vega > svg {
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -223,7 +223,8 @@ pre.flow-chart,
|
|||
pre.sequence-diagram,
|
||||
pre.graphviz,
|
||||
pre.mermaid,
|
||||
pre.abc {
|
||||
pre.abc,
|
||||
pre.vega {
|
||||
text-align: center;
|
||||
background-color: white;
|
||||
border-radius: 0;
|
||||
|
@ -234,7 +235,8 @@ pre.flow-chart > code,
|
|||
pre.sequence-diagram > code,
|
||||
pre.graphviz > code,
|
||||
pre.mermaid > code,
|
||||
pre.abc > code {
|
||||
pre.abc > code,
|
||||
pre.vega > code {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
@ -242,7 +244,8 @@ pre.flow-chart > svg,
|
|||
pre.sequence-diagram > svg,
|
||||
pre.graphviz > svg,
|
||||
pre.mermaid > svg,
|
||||
pre.abc > svg {
|
||||
pre.abc > svg,
|
||||
pre.vega > svg {
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -255,19 +258,10 @@ pre.abc > svg {
|
|||
margin-bottom: -.25em !important;
|
||||
}
|
||||
|
||||
.reveal .slides, .reveal .backgrounds, .reveal.overview {
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.slides, #meta {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.reveal .slides > section,
|
||||
.reveal .slides > section > section {
|
||||
transform-style: flat;
|
||||
}
|
||||
|
||||
.reveal.rtl .slides,
|
||||
.reveal.rtl .slides h1,
|
||||
.reveal.rtl .slides h2,
|
||||
|
|
|
@ -313,11 +313,45 @@ GABc dedB|dedB dedB|c2ec B2dB|A2F2 G4:|
|
|||
g2gf g2Bd|g2f2 e2d2|c2ec B2dB|A2F2 G4:|
|
||||
```
|
||||
|
||||
### PlantUML
|
||||
```plantuml
|
||||
start
|
||||
if (condition A) then (yes)
|
||||
:Text 1;
|
||||
elseif (condition B) then (yes)
|
||||
:Text 2;
|
||||
stop
|
||||
elseif (condition C) then (yes)
|
||||
:Text 3;
|
||||
elseif (condition D) then (yes)
|
||||
:Text 4;
|
||||
else (nothing)
|
||||
:Text else;
|
||||
endif
|
||||
stop
|
||||
```
|
||||
|
||||
### Vega-Lite
|
||||
```vega
|
||||
{
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v3.json",
|
||||
"data": {"url": "https://vega.github.io/editor/data/barley.json"},
|
||||
"mark": "bar",
|
||||
"encoding": {
|
||||
"x": {"aggregate": "sum", "field": "yield", "type": "quantitative"},
|
||||
"y": {"field": "variety", "type": "nominal"},
|
||||
"color": {"field": "site", "type": "nominal"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> More information about **sequence diagrams** syntax [here](http://bramp.github.io/js-sequence-diagrams/).
|
||||
> More information about **flow charts** syntax [here](http://adrai.github.io/flowchart.js/).
|
||||
> More information about **graphviz** syntax [here](http://www.tonyballantyne.com/graphs.html)
|
||||
> More information about **mermaid** syntax [here](http://knsv.github.io/mermaid)
|
||||
> More information about **abc** syntax [here](http://abcnotation.com/learn)
|
||||
> More information about **plantuml** syntax [here](http://plantuml.com/index)
|
||||
> More information about **vega** syntax [here](https://vega.github.io/vega-lite/docs)
|
||||
|
||||
Alert Area
|
||||
---
|
||||
|
@ -337,6 +371,10 @@ Watch out :zap:
|
|||
Oh No! :fire:
|
||||
:::
|
||||
|
||||
:::spoiler Click to show details
|
||||
You found me :stuck_out_tongue_winking_eye:
|
||||
:::
|
||||
|
||||
## Typography
|
||||
|
||||
### Headers
|
||||
|
@ -397,6 +435,7 @@ Subscript: H~2~O
|
|||
|
||||
==Marked text==
|
||||
|
||||
{ruby base|rubytext}
|
||||
|
||||
### Blockquotes
|
||||
|
||||
|
|
|
@ -1,6 +1,36 @@
|
|||
Release Notes
|
||||
===
|
||||
|
||||
<i class="fa fa-tag"></i> 1.4.0 Syrmaticus mikado <i class="fa fa-clock-o"></i> 2019-11-11
|
||||
---
|
||||
|
||||
<div style="text-align: center; margin-bottom: 1em;">
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/6/60/Mikado_Pheasant_398.jpg" width="300">
|
||||
<small style="display: block;">Mikado Pheasant, photo credits to <a href="https://zh.wikipedia.org/wiki/File:Mikado_Pheasant_398.jpg">Snowyowls from wikipedia</a></small>
|
||||
</div>
|
||||
|
||||
Starting from version 1.4.0, we'll pick one species from [_the endemic species of Taiwan_](https://en.wikipedia.org/wiki/List_of_endemic_species_of_Taiwan) as version name. Is there anyone still remember we've once used type of coffee as our version name? It's time to revive that good convention, but this time we don't need coffee to stay up all night. 💤
|
||||
|
||||
It has been over 200+ commits since our last release. These are the highlights from version 1.4.0:
|
||||
|
||||
- [New table tools][table-tools] - Create table with auto-formatting and keyboard shortcut
|
||||
- [Markdownlint integration][markdownlint] - Lint you markdown document
|
||||
- [Support PlantUML, vega-lite renderer][more-renderers] - More renderers to come
|
||||
- [Support spoiler container, ruby markdown syntax][more-syntax]
|
||||
- [New Emoji sets][new-emoji]
|
||||
- [Slide mode plugins][slide-mode-plugins]: Elapsed time bar and Spotlight
|
||||
|
||||
[Go read the complete release note here][v1_4_0]. Thank you CodiMD community and all our contributors. ❤️
|
||||
|
||||
[table-tools]: https://hackmd.io/@codimd/v1_4_0#New-Table-Tools
|
||||
[markdownlint]: https://hackmd.io/@codimd/v1_4_0#Markdownlint-integration
|
||||
[more-renderers]: https://hackmd.io/@codimd/v1_4_0#Support-2-new-render-engines-PlantUML-and-Vega-lite
|
||||
[more-syntax]: https://hackmd.io/@codimd/v1_4_0#Suppport-2-New-markdown-syntax-Spoiler-and-Ruby
|
||||
[new-emoji]: https://hackmd.io/@codimd/v1_4_0#New-emoji-sets
|
||||
[slide-mode-plugins]: https://hackmd.io/@codimd/v1_4_0#Slide-mode-enhancement
|
||||
|
||||
[v1_4_0]: https://hackmd.io/@codimd/release-notes/%2F%40codimd%2Fv1_4_0
|
||||
|
||||
<i class="fa fa-tag"></i> 1.3.1 <i class="fa fa-clock-o"></i> 2019-03-23 00:00
|
||||
---
|
||||
|
||||
|
|
After Width: | Height: | Size: 193 B |
After Width: | Height: | Size: 126 B |
After Width: | Height: | Size: 215 B |
After Width: | Height: | Size: 194 B |
After Width: | Height: | Size: 233 B |
|
@ -1,5 +1,5 @@
|
|||
/* eslint-env browser, jquery */
|
||||
/* global moment, serverurl */
|
||||
/* global moment, serverurl, plantumlServer */
|
||||
|
||||
import Prism from 'prismjs'
|
||||
import hljs from 'highlight.js'
|
||||
|
@ -30,7 +30,9 @@ require('prismjs/components/prism-gherkin')
|
|||
|
||||
require('./lib/common/login')
|
||||
require('../vendor/md-toc')
|
||||
var Viz = require('viz.js')
|
||||
const viz = new window.Viz()
|
||||
const plantumlEncoder = require('plantuml-encoder')
|
||||
|
||||
const ui = getUIElements()
|
||||
|
||||
// auto update last change
|
||||
|
@ -166,11 +168,11 @@ export function renderTags (view) {
|
|||
}
|
||||
|
||||
function slugifyWithUTF8 (text) {
|
||||
// remove html tags and trim spaces
|
||||
// remove HTML tags and trim spaces
|
||||
let newText = stripTags(text.toString().trim())
|
||||
// replace all spaces in between to dashes
|
||||
// replace space between words with dashes
|
||||
newText = newText.replace(/\s+/g, '-')
|
||||
// slugify string to make it valid for attribute
|
||||
// slugify string to make it valid as an attribute
|
||||
newText = newText.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '')
|
||||
return newText
|
||||
}
|
||||
|
@ -193,7 +195,7 @@ export function isValidURL (str) {
|
|||
export function parseMeta (md, edit, view, toc, tocAffix) {
|
||||
let lang = null
|
||||
let dir = null
|
||||
let breaks = true
|
||||
let breaks = window.defaultUseHardbreak
|
||||
if (md && md.meta) {
|
||||
const meta = md.meta
|
||||
lang = meta.lang
|
||||
|
@ -223,10 +225,10 @@ export function parseMeta (md, edit, view, toc, tocAffix) {
|
|||
tocAffix.removeAttr('dir')
|
||||
}
|
||||
// breaks
|
||||
if (typeof breaks === 'boolean' && !breaks) {
|
||||
md.options.breaks = false
|
||||
if (typeof breaks === 'boolean') {
|
||||
md.options.breaks = breaks
|
||||
} else {
|
||||
md.options.breaks = true
|
||||
md.options.breaks = window.defaultUseHardbreak
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -348,7 +350,7 @@ export function finishView (view) {
|
|||
$value.html('')
|
||||
chart.drawSVG(value, {
|
||||
'line-width': 2,
|
||||
'fill': 'none',
|
||||
fill: 'none',
|
||||
'font-size': '16px',
|
||||
'font-family': "'Andale Mono', monospace"
|
||||
})
|
||||
|
@ -367,13 +369,15 @@ export function finishView (view) {
|
|||
try {
|
||||
var $value = $(value)
|
||||
var $ele = $(value).parent().parent()
|
||||
|
||||
var graphviz = Viz($value.text())
|
||||
$value.unwrap()
|
||||
viz.renderString($value.text())
|
||||
.then(graphviz => {
|
||||
if (!graphviz) throw Error('viz.js output empty graph')
|
||||
$value.html(graphviz)
|
||||
|
||||
$ele.addClass('graphviz')
|
||||
$value.children().unwrap().unwrap()
|
||||
$value.children().unwrap()
|
||||
})
|
||||
} catch (err) {
|
||||
$value.unwrap()
|
||||
$value.parent().append(`<div class="alert alert-warning">${escapeHTML(err)}</div>`)
|
||||
|
@ -387,19 +391,14 @@ export function finishView (view) {
|
|||
var $value = $(value)
|
||||
const $ele = $(value).closest('pre')
|
||||
|
||||
window.mermaid.mermaidAPI.parse($value.text())
|
||||
window.mermaid.parse($value.text())
|
||||
$ele.addClass('mermaid')
|
||||
$ele.html($value.text())
|
||||
window.mermaid.init(undefined, $ele)
|
||||
} catch (err) {
|
||||
var errormessage = err
|
||||
if (err.str) {
|
||||
errormessage = err.str
|
||||
}
|
||||
|
||||
$value.unwrap()
|
||||
$value.parent().append(`<div class="alert alert-warning">${escapeHTML(errormessage)}</div>`)
|
||||
console.warn(errormessage)
|
||||
$value.parent().append(`<div class="alert alert-warning">${escapeHTML(err.str)}</div>`)
|
||||
console.warn(err)
|
||||
}
|
||||
})
|
||||
// abc.js
|
||||
|
@ -422,6 +421,32 @@ export function finishView (view) {
|
|||
console.warn(err)
|
||||
}
|
||||
})
|
||||
// vega-lite
|
||||
const vegas = view.find('div.vega.raw').removeClass('raw')
|
||||
vegas.each((key, value) => {
|
||||
try {
|
||||
var $value = $(value)
|
||||
var $ele = $(value).parent().parent()
|
||||
|
||||
const specText = $value.text()
|
||||
|
||||
$value.unwrap()
|
||||
window.vegaEmbed($ele[0], JSON.parse(specText))
|
||||
.then(result => {
|
||||
$ele.addClass('vega')
|
||||
})
|
||||
.catch(err => {
|
||||
$ele.append(`<div class="alert alert-warning">${escapeHTML(err)}</div>`)
|
||||
console.warn(err)
|
||||
})
|
||||
.finally(() => {
|
||||
if (window.viewAjaxCallback) window.viewAjaxCallback()
|
||||
})
|
||||
} catch (err) {
|
||||
$ele.append(`<div class="alert alert-warning">${escapeHTML(err)}</div>`)
|
||||
console.warn(err)
|
||||
}
|
||||
})
|
||||
// image href new window(emoji not included)
|
||||
const images = view.find('img.raw[src]').removeClass('raw')
|
||||
images.each((key, value) => {
|
||||
|
@ -542,6 +567,12 @@ export function finishView (view) {
|
|||
} catch (err) {
|
||||
console.warn(err)
|
||||
}
|
||||
|
||||
// register details toggle for scrollmap recalulation
|
||||
view.find('details.raw').removeClass('raw').each(function (key, val) {
|
||||
$(val).on('toggle', window.viewAjaxCallback)
|
||||
})
|
||||
|
||||
// render title
|
||||
document.title = renderTitle(view)
|
||||
}
|
||||
|
@ -744,12 +775,12 @@ export function generateToc (id) {
|
|||
target.html('')
|
||||
/* eslint-disable no-unused-vars */
|
||||
var toc = new window.Toc('doc', {
|
||||
'level': 3,
|
||||
'top': -1,
|
||||
'class': 'toc',
|
||||
'ulClass': 'nav',
|
||||
'targetId': id,
|
||||
'process': getHeaderContent
|
||||
level: 3,
|
||||
top: -1,
|
||||
class: 'toc',
|
||||
ulClass: 'nav',
|
||||
targetId: id,
|
||||
process: getHeaderContent
|
||||
})
|
||||
/* eslint-enable no-unused-vars */
|
||||
if (target.text() === 'undefined') { target.html('') }
|
||||
|
@ -828,16 +859,44 @@ const anchorForId = id => {
|
|||
return anchor
|
||||
}
|
||||
|
||||
const createHeaderId = (headerContent, headerIds = null) => {
|
||||
// to escape characters not allow in css and humanize
|
||||
const slug = slugifyWithUTF8(headerContent)
|
||||
let id
|
||||
if (window.linkifyHeaderStyle === 'keep-case') {
|
||||
id = slug
|
||||
} else if (window.linkifyHeaderStyle === 'lower-case') {
|
||||
// to make compatible with GitHub, GitLab, Pandoc and many more
|
||||
id = slug.toLowerCase()
|
||||
} else if (window.linkifyHeaderStyle === 'gfm') {
|
||||
// see GitHub implementation reference:
|
||||
// https://gist.github.com/asabaylus/3071099#gistcomment-1593627
|
||||
// it works like 'lower-case', but ...
|
||||
const idBase = slug.toLowerCase()
|
||||
id = idBase
|
||||
if (headerIds !== null) {
|
||||
// ... making sure the id is unique
|
||||
let i = 1
|
||||
while (headerIds.has(id)) {
|
||||
id = idBase + '-' + i
|
||||
i++
|
||||
}
|
||||
headerIds.add(id)
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unknown linkifyHeaderStyle value "' + window.linkifyHeaderStyle + '"')
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
const linkifyAnchors = (level, containingElement) => {
|
||||
const headers = containingElement.getElementsByTagName(`h${level}`)
|
||||
|
||||
for (let i = 0, l = headers.length; i < l; i++) {
|
||||
let header = headers[i]
|
||||
const header = headers[i]
|
||||
if (header.getElementsByClassName('anchor').length === 0) {
|
||||
if (typeof header.id === 'undefined' || header.id === '') {
|
||||
// to escape characters not allow in css and humanize
|
||||
const id = slugifyWithUTF8(getHeaderContent(header))
|
||||
header.id = id
|
||||
header.id = createHeaderId(getHeaderContent(header))
|
||||
}
|
||||
if (!(typeof header.id === 'undefined' || header.id === '')) {
|
||||
header.insertBefore(anchorForId(header.id), header.firstChild)
|
||||
|
@ -863,8 +922,33 @@ function getHeaderContent (header) {
|
|||
return headerHTML[0].innerHTML
|
||||
}
|
||||
|
||||
function changeHeaderId ($header, id, newId) {
|
||||
$header.attr('id', newId)
|
||||
const $headerLink = $header.find(`> a.anchor[href="#${id}"]`)
|
||||
$headerLink.attr('href', `#${newId}`)
|
||||
$headerLink.attr('title', newId)
|
||||
}
|
||||
|
||||
export function deduplicatedHeaderId (view) {
|
||||
// headers contained in the last change
|
||||
const headers = view.find(':header.raw').removeClass('raw').toArray()
|
||||
if (headers.length === 0) {
|
||||
return
|
||||
}
|
||||
if (window.linkifyHeaderStyle === 'gfm') {
|
||||
// consistent with GitHub, GitLab, Pandoc & co.
|
||||
// all headers contained in the document, in order of appearance
|
||||
const allHeaders = view.find(`:header`).toArray()
|
||||
// list of finaly assigned header IDs
|
||||
const headerIds = new Set()
|
||||
for (let j = 0; j < allHeaders.length; j++) {
|
||||
const $header = $(allHeaders[j])
|
||||
const id = $header.attr('id')
|
||||
const newId = createHeaderId(getHeaderContent($header), headerIds)
|
||||
changeHeaderId($header, id, newId)
|
||||
}
|
||||
} else {
|
||||
// the legacy way
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const id = $(headers[i]).attr('id')
|
||||
if (!id) continue
|
||||
|
@ -872,11 +956,9 @@ export function deduplicatedHeaderId (view) {
|
|||
for (let j = 0; j < duplicatedHeaders.length; j++) {
|
||||
if (duplicatedHeaders[j] !== headers[i]) {
|
||||
const newId = id + j
|
||||
const $duplicatedHeader = $(duplicatedHeaders[j])
|
||||
$duplicatedHeader.attr('id', newId)
|
||||
const $headerLink = $duplicatedHeader.find(`> a.anchor[href="#${id}"]`)
|
||||
$headerLink.attr('href', `#${newId}`)
|
||||
$headerLink.attr('title', newId)
|
||||
const $header = $(duplicatedHeaders[j])
|
||||
changeHeaderId($header, id, newId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -891,12 +973,12 @@ export function renderTOC (view) {
|
|||
const target = $(`#${id}`)
|
||||
target.html('')
|
||||
/* eslint-disable no-unused-vars */
|
||||
let TOC = new window.Toc('doc', {
|
||||
'level': 3,
|
||||
'top': -1,
|
||||
'class': 'toc',
|
||||
'targetId': id,
|
||||
'process': getHeaderContent
|
||||
const TOC = new window.Toc('doc', {
|
||||
level: 3,
|
||||
top: -1,
|
||||
class: 'toc',
|
||||
targetId: id,
|
||||
process: getHeaderContent
|
||||
})
|
||||
/* eslint-enable no-unused-vars */
|
||||
if (target.text() === 'undefined') { target.html('') }
|
||||
|
@ -923,6 +1005,8 @@ function highlightRender (code, lang) {
|
|||
return `<div class="mermaid raw">${code}</div>`
|
||||
} else if (lang === 'abc') {
|
||||
return `<div class="abc raw">${code}</div>`
|
||||
} else if (lang === 'vega') {
|
||||
return `<div class="vega raw">${code}</div>`
|
||||
}
|
||||
const result = {
|
||||
value: code
|
||||
|
@ -944,9 +1028,9 @@ function highlightRender (code, lang) {
|
|||
return result.value
|
||||
}
|
||||
|
||||
export let md = markdownit('default', {
|
||||
export const md = markdownit('default', {
|
||||
html: true,
|
||||
breaks: true,
|
||||
breaks: window.defaultUseHardbreak,
|
||||
langPrefix: '',
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
|
@ -970,6 +1054,7 @@ md.use(require('markdown-it-mathjax')({
|
|||
afterDisplayMath: '\\]</span>'
|
||||
}))
|
||||
md.use(require('markdown-it-imsize'))
|
||||
md.use(require('markdown-it-ruby'))
|
||||
|
||||
md.use(require('markdown-it-emoji'), {
|
||||
shortcuts: {}
|
||||
|
@ -996,8 +1081,29 @@ md.use(markdownitContainer, 'success', { render: renderContainer })
|
|||
md.use(markdownitContainer, 'info', { render: renderContainer })
|
||||
md.use(markdownitContainer, 'warning', { render: renderContainer })
|
||||
md.use(markdownitContainer, 'danger', { render: renderContainer })
|
||||
md.use(markdownitContainer, 'spoiler', {
|
||||
validate: function (params) {
|
||||
return params.trim().match(/^spoiler(\s+.*)?$/)
|
||||
},
|
||||
render: function (tokens, idx) {
|
||||
const m = tokens[idx].info.trim().match(/^spoiler(\s+.*)?$/)
|
||||
|
||||
let defaultImageRender = md.renderer.rules.image
|
||||
if (tokens[idx].nesting === 1) {
|
||||
// opening tag
|
||||
const summary = m[1] && m[1].trim()
|
||||
if (summary) {
|
||||
return `<details><summary>${md.utils.escapeHtml(summary)}</summary>\n`
|
||||
} else {
|
||||
return `<details>\n`
|
||||
}
|
||||
} else {
|
||||
// closing tag
|
||||
return '</details>\n'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const defaultImageRender = md.renderer.rules.image
|
||||
md.renderer.rules.image = function (tokens, idx, options, env, self) {
|
||||
tokens[idx].attrJoin('class', 'raw')
|
||||
return defaultImageRender(...arguments)
|
||||
|
@ -1041,6 +1147,33 @@ md.renderer.rules.fence = (tokens, idx, options, env, self) => {
|
|||
return `<pre><code${self.renderAttrs(token)}>${highlighted}</code></pre>\n`
|
||||
}
|
||||
|
||||
const makePlantumlURL = (umlCode) => {
|
||||
const format = 'svg'
|
||||
const code = plantumlEncoder.encode(umlCode)
|
||||
return `${plantumlServer}/${format}/${code}`
|
||||
}
|
||||
|
||||
// https://github.com/qjebbs/vscode-plantuml/tree/master/src/markdown-it-plantuml
|
||||
md.renderer.rules.plantuml = (tokens, idx) => {
|
||||
const token = tokens[idx]
|
||||
if (token.type !== 'plantuml') {
|
||||
return tokens[idx].content
|
||||
}
|
||||
|
||||
const url = makePlantumlURL(token.content)
|
||||
return `<img src="${url}" />`
|
||||
}
|
||||
|
||||
// https://github.com/qjebbs/vscode-plantuml/tree/master/src/markdown-it-plantuml
|
||||
md.core.ruler.push('plantuml', (state) => {
|
||||
const blockTokens = state.tokens
|
||||
for (const blockToken of blockTokens) {
|
||||
if (blockToken.type === 'fence' && blockToken.info === 'plantuml') {
|
||||
blockToken.type = 'plantuml'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// youtube
|
||||
const youtubePlugin = new Plugin(
|
||||
// regexp to match
|
||||
|
@ -1065,7 +1198,7 @@ const vimeoPlugin = new Plugin(
|
|||
/{%vimeo\s*([\d\D]*?)\s*%}/,
|
||||
|
||||
(match, utils) => {
|
||||
const videoid = match[1]
|
||||
const videoid = match[1].split(/[?&=]+/)[0]
|
||||
if (!videoid) return
|
||||
const div = $('<div class="vimeo raw"></div>')
|
||||
div.attr('data-videoid', videoid)
|
||||
|
@ -1080,7 +1213,7 @@ const gistPlugin = new Plugin(
|
|||
/{%gist\s*([\d\D]*?)\s*%}/,
|
||||
|
||||
(match, utils) => {
|
||||
const gistid = match[1]
|
||||
const gistid = match[1].split(/[?&=]+/)[0]
|
||||
const code = `<code data-gist-id="${gistid}"></code>`
|
||||
return code
|
||||
}
|
||||
|
@ -1098,7 +1231,7 @@ const slidesharePlugin = new Plugin(
|
|||
/{%slideshare\s*([\d\D]*?)\s*%}/,
|
||||
|
||||
(match, utils) => {
|
||||
const slideshareid = match[1]
|
||||
const slideshareid = match[1].split(/[?&=]+/)[0]
|
||||
const div = $('<div class="slideshare raw"></div>')
|
||||
div.attr('data-slideshareid', slideshareid)
|
||||
return div[0].outerHTML
|
||||
|
|
|
@ -144,7 +144,7 @@ export function writeHistory (title, tags) {
|
|||
}
|
||||
|
||||
function writeHistoryToStorage (title, tags) {
|
||||
let data = store.get('notehistory')
|
||||
const data = store.get('notehistory')
|
||||
let notehistory
|
||||
if (data && typeof data === 'string') {
|
||||
notehistory = JSON.parse(data)
|
||||
|
@ -260,7 +260,7 @@ function parseToHistory (list, notehistory, callback) {
|
|||
for (let i = 0; i < notehistory.length; i++) {
|
||||
// migrate LZString encoded id to base64url encoded id
|
||||
try {
|
||||
let id = LZString.decompressFromBase64(notehistory[i].id)
|
||||
const id = LZString.decompressFromBase64(notehistory[i].id)
|
||||
if (id && checkNoteIdValid(id)) {
|
||||
notehistory[i].id = encodeNoteId(id)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-env browser, jquery */
|
||||
/* global CodeMirror, Cookies, moment, Spinner, serverurl,
|
||||
key, Dropbox, ot, hex2rgb, Visibility */
|
||||
/* global CodeMirror, Cookies, moment, serverurl,
|
||||
key, Dropbox, ot, hex2rgb, Visibility, inlineAttachment */
|
||||
|
||||
import TurndownService from 'turndown'
|
||||
|
||||
|
@ -17,6 +17,8 @@ import List from 'list.js'
|
|||
|
||||
import Idle from '@hackmd/idle-js'
|
||||
|
||||
import { Spinner } from 'spin.js'
|
||||
|
||||
import {
|
||||
checkLoginStateChanged,
|
||||
setloginStateChangeEvent
|
||||
|
@ -83,6 +85,7 @@ require('../css/index.css')
|
|||
require('../css/extra.css')
|
||||
require('../css/slide-preview.css')
|
||||
require('../css/site.css')
|
||||
require('spin.js/spin.css')
|
||||
|
||||
require('highlight.js/styles/github-gist.css')
|
||||
|
||||
|
@ -95,9 +98,9 @@ var updateViewDebounce = 100
|
|||
var cursorMenuThrottle = 50
|
||||
var cursorActivityDebounce = 50
|
||||
var cursorAnimatePeriod = 100
|
||||
var supportContainers = ['success', 'info', 'warning', 'danger']
|
||||
var supportContainers = ['success', 'info', 'warning', 'danger', 'spoiler']
|
||||
var supportCodeModes = ['javascript', 'typescript', 'jsx', 'htmlmixed', 'htmlembedded', 'css', 'xml', 'clike', 'clojure', 'ruby', 'python', 'shell', 'php', 'sql', 'haskell', 'coffeescript', 'yaml', 'pug', 'lua', 'cmake', 'nginx', 'perl', 'sass', 'r', 'dockerfile', 'tiddlywiki', 'mediawiki', 'go', 'gherkin'].concat(hljs.listLanguages())
|
||||
var supportCharts = ['sequence', 'flow', 'graphviz', 'mermaid', 'abc']
|
||||
var supportCharts = ['sequence', 'flow', 'graphviz', 'mermaid', 'abc', 'plantuml', 'vega']
|
||||
var supportHeaders = [
|
||||
{
|
||||
text: '# h1',
|
||||
|
@ -260,7 +263,7 @@ let visibleMD = false
|
|||
let visibleLG = false
|
||||
const isTouchDevice = 'ontouchstart' in document.documentElement
|
||||
let currentStatus = statusType.offline
|
||||
let lastInfo = {
|
||||
const lastInfo = {
|
||||
needRestore: false,
|
||||
cursor: null,
|
||||
scroll: null,
|
||||
|
@ -286,14 +289,14 @@ let lastInfo = {
|
|||
let personalInfo = {}
|
||||
let onlineUsers = []
|
||||
const fileTypes = {
|
||||
'pl': 'perl',
|
||||
'cgi': 'perl',
|
||||
'js': 'javascript',
|
||||
'php': 'php',
|
||||
'sh': 'bash',
|
||||
'rb': 'ruby',
|
||||
'html': 'html',
|
||||
'py': 'python'
|
||||
pl: 'perl',
|
||||
cgi: 'perl',
|
||||
js: 'javascript',
|
||||
php: 'php',
|
||||
sh: 'bash',
|
||||
rb: 'ruby',
|
||||
html: 'html',
|
||||
py: 'python'
|
||||
}
|
||||
|
||||
// editor settings
|
||||
|
@ -334,9 +337,7 @@ var opts = {
|
|||
left: '50%' // Left position relative to parent
|
||||
}
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
var spinner = new Spinner(opts).spin(ui.spinner[0])
|
||||
/* eslint-enable no-unused-vars */
|
||||
new Spinner(opts).spin(ui.spinner[0])
|
||||
|
||||
// idle
|
||||
var idle = new Idle({
|
||||
|
@ -955,8 +956,8 @@ ui.toolbar.export.dropbox.click(function () {
|
|||
var options = {
|
||||
files: [
|
||||
{
|
||||
'url': noteurl + '/download',
|
||||
'filename': filename
|
||||
url: noteurl + '/download',
|
||||
filename: filename
|
||||
}
|
||||
],
|
||||
error: function (errorMessage) {
|
||||
|
@ -1787,29 +1788,29 @@ var authorMarks = {} // temp variable
|
|||
var addTextMarkers = [] // temp variable
|
||||
function updateInfo (data) {
|
||||
// console.log(data);
|
||||
if (data.hasOwnProperty('createtime') && window.createtime !== data.createtime) {
|
||||
if (Object.hasOwnProperty.call(data, 'createtime') && window.createtime !== data.createtime) {
|
||||
window.createtime = data.createtime
|
||||
updateLastChange()
|
||||
}
|
||||
if (data.hasOwnProperty('updatetime') && window.lastchangetime !== data.updatetime) {
|
||||
if (Object.hasOwnProperty.call(data, 'updatetime') && window.lastchangetime !== data.updatetime) {
|
||||
window.lastchangetime = data.updatetime
|
||||
updateLastChange()
|
||||
}
|
||||
if (data.hasOwnProperty('owner') && window.owner !== data.owner) {
|
||||
if (Object.hasOwnProperty.call(data, 'owner') && window.owner !== data.owner) {
|
||||
window.owner = data.owner
|
||||
window.ownerprofile = data.ownerprofile
|
||||
updateOwner()
|
||||
}
|
||||
if (data.hasOwnProperty('lastchangeuser') && window.lastchangeuser !== data.lastchangeuser) {
|
||||
if (Object.hasOwnProperty.call(data, 'lastchangeuser') && window.lastchangeuser !== data.lastchangeuser) {
|
||||
window.lastchangeuser = data.lastchangeuser
|
||||
window.lastchangeuserprofile = data.lastchangeuserprofile
|
||||
updateLastChangeUser()
|
||||
updateOwner()
|
||||
}
|
||||
if (data.hasOwnProperty('authors') && authors !== data.authors) {
|
||||
if (Object.hasOwnProperty.call(data, 'authors') && authors !== data.authors) {
|
||||
authors = data.authors
|
||||
}
|
||||
if (data.hasOwnProperty('authorship') && authorship !== data.authorship) {
|
||||
if (Object.hasOwnProperty.call(data, 'authorship') && authorship !== data.authorship) {
|
||||
authorship = data.authorship
|
||||
updateAuthorship()
|
||||
}
|
||||
|
@ -1854,7 +1855,7 @@ function updateAuthorshipInner () {
|
|||
authorMarks = {}
|
||||
for (let i = 0; i < authorship.length; i++) {
|
||||
var atom = authorship[i]
|
||||
let author = authors[atom[0]]
|
||||
const author = authors[atom[0]]
|
||||
if (author) {
|
||||
var prePos = editor.posFromIndex(atom[1])
|
||||
var preLine = editor.getLine(prePos.line)
|
||||
|
@ -1872,7 +1873,7 @@ function updateAuthorshipInner () {
|
|||
if (prePos.ch === preLine.length) {
|
||||
startLine++
|
||||
} else if (prePos.ch !== 0) {
|
||||
let mark = initMarkAndCheckGutter(authorMarks[prePos.line], author, atom[3])
|
||||
const mark = initMarkAndCheckGutter(authorMarks[prePos.line], author, atom[3])
|
||||
var _postPos = {
|
||||
line: prePos.line,
|
||||
ch: preLine.length
|
||||
|
@ -1889,7 +1890,7 @@ function updateAuthorshipInner () {
|
|||
if (postPos.ch === 0) {
|
||||
endLine--
|
||||
} else if (postPos.ch !== postLine.length) {
|
||||
let mark = initMarkAndCheckGutter(authorMarks[postPos.line], author, atom[3])
|
||||
const mark = initMarkAndCheckGutter(authorMarks[postPos.line], author, atom[3])
|
||||
var _prePos = {
|
||||
line: postPos.line,
|
||||
ch: 0
|
||||
|
@ -1909,7 +1910,7 @@ function updateAuthorshipInner () {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
let mark = initMarkAndCheckGutter(authorMarks[prePos.line], author, atom[3])
|
||||
const mark = initMarkAndCheckGutter(authorMarks[prePos.line], author, atom[3])
|
||||
if (JSON.stringify(prePos) !== JSON.stringify(postPos)) {
|
||||
mark.textmarkers.push({
|
||||
userid: author.userid,
|
||||
|
@ -1922,15 +1923,15 @@ function updateAuthorshipInner () {
|
|||
}
|
||||
addTextMarkers = []
|
||||
editor.eachLine(iterateLine)
|
||||
var allTextMarks = editor.getAllMarks()
|
||||
const allTextMarks = editor.getAllMarks()
|
||||
for (let i = 0; i < allTextMarks.length; i++) {
|
||||
let _textMarker = allTextMarks[i]
|
||||
var pos = _textMarker.find()
|
||||
var found = false
|
||||
const _textMarker = allTextMarks[i]
|
||||
const pos = _textMarker.find()
|
||||
let found = false
|
||||
for (let j = 0; j < addTextMarkers.length; j++) {
|
||||
let textMarker = addTextMarkers[j]
|
||||
let author = authors[textMarker.userid]
|
||||
let className = 'authorship-inline-' + author.color.substr(1)
|
||||
const textMarker = addTextMarkers[j]
|
||||
const author = authors[textMarker.userid]
|
||||
const className = 'authorship-inline-' + author.color.substr(1)
|
||||
var obj = {
|
||||
from: textMarker.pos[0],
|
||||
to: textMarker.pos[1]
|
||||
|
@ -1948,12 +1949,12 @@ function updateAuthorshipInner () {
|
|||
}
|
||||
}
|
||||
for (let i = 0; i < addTextMarkers.length; i++) {
|
||||
let textMarker = addTextMarkers[i]
|
||||
let author = authors[textMarker.userid]
|
||||
const textMarker = addTextMarkers[i]
|
||||
const author = authors[textMarker.userid]
|
||||
const rgbcolor = hex2rgb(author.color)
|
||||
const colorString = `rgba(${rgbcolor.red},${rgbcolor.green},${rgbcolor.blue},0.7)`
|
||||
const styleString = `background-image: linear-gradient(to top, ${colorString} 1px, transparent 1px);`
|
||||
let className = `authorship-inline-${author.color.substr(1)}`
|
||||
const className = `authorship-inline-${author.color.substr(1)}`
|
||||
const rule = `.${className} { ${styleString} }`
|
||||
addStyleRule(rule)
|
||||
editor.markText(textMarker.pos[0], textMarker.pos[1], {
|
||||
|
@ -1963,11 +1964,11 @@ function updateAuthorshipInner () {
|
|||
}
|
||||
}
|
||||
function iterateLine (line) {
|
||||
var lineNumber = line.lineNo()
|
||||
var currMark = authorMarks[lineNumber]
|
||||
var author = currMark ? authors[currMark.gutter.userid] : null
|
||||
const lineNumber = line.lineNo()
|
||||
const currMark = authorMarks[lineNumber]
|
||||
const author = currMark ? authors[currMark.gutter.userid] : null
|
||||
if (currMark && author) {
|
||||
let className = 'authorship-gutter-' + author.color.substr(1)
|
||||
const className = 'authorship-gutter-' + author.color.substr(1)
|
||||
const gutters = line.gutterMarkers
|
||||
if (!gutters || !gutters['authorship-gutters'] ||
|
||||
!gutters['authorship-gutters'].className ||
|
||||
|
@ -1975,7 +1976,7 @@ function iterateLine (line) {
|
|||
const styleString = `border-left: 3px solid ${author.color}; height: ${defaultTextHeight}px; margin-left: 3px;`
|
||||
const rule = `.${className} { ${styleString} }`
|
||||
addStyleRule(rule)
|
||||
var gutter = $('<div>', {
|
||||
const gutter = $('<div>', {
|
||||
class: 'authorship-gutter ' + className,
|
||||
title: author.name
|
||||
})
|
||||
|
@ -1985,8 +1986,8 @@ function iterateLine (line) {
|
|||
editor.setGutterMarker(line, 'authorship-gutters', null)
|
||||
}
|
||||
if (currMark && currMark.textmarkers.length > 0) {
|
||||
for (var i = 0; i < currMark.textmarkers.length; i++) {
|
||||
let textMarker = currMark.textmarkers[i]
|
||||
for (let i = 0; i < currMark.textmarkers.length; i++) {
|
||||
const textMarker = currMark.textmarkers[i]
|
||||
if (textMarker.userid !== currMark.gutter.userid) {
|
||||
addTextMarkers.push(textMarker)
|
||||
}
|
||||
|
@ -1997,12 +1998,12 @@ editorInstance.on('update', function () {
|
|||
$('.authorship-gutter:not([data-original-title])').tooltip({
|
||||
container: '.CodeMirror-lines',
|
||||
placement: 'right',
|
||||
delay: { 'show': 500, 'hide': 100 }
|
||||
delay: { show: 500, hide: 100 }
|
||||
})
|
||||
$('.authorship-inline:not([data-original-title])').tooltip({
|
||||
container: '.CodeMirror-lines',
|
||||
placement: 'bottom',
|
||||
delay: { 'show': 500, 'hide': 100 }
|
||||
delay: { show: 500, hide: 100 }
|
||||
})
|
||||
// clear tooltip which described element has been removed
|
||||
$('[id^="tooltip"]').each(function (index, element) {
|
||||
|
@ -2063,7 +2064,7 @@ var cmClient = null
|
|||
var synchronized_ = null
|
||||
|
||||
function havePendingOperation () {
|
||||
return !!((cmClient && cmClient.state && cmClient.state.hasOwnProperty('outstanding')))
|
||||
return !!((cmClient && cmClient.state && Object.hasOwnProperty.call(cmClient.state, 'outstanding')))
|
||||
}
|
||||
|
||||
socket.on('doc', function (obj) {
|
||||
|
@ -2223,7 +2224,7 @@ function updateOnlineStatus () {
|
|||
break
|
||||
}
|
||||
}
|
||||
let id = items[i].values().id
|
||||
const id = items[i].values().id
|
||||
if (found) {
|
||||
onlineUserList.get('id', id)[0].values(_onlineUsers[foundindex])
|
||||
shortOnlineUserList.get('id', id)[0].values(_onlineUsers[foundindex])
|
||||
|
@ -2417,19 +2418,19 @@ function buildCursor (user) {
|
|||
break
|
||||
}
|
||||
if ($('div[data-clientid="' + user.id + '"]').length <= 0) {
|
||||
let cursor = $('<div data-clientid="' + user.id + '" class="CodeMirror-other-cursor" style="display:none;"></div>')
|
||||
const cursor = $('<div data-clientid="' + user.id + '" class="CodeMirror-other-cursor" style="display:none;"></div>')
|
||||
cursor.attr('data-line', user.cursor.line)
|
||||
cursor.attr('data-ch', user.cursor.ch)
|
||||
cursor.attr('data-offset-left', 0)
|
||||
cursor.attr('data-offset-top', 0)
|
||||
|
||||
let cursorbar = $('<div class="cursorbar"> </div>')
|
||||
const cursorbar = $('<div class="cursorbar"> </div>')
|
||||
cursorbar[0].style.height = defaultTextHeight + 'px'
|
||||
cursorbar[0].style.borderLeft = '2px solid ' + user.color
|
||||
|
||||
var icon = '<i class="fa ' + iconClass + '"></i>'
|
||||
|
||||
let cursortag = $('<div class="cursortag">' + icon + ' <span class="name">' + user.name + '</span></div>')
|
||||
const cursortag = $('<div class="cursortag">' + icon + ' <span class="name">' + user.name + '</span></div>')
|
||||
// cursortag[0].style.background = color;
|
||||
cursortag[0].style.color = user.color
|
||||
|
||||
|
@ -2485,15 +2486,15 @@ function buildCursor (user) {
|
|||
|
||||
checkCursorTag(coord, cursortag)
|
||||
} else {
|
||||
let cursor = $('div[data-clientid="' + user.id + '"]')
|
||||
const cursor = $('div[data-clientid="' + user.id + '"]')
|
||||
cursor.attr('data-line', user.cursor.line)
|
||||
cursor.attr('data-ch', user.cursor.ch)
|
||||
|
||||
let cursorbar = cursor.find('.cursorbar')
|
||||
const cursorbar = cursor.find('.cursorbar')
|
||||
cursorbar[0].style.height = defaultTextHeight + 'px'
|
||||
cursorbar[0].style.borderLeft = '2px solid ' + user.color
|
||||
|
||||
let cursortag = cursor.find('.cursortag')
|
||||
const cursortag = cursor.find('.cursortag')
|
||||
cursortag.find('i').removeClass().addClass('fa').addClass(iconClass)
|
||||
cursortag.find('.name').text(user.name)
|
||||
|
||||
|
@ -2502,8 +2503,8 @@ function buildCursor (user) {
|
|||
cursor[0].style.top = coord.top + 'px'
|
||||
} else {
|
||||
cursor.animate({
|
||||
'left': coord.left,
|
||||
'top': coord.top
|
||||
left: coord.left,
|
||||
top: coord.top
|
||||
}, {
|
||||
duration: cursorAnimatePeriod,
|
||||
queue: false
|
||||
|
@ -2712,8 +2713,8 @@ function restoreInfo () {
|
|||
$(window).scrollLeft(lastInfo.edit.scroll.left)
|
||||
$(window).scrollTop(lastInfo.edit.scroll.top)
|
||||
} else {
|
||||
let left = lastInfo.edit.scroll.left
|
||||
let top = lastInfo.edit.scroll.top
|
||||
const left = lastInfo.edit.scroll.left
|
||||
const top = lastInfo.edit.scroll.top
|
||||
editor.scrollIntoView()
|
||||
editor.scrollTo(left, top)
|
||||
}
|
||||
|
@ -2723,8 +2724,8 @@ function restoreInfo () {
|
|||
$(window).scrollTop(lastInfo.view.scroll.top)
|
||||
break
|
||||
case modeType.both:
|
||||
let left = lastInfo.edit.scroll.left
|
||||
let top = lastInfo.edit.scroll.top
|
||||
const left = lastInfo.edit.scroll.left
|
||||
const top = lastInfo.edit.scroll.top
|
||||
editor.scrollIntoView()
|
||||
editor.scrollTo(left, top)
|
||||
ui.area.view.scrollLeft(lastInfo.view.scroll.left)
|
||||
|
@ -2846,8 +2847,8 @@ function partialUpdate (src, tar, des) {
|
|||
for (let i = 0; i < tar.length; i++) {
|
||||
// copyAttribute(src[i], des[i], 'data-startline');
|
||||
// copyAttribute(src[i], des[i], 'data-endline');
|
||||
let rawSrc = cloneAndRemoveDataAttr(src[i])
|
||||
let rawTar = cloneAndRemoveDataAttr(tar[i])
|
||||
const rawSrc = cloneAndRemoveDataAttr(src[i])
|
||||
const rawTar = cloneAndRemoveDataAttr(tar[i])
|
||||
if (!rawSrc || !rawTar || rawSrc.outerHTML !== rawTar.outerHTML) {
|
||||
start = i
|
||||
break
|
||||
|
@ -2859,8 +2860,8 @@ function partialUpdate (src, tar, des) {
|
|||
for (let i = 0; i < src.length; i++) {
|
||||
// copyAttribute(src[i], des[i], 'data-startline');
|
||||
// copyAttribute(src[i], des[i], 'data-endline');
|
||||
let rawSrc = cloneAndRemoveDataAttr(src[i])
|
||||
let rawTar = cloneAndRemoveDataAttr(tar[i])
|
||||
const rawSrc = cloneAndRemoveDataAttr(src[i])
|
||||
const rawTar = cloneAndRemoveDataAttr(tar[i])
|
||||
if (!rawSrc || !rawTar || rawSrc.outerHTML !== rawTar.outerHTML) {
|
||||
start = i
|
||||
break
|
||||
|
@ -2868,12 +2869,12 @@ function partialUpdate (src, tar, des) {
|
|||
}
|
||||
// tar end
|
||||
for (let i = 1; i <= tar.length + 1; i++) {
|
||||
let srcLength = src.length
|
||||
let tarLength = tar.length
|
||||
const srcLength = src.length
|
||||
const tarLength = tar.length
|
||||
// copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline');
|
||||
// copyAttribute(src[srcLength - i], des[srcLength - i], 'data-endline');
|
||||
let rawSrc = cloneAndRemoveDataAttr(src[srcLength - i])
|
||||
let rawTar = cloneAndRemoveDataAttr(tar[tarLength - i])
|
||||
const rawSrc = cloneAndRemoveDataAttr(src[srcLength - i])
|
||||
const rawTar = cloneAndRemoveDataAttr(tar[tarLength - i])
|
||||
if (!rawSrc || !rawTar || rawSrc.outerHTML !== rawTar.outerHTML) {
|
||||
tarEnd = tar.length - i
|
||||
break
|
||||
|
@ -2881,12 +2882,12 @@ function partialUpdate (src, tar, des) {
|
|||
}
|
||||
// src end
|
||||
for (let i = 1; i <= src.length + 1; i++) {
|
||||
let srcLength = src.length
|
||||
let tarLength = tar.length
|
||||
const srcLength = src.length
|
||||
const tarLength = tar.length
|
||||
// copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline');
|
||||
// copyAttribute(src[srcLength - i], des[srcLength - i], 'data-endline');
|
||||
let rawSrc = cloneAndRemoveDataAttr(src[srcLength - i])
|
||||
let rawTar = cloneAndRemoveDataAttr(tar[tarLength - i])
|
||||
const rawSrc = cloneAndRemoveDataAttr(src[srcLength - i])
|
||||
const rawTar = cloneAndRemoveDataAttr(tar[tarLength - i])
|
||||
if (!rawSrc || !rawTar || rawSrc.outerHTML !== rawTar.outerHTML) {
|
||||
srcEnd = src.length - i
|
||||
break
|
||||
|
@ -3115,6 +3116,27 @@ function matchInContainer (text) {
|
|||
}
|
||||
}
|
||||
|
||||
const textCompleteKeyMap = {
|
||||
Up: function () {
|
||||
return false
|
||||
},
|
||||
Right: function () {
|
||||
editor.doc.cm.execCommand('goCharRight')
|
||||
},
|
||||
Down: function () {
|
||||
return false
|
||||
},
|
||||
Left: function () {
|
||||
editor.doc.cm.execCommand('goCharLeft')
|
||||
},
|
||||
Enter: function () {
|
||||
return false
|
||||
},
|
||||
Backspace: function () {
|
||||
editor.doc.cm.execCommand('delCharBefore')
|
||||
}
|
||||
}
|
||||
|
||||
$(editor.getInputField())
|
||||
.textcomplete([
|
||||
{ // emoji strategy
|
||||
|
@ -3316,29 +3338,10 @@ $(editor.getInputField())
|
|||
},
|
||||
'textComplete:show': function (e) {
|
||||
$(this).data('autocompleting', true)
|
||||
editor.setOption('extraKeys', {
|
||||
'Up': function () {
|
||||
return false
|
||||
},
|
||||
'Right': function () {
|
||||
editor.doc.cm.execCommand('goCharRight')
|
||||
},
|
||||
'Down': function () {
|
||||
return false
|
||||
},
|
||||
'Left': function () {
|
||||
editor.doc.cm.execCommand('goCharLeft')
|
||||
},
|
||||
'Enter': function () {
|
||||
return false
|
||||
},
|
||||
'Backspace': function () {
|
||||
editor.doc.cm.execCommand('delCharBefore')
|
||||
}
|
||||
})
|
||||
editor.addKeyMap(textCompleteKeyMap)
|
||||
},
|
||||
'textComplete:hide': function (e) {
|
||||
$(this).data('autocompleting', false)
|
||||
editor.setOption('extraKeys', editorInstance.defaultExtraKeys)
|
||||
editor.removeKeyMap(textCompleteKeyMap)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import modeType from './modeType'
|
||||
|
||||
let state = {
|
||||
const state = {
|
||||
syncscroll: true,
|
||||
currentMode: modeType.view,
|
||||
nightMode: false
|
||||
|
|
|
@ -2,7 +2,12 @@ window.domain = '<%- domain %>'
|
|||
window.urlpath = '<%- urlpath %>'
|
||||
window.debug = <%- debug %>
|
||||
window.version = '<%- version %>'
|
||||
window.plantumlServer = '<%- plantumlServer %>'
|
||||
|
||||
window.allowedUploadMimeTypes = <%- JSON.stringify(allowedUploadMimeTypes) %>
|
||||
|
||||
window.defaultUseHardbreak = <%- defaultUseHardbreak %>
|
||||
|
||||
window.linkifyHeaderStyle = '<%- linkifyHeaderStyle %>'
|
||||
|
||||
window.DROPBOX_APP_KEY = '<%- DROPBOX_APP_KEY %>'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
let config = {
|
||||
const config = {
|
||||
docmaxlength: null
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
/* global CodeMirror, $, editor, Cookies */
|
||||
import * as utils from './utils'
|
||||
import config from './config'
|
||||
import statusBarTemplate from './statusbar.html'
|
||||
import toolBarTemplate from './toolbar.html'
|
||||
import './markdown-lint'
|
||||
import { initTableEditor } from './table-editor'
|
||||
import { options, Alignment, FormatType } from '@susisu/mte-kernel'
|
||||
|
||||
/* config section */
|
||||
const isMac = CodeMirror.keyMap.default === CodeMirror.keyMap.macDefault
|
||||
|
@ -75,8 +79,8 @@ export default class Editor {
|
|||
},
|
||||
'Cmd-Left': 'goLineLeftSmart',
|
||||
'Cmd-Right': 'goLineRight',
|
||||
'Home': 'goLineLeftSmart',
|
||||
'End': 'goLineRight',
|
||||
Home: 'goLineLeftSmart',
|
||||
End: 'goLineRight',
|
||||
'Ctrl-C': function (cm) {
|
||||
if (!isMac && cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
||||
document.execCommand('copy')
|
||||
|
@ -158,6 +162,19 @@ export default class Editor {
|
|||
var makeLine = $('#makeLine')
|
||||
var makeComment = $('#makeComment')
|
||||
|
||||
var insertRow = $('#insertRow')
|
||||
var deleteRow = $('#deleteRow')
|
||||
var moveRowUp = $('#moveRowUp')
|
||||
var moveRowDown = $('#moveRowDown')
|
||||
var insertColumn = $('#insertColumn')
|
||||
var deleteColumn = $('#deleteColumn')
|
||||
var moveColumnLeft = $('#moveColumnLeft')
|
||||
var moveColumnRight = $('#moveColumnRight')
|
||||
var alignLeft = $('#alignLeft')
|
||||
var alignCenter = $('#alignCenter')
|
||||
var alignRight = $('#alignRight')
|
||||
var alignNone = $('#alignNone')
|
||||
|
||||
makeBold.click(() => {
|
||||
utils.wrapTextWith(this.editor, this.editor, '**')
|
||||
this.editor.focus()
|
||||
|
@ -217,6 +234,72 @@ export default class Editor {
|
|||
makeComment.click(() => {
|
||||
utils.insertText(this.editor, '> []')
|
||||
})
|
||||
|
||||
// table tools UI
|
||||
const opts = options({
|
||||
smartCursor: true,
|
||||
formatType: FormatType.NORMAL
|
||||
})
|
||||
|
||||
insertRow.click(() => {
|
||||
this.tableEditor.insertRow(opts)
|
||||
this.editor.focus()
|
||||
})
|
||||
|
||||
deleteRow.click(() => {
|
||||
this.tableEditor.deleteRow(opts)
|
||||
this.editor.focus()
|
||||
})
|
||||
|
||||
moveRowUp.click(() => {
|
||||
this.tableEditor.moveRow(-1, opts)
|
||||
this.editor.focus()
|
||||
})
|
||||
|
||||
moveRowDown.click(() => {
|
||||
this.tableEditor.moveRow(1, opts)
|
||||
this.editor.focus()
|
||||
})
|
||||
|
||||
insertColumn.click(() => {
|
||||
this.tableEditor.insertColumn(opts)
|
||||
this.editor.focus()
|
||||
})
|
||||
|
||||
deleteColumn.click(() => {
|
||||
this.tableEditor.deleteColumn(opts)
|
||||
this.editor.focus()
|
||||
})
|
||||
|
||||
moveColumnLeft.click(() => {
|
||||
this.tableEditor.moveColumn(-1, opts)
|
||||
this.editor.focus()
|
||||
})
|
||||
|
||||
moveColumnRight.click(() => {
|
||||
this.tableEditor.moveColumn(1, opts)
|
||||
this.editor.focus()
|
||||
})
|
||||
|
||||
alignLeft.click(() => {
|
||||
this.tableEditor.alignColumn(Alignment.LEFT, opts)
|
||||
this.editor.focus()
|
||||
})
|
||||
|
||||
alignCenter.click(() => {
|
||||
this.tableEditor.alignColumn(Alignment.CENTER, opts)
|
||||
this.editor.focus()
|
||||
})
|
||||
|
||||
alignRight.click(() => {
|
||||
this.tableEditor.alignColumn(Alignment.RIGHT, opts)
|
||||
this.editor.focus()
|
||||
})
|
||||
|
||||
alignNone.click(() => {
|
||||
this.tableEditor.alignColumn(Alignment.NONE, opts)
|
||||
this.editor.focus()
|
||||
})
|
||||
}
|
||||
|
||||
addStatusBar () {
|
||||
|
@ -230,6 +313,7 @@ export default class Editor {
|
|||
this.statusLength = this.statusBar.find('.status-length')
|
||||
this.statusTheme = this.statusBar.find('.status-theme')
|
||||
this.statusSpellcheck = this.statusBar.find('.status-spellcheck')
|
||||
this.statusLinter = this.statusBar.find('.status-linter')
|
||||
this.statusPreferences = this.statusBar.find('.status-preferences')
|
||||
this.statusPanel = this.editor.addPanel(this.statusBar[0], {
|
||||
position: 'bottom'
|
||||
|
@ -239,6 +323,7 @@ export default class Editor {
|
|||
this.setKeymap()
|
||||
this.setTheme()
|
||||
this.setSpellcheck()
|
||||
this.setLinter()
|
||||
this.setPreferences()
|
||||
}
|
||||
|
||||
|
@ -497,6 +582,42 @@ export default class Editor {
|
|||
}
|
||||
}
|
||||
|
||||
toggleLinter (enable) {
|
||||
const gutters = this.editor.getOption('gutters')
|
||||
const lintGutter = 'CodeMirror-lint-markers'
|
||||
|
||||
if (enable) {
|
||||
if (!gutters.includes(lintGutter)) {
|
||||
this.editor.setOption('gutters', [lintGutter, ...gutters])
|
||||
}
|
||||
Cookies.set('linter', true, {
|
||||
expires: 365
|
||||
})
|
||||
} else {
|
||||
this.editor.setOption('gutters', gutters.filter(g => g !== lintGutter))
|
||||
Cookies.remove('linter')
|
||||
}
|
||||
this.editor.setOption('lint', enable)
|
||||
}
|
||||
|
||||
setLinter () {
|
||||
const linterToggle = this.statusLinter.find('.ui-linter-toggle')
|
||||
|
||||
const updateLinterStatus = (enable) => {
|
||||
linterToggle.toggleClass('active', enable)
|
||||
}
|
||||
|
||||
linterToggle.click(() => {
|
||||
const lintEnable = this.editor.getOption('lint')
|
||||
this.toggleLinter.bind(this)(!lintEnable)
|
||||
updateLinterStatus(!lintEnable)
|
||||
})
|
||||
|
||||
const enable = !!Cookies.get('linter')
|
||||
this.toggleLinter.bind(this)(enable)
|
||||
updateLinterStatus(enable)
|
||||
}
|
||||
|
||||
resetEditorKeymapToBrowserKeymap () {
|
||||
var keymap = this.editor.getOption('keyMap')
|
||||
if (!this.jumpToAddressBarKeymapValue) {
|
||||
|
@ -512,6 +633,7 @@ export default class Editor {
|
|||
this.jumpToAddressBarKeymapValue = null
|
||||
}
|
||||
}
|
||||
|
||||
setOverrideBrowserKeymap () {
|
||||
var overrideBrowserKeymap = $(
|
||||
'.ui-preferences-override-browser-keymap label > input[type="checkbox"]'
|
||||
|
@ -582,6 +704,8 @@ export default class Editor {
|
|||
placeholder: "← Start by entering a title here\n===\nVisit /features if you don't know what to do.\nHappy hacking :)"
|
||||
})
|
||||
|
||||
this.tableEditor = initTableEditor(this.editor)
|
||||
|
||||
return this.editor
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/* global CodeMirror */
|
||||
|
||||
// load CM lint plugin explicitly
|
||||
import '@hackmd/codemirror/addon/lint/lint'
|
||||
import './lint.css'
|
||||
|
||||
window.markdownit = require('markdown-it')
|
||||
// eslint-disable-next-line
|
||||
require('script-loader!markdownlint');
|
||||
|
||||
(function (mod) {
|
||||
mod(CodeMirror)
|
||||
})(function (CodeMirror) {
|
||||
function validator (text) {
|
||||
return lint(text).map(error => {
|
||||
const {
|
||||
ruleNames,
|
||||
ruleDescription,
|
||||
lineNumber: ln,
|
||||
errorRange
|
||||
} = error
|
||||
const lineNumber = ln - 1
|
||||
|
||||
let start = 0; let end = -1
|
||||
if (errorRange) {
|
||||
[start, end] = errorRange.map(r => r - 1)
|
||||
}
|
||||
|
||||
return {
|
||||
messageHTML: `${ruleNames.join('/')}: ${ruleDescription}`,
|
||||
severity: 'error',
|
||||
from: CodeMirror.Pos(lineNumber, start),
|
||||
to: CodeMirror.Pos(lineNumber, end)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
CodeMirror.registerHelper('lint', 'markdown', validator)
|
||||
})
|
||||
|
||||
function lint (content) {
|
||||
const { content: errors } = window.markdownlint.sync({
|
||||
strings: {
|
||||
content
|
||||
}
|
||||
})
|
||||
return errors
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/* The lint marker gutter */
|
||||
.CodeMirror-lint-markers {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-tooltip {
|
||||
background-color: #333333;
|
||||
border: 1px solid #eeeeee;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-family: "Source Code Pro", Consolas, monaco, monospace;
|
||||
font-size: 10pt;
|
||||
overflow: hidden;
|
||||
padding: 2px 5px;
|
||||
position: fixed;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
z-index: 100;
|
||||
max-width: 600px;
|
||||
opacity: 0;
|
||||
transition: opacity .4s;
|
||||
-moz-transition: opacity .4s;
|
||||
-webkit-transition: opacity .4s;
|
||||
-o-transition: opacity .4s;
|
||||
-ms-transition: opacity .4s;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning {
|
||||
background-position: left bottom;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-error {
|
||||
background-image: url(/images/lint/mark-error.png);
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-warning {
|
||||
background-image: url(/images/lint/mark-warning.png);
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning {
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning {
|
||||
padding-left: 20px;
|
||||
background-position: top left;
|
||||
background-repeat: no-repeat;
|
||||
background-position-y: 2px;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
|
||||
background-image: url(/images/lint/message-error.png);
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
|
||||
background-image: url(/images/lint/message-warning.png);
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-multiple {
|
||||
background-image: url(/images/lint/mark-multiple.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: right bottom;
|
||||
width: 100%; height: 100%;
|
||||
}
|
|
@ -37,5 +37,8 @@
|
|||
<div class="status-spellcheck">
|
||||
<a class="ui-spellcheck-toggle" title="Toggle spellcheck"><i class="fa fa-check fa-fw"></i></a>
|
||||
</div>
|
||||
<div class="status-linter">
|
||||
<a class="ui-linter-toggle" title="Toggle linter"><i class="fa fa-lightbulb-o fa-fw"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
/* global CodeMirror, $ */
|
||||
import { TableEditor, Point, options, Alignment, FormatType } from '@susisu/mte-kernel'
|
||||
|
||||
// port of the code from: https://github.com/susisu/mte-demo/blob/master/src/main.js
|
||||
|
||||
// text editor interface
|
||||
// see https://doc.esdoc.org/github.com/susisu/mte-kernel/class/lib/text-editor.js~ITextEditor.html
|
||||
class TextEditorInterface {
|
||||
constructor (editor) {
|
||||
this.editor = editor
|
||||
this.doc = editor.getDoc()
|
||||
this.transaction = false
|
||||
this.onDidFinishTransaction = null
|
||||
}
|
||||
|
||||
getCursorPosition () {
|
||||
const { line, ch } = this.doc.getCursor()
|
||||
return new Point(line, ch)
|
||||
}
|
||||
|
||||
setCursorPosition (pos) {
|
||||
this.doc.setCursor({ line: pos.row, ch: pos.column })
|
||||
}
|
||||
|
||||
setSelectionRange (range) {
|
||||
this.doc.setSelection(
|
||||
{ line: range.start.row, ch: range.start.column },
|
||||
{ line: range.end.row, ch: range.end.column }
|
||||
)
|
||||
}
|
||||
|
||||
getLastRow () {
|
||||
return this.doc.lineCount() - 1
|
||||
}
|
||||
|
||||
acceptsTableEdit () {
|
||||
return true
|
||||
}
|
||||
|
||||
getLine (row) {
|
||||
return this.doc.getLine(row)
|
||||
}
|
||||
|
||||
insertLine (row, line) {
|
||||
const lastRow = this.getLastRow()
|
||||
if (row > lastRow) {
|
||||
const lastLine = this.getLine(lastRow)
|
||||
this.doc.replaceRange(
|
||||
'\n' + line,
|
||||
{ line: lastRow, ch: lastLine.length },
|
||||
{ line: lastRow, ch: lastLine.length }
|
||||
)
|
||||
} else {
|
||||
this.doc.replaceRange(
|
||||
line + '\n',
|
||||
{ line: row, ch: 0 },
|
||||
{ line: row, ch: 0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
deleteLine (row) {
|
||||
const lastRow = this.getLastRow()
|
||||
if (row >= lastRow) {
|
||||
if (lastRow > 0) {
|
||||
const preLastLine = this.getLine(lastRow - 1)
|
||||
const lastLine = this.getLine(lastRow)
|
||||
this.doc.replaceRange(
|
||||
'',
|
||||
{ line: lastRow - 1, ch: preLastLine.length },
|
||||
{ line: lastRow, ch: lastLine.length }
|
||||
)
|
||||
} else {
|
||||
const lastLine = this.getLine(lastRow)
|
||||
this.doc.replaceRange(
|
||||
'',
|
||||
{ line: lastRow, ch: 0 },
|
||||
{ line: lastRow, ch: lastLine.length }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
this.doc.replaceRange(
|
||||
'',
|
||||
{ line: row, ch: 0 },
|
||||
{ line: row + 1, ch: 0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
replaceLines (startRow, endRow, lines) {
|
||||
const lastRow = this.getLastRow()
|
||||
if (endRow > lastRow) {
|
||||
const lastLine = this.getLine(lastRow)
|
||||
this.doc.replaceRange(
|
||||
lines.join('\n'),
|
||||
{ line: startRow, ch: 0 },
|
||||
{ line: lastRow, ch: lastLine.length }
|
||||
)
|
||||
} else {
|
||||
this.doc.replaceRange(
|
||||
lines.join('\n') + '\n',
|
||||
{ line: startRow, ch: 0 },
|
||||
{ line: endRow, ch: 0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
transact (func) {
|
||||
this.transaction = true
|
||||
func()
|
||||
this.transaction = false
|
||||
if (this.onDidFinishTransaction) {
|
||||
this.onDidFinishTransaction.call(undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function initTableEditor (editor) {
|
||||
// create an interface to the text editor
|
||||
const editorIntf = new TextEditorInterface(editor)
|
||||
// create a table editor object
|
||||
const tableEditor = new TableEditor(editorIntf)
|
||||
// options for the table editor
|
||||
const opts = options({
|
||||
smartCursor: true,
|
||||
formatType: FormatType.NORMAL
|
||||
})
|
||||
// keymap of the commands
|
||||
// from https://github.com/susisu/mte-demo/blob/master/src/main.js
|
||||
const keyMap = CodeMirror.normalizeKeyMap({
|
||||
Tab: () => { tableEditor.nextCell(opts) },
|
||||
'Shift-Tab': () => { tableEditor.previousCell(opts) },
|
||||
Enter: () => { tableEditor.nextRow(opts) },
|
||||
'Ctrl-Enter': () => { tableEditor.escape(opts) },
|
||||
'Cmd-Enter': () => { tableEditor.escape(opts) },
|
||||
'Shift-Ctrl-Left': () => { tableEditor.alignColumn(Alignment.LEFT, opts) },
|
||||
'Shift-Cmd-Left': () => { tableEditor.alignColumn(Alignment.LEFT, opts) },
|
||||
'Shift-Ctrl-Right': () => { tableEditor.alignColumn(Alignment.RIGHT, opts) },
|
||||
'Shift-Cmd-Right': () => { tableEditor.alignColumn(Alignment.RIGHT, opts) },
|
||||
'Shift-Ctrl-Up': () => { tableEditor.alignColumn(Alignment.CENTER, opts) },
|
||||
'Shift-Cmd-Up': () => { tableEditor.alignColumn(Alignment.CENTER, opts) },
|
||||
'Shift-Ctrl-Down': () => { tableEditor.alignColumn(Alignment.NONE, opts) },
|
||||
'Shift-Cmd-Down': () => { tableEditor.alignColumn(Alignment.NONE, opts) },
|
||||
'Ctrl-Left': () => { tableEditor.moveFocus(0, -1, opts) },
|
||||
'Cmd-Left': () => { tableEditor.moveFocus(0, -1, opts) },
|
||||
'Ctrl-Right': () => { tableEditor.moveFocus(0, 1, opts) },
|
||||
'Cmd-Right': () => { tableEditor.moveFocus(0, 1, opts) },
|
||||
'Ctrl-Up': () => { tableEditor.moveFocus(-1, 0, opts) },
|
||||
'Cmd-Up': () => { tableEditor.moveFocus(-1, 0, opts) },
|
||||
'Ctrl-Down': () => { tableEditor.moveFocus(1, 0, opts) },
|
||||
'Cmd-Down': () => { tableEditor.moveFocus(1, 0, opts) },
|
||||
'Ctrl-K Ctrl-I': () => { tableEditor.insertRow(opts) },
|
||||
'Cmd-K Cmd-I': () => { tableEditor.insertRow(opts) },
|
||||
'Ctrl-L Ctrl-I': () => { tableEditor.deleteRow(opts) },
|
||||
'Cmd-L Cmd-I': () => { tableEditor.deleteRow(opts) },
|
||||
'Ctrl-K Ctrl-J': () => { tableEditor.insertColumn(opts) },
|
||||
'Cmd-K Cmd-J': () => { tableEditor.insertColumn(opts) },
|
||||
'Ctrl-L Ctrl-J': () => { tableEditor.deleteColumn(opts) },
|
||||
'Cmd-L Cmd-J': () => { tableEditor.deleteColumn(opts) },
|
||||
'Alt-Shift-Ctrl-Left': () => { tableEditor.moveColumn(-1, opts) },
|
||||
'Alt-Shift-Cmd-Left': () => { tableEditor.moveColumn(-1, opts) },
|
||||
'Alt-Shift-Ctrl-Right': () => { tableEditor.moveColumn(1, opts) },
|
||||
'Alt-Shift-Cmd-Right': () => { tableEditor.moveColumn(1, opts) },
|
||||
'Alt-Shift-Ctrl-Up': () => { tableEditor.moveRow(-1, opts) },
|
||||
'Alt-Shift-Cmd-Up': () => { tableEditor.moveRow(-1, opts) },
|
||||
'Alt-Shift-Ctrl-Down': () => { tableEditor.moveRow(1, opts) },
|
||||
'Alt-Shift-Cmd-Down': () => { tableEditor.moveRow(1, opts) }
|
||||
})
|
||||
let lastActive
|
||||
// enable keymap if the cursor is in a table
|
||||
function updateActiveState () {
|
||||
const tableTools = $('.toolbar .table-tools')
|
||||
const active = tableEditor.cursorIsInTable(opts)
|
||||
// avoid to update if state not changed
|
||||
if (lastActive === active) {
|
||||
return
|
||||
}
|
||||
if (active) {
|
||||
tableTools.show()
|
||||
tableTools.parent().scrollLeft(tableTools.parent()[0].scrollWidth)
|
||||
editor.addKeyMap(keyMap)
|
||||
} else {
|
||||
tableTools.hide()
|
||||
editor.removeKeyMap(keyMap)
|
||||
tableEditor.resetSmartCursor()
|
||||
}
|
||||
lastActive = active
|
||||
}
|
||||
// event subscriptions
|
||||
editor.on('cursorActivity', () => {
|
||||
if (!editorIntf.transaction) {
|
||||
updateActiveState()
|
||||
}
|
||||
})
|
||||
editor.on('changes', () => {
|
||||
if (!editorIntf.transaction) {
|
||||
updateActiveState()
|
||||
}
|
||||
})
|
||||
editorIntf.onDidFinishTransaction = () => {
|
||||
updateActiveState()
|
||||
}
|
||||
|
||||
return tableEditor
|
||||
}
|
|
@ -44,5 +44,47 @@
|
|||
<i class="fa fa-comment fa-fw"></i>
|
||||
</a>
|
||||
</div>
|
||||
<span class="btn-group table-tools hidden-xs" style="display: none;">
|
||||
<span class="separator" style="margin-left: -10px;">|</span>
|
||||
<span>Row</span>
|
||||
<a id="insertRow" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Insert Row">
|
||||
<i class="fa fa-plus-circle fa-fw"></i>
|
||||
</a>
|
||||
<a id="deleteRow" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Delete Row">
|
||||
<i class="fa fa-minus-circle fa-fw"></i>
|
||||
</a>
|
||||
<a id="moveRowUp" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Move Row Up">
|
||||
<i class="fa fa-long-arrow-up fa-fw"></i>
|
||||
</a>
|
||||
<a id="moveRowDown" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Move Row Down">
|
||||
<i class="fa fa-long-arrow-down fa-fw"></i>
|
||||
</a>
|
||||
<span>Column</span>
|
||||
<a id="insertColumn" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Insert Column">
|
||||
<i class="fa fa-plus-circle fa-fw"></i>
|
||||
</a>
|
||||
<a id="deleteColumn" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Delete Column">
|
||||
<i class="fa fa-minus-circle fa-fw"></i>
|
||||
</a>
|
||||
<a id="moveColumnLeft" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Move Column Left">
|
||||
<i class="fa fa-long-arrow-left fa-fw"></i>
|
||||
</a>
|
||||
<a id="moveColumnRight" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Move Column Right">
|
||||
<i class="fa fa-long-arrow-right fa-fw"></i>
|
||||
</a>
|
||||
<span>Alignment</span>
|
||||
<a id="alignLeft" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Align Left">
|
||||
<i class="fa fa-align-left fa-fw"></i>
|
||||
</a>
|
||||
<a id="alignCenter" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Align Center">
|
||||
<i class="fa fa-align-center fa-fw"></i>
|
||||
</a>
|
||||
<a id="alignRight" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Align Right">
|
||||
<i class="fa fa-align-right fa-fw"></i>
|
||||
</a>
|
||||
<a id="alignNone" class="btn btn-sm btn-dark text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" title="Align None">
|
||||
<i class="fa fa-ban fa-fw"></i>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
* Global UI elements references
|
||||
*/
|
||||
/* global $ */
|
||||
|
||||
export const getUIElements = () => ({
|
||||
spinner: $('.ui-spinner'),
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
/* global CodeMirror, editor */
|
||||
const wrapSymbols = ['*', '_', '~', '^', '+', '=']
|
||||
export function wrapTextWith (editor, cm, symbol) {
|
||||
if (!cm.getSelection()) {
|
||||
return CodeMirror.Pass
|
||||
} else {
|
||||
let ranges = cm.listSelections()
|
||||
const ranges = cm.listSelections()
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
let range = ranges[i]
|
||||
const range = ranges[i]
|
||||
if (!range.empty()) {
|
||||
const from = range.from()
|
||||
const to = range.to()
|
||||
|
||||
if (symbol !== 'Backspace') {
|
||||
let selection = cm.getRange(from, to)
|
||||
let anchorIndex = editor.indexFromPos(ranges[i].anchor)
|
||||
let headIndex = editor.indexFromPos(ranges[i].head)
|
||||
const selection = cm.getRange(from, to)
|
||||
const anchorIndex = editor.indexFromPos(ranges[i].anchor)
|
||||
const headIndex = editor.indexFromPos(ranges[i].head)
|
||||
cm.replaceRange(symbol + selection + symbol, from, to, '+input')
|
||||
if (anchorIndex > headIndex) {
|
||||
ranges[i].anchor.ch += symbol.length
|
||||
|
@ -24,18 +25,18 @@ export function wrapTextWith (editor, cm, symbol) {
|
|||
}
|
||||
cm.setSelections(ranges)
|
||||
} else {
|
||||
let preEndPos = {
|
||||
const preEndPos = {
|
||||
line: to.line,
|
||||
ch: to.ch + symbol.length
|
||||
}
|
||||
let preText = cm.getRange(to, preEndPos)
|
||||
let preIndex = wrapSymbols.indexOf(preText)
|
||||
let postEndPos = {
|
||||
const preText = cm.getRange(to, preEndPos)
|
||||
const preIndex = wrapSymbols.indexOf(preText)
|
||||
const postEndPos = {
|
||||
line: from.line,
|
||||
ch: from.ch - symbol.length
|
||||
}
|
||||
let postText = cm.getRange(postEndPos, from)
|
||||
let postIndex = wrapSymbols.indexOf(postText)
|
||||
const postText = cm.getRange(postEndPos, from)
|
||||
const postIndex = wrapSymbols.indexOf(postText)
|
||||
// check if surround symbol are list in array and matched
|
||||
if (preIndex > -1 && postIndex > -1 && preIndex === postIndex) {
|
||||
cm.replaceRange('', to, preEndPos, '+delete')
|
||||
|
@ -48,25 +49,25 @@ export function wrapTextWith (editor, cm, symbol) {
|
|||
}
|
||||
|
||||
export function insertText (cm, text, cursorEnd = 0) {
|
||||
let cursor = cm.getCursor()
|
||||
const cursor = cm.getCursor()
|
||||
cm.replaceSelection(text, cursor, cursor)
|
||||
cm.focus()
|
||||
cm.setCursor({ line: cursor.line, ch: cursor.ch + cursorEnd })
|
||||
}
|
||||
|
||||
export function insertLink (cm, isImage) {
|
||||
let cursor = cm.getCursor()
|
||||
let ranges = cm.listSelections()
|
||||
const cursor = cm.getCursor()
|
||||
const ranges = cm.listSelections()
|
||||
const linkEnd = '](https://)'
|
||||
const symbol = (isImage) ? '![' : '['
|
||||
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
let range = ranges[i]
|
||||
const range = ranges[i]
|
||||
if (!range.empty()) {
|
||||
const from = range.from()
|
||||
const to = range.to()
|
||||
let anchorIndex = editor.indexFromPos(ranges[i].anchor)
|
||||
let headIndex = editor.indexFromPos(ranges[i].head)
|
||||
const anchorIndex = editor.indexFromPos(ranges[i].anchor)
|
||||
const headIndex = editor.indexFromPos(ranges[i].head)
|
||||
let selection = cm.getRange(from, to)
|
||||
selection = symbol + selection + linkEnd
|
||||
cm.replaceRange(selection, from, to)
|
||||
|
@ -87,9 +88,9 @@ export function insertLink (cm, isImage) {
|
|||
}
|
||||
|
||||
export function insertHeader (cm) {
|
||||
let cursor = cm.getCursor()
|
||||
let startOfLine = { line: cursor.line, ch: 0 }
|
||||
let startOfLineText = cm.getRange(startOfLine, { line: cursor.line, ch: 1 })
|
||||
const cursor = cm.getCursor()
|
||||
const startOfLine = { line: cursor.line, ch: 0 }
|
||||
const startOfLineText = cm.getRange(startOfLine, { line: cursor.line, ch: 1 })
|
||||
// See if it is already a header
|
||||
if (startOfLineText === '#') {
|
||||
cm.replaceRange('#', startOfLine, startOfLine)
|
||||
|
@ -100,11 +101,11 @@ export function insertHeader (cm) {
|
|||
}
|
||||
|
||||
export function insertOnStartOfLines (cm, symbol) {
|
||||
let cursor = cm.getCursor()
|
||||
let ranges = cm.listSelections()
|
||||
const cursor = cm.getCursor()
|
||||
const ranges = cm.listSelections()
|
||||
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
let range = ranges[i]
|
||||
const range = ranges[i]
|
||||
if (!range.empty()) {
|
||||
const from = range.from()
|
||||
const to = range.to()
|
||||
|
|
|
@ -110,6 +110,31 @@ md.use(markdownitContainer, 'success', { render: renderContainer })
|
|||
md.use(markdownitContainer, 'info', { render: renderContainer })
|
||||
md.use(markdownitContainer, 'warning', { render: renderContainer })
|
||||
md.use(markdownitContainer, 'danger', { render: renderContainer })
|
||||
md.use(markdownitContainer, 'spoiler', {
|
||||
validate: function (params) {
|
||||
return params.trim().match(/^spoiler(\s+.*)?$/)
|
||||
},
|
||||
render: function (tokens, idx) {
|
||||
const m = tokens[idx].info.trim().match(/^spoiler(\s+.*)?$/)
|
||||
|
||||
if (tokens[idx].nesting === 1) {
|
||||
// opening tag
|
||||
const startline = tokens[idx].map[0] + 1
|
||||
const endline = tokens[idx].map[1]
|
||||
|
||||
const partClass = `class="part raw" data-startline="${startline}" data-endline="${endline}"`
|
||||
const summary = m[1] && m[1].trim()
|
||||
if (summary) {
|
||||
return `<details ${partClass}><summary>${md.utils.escapeHtml(summary)}</summary>\n`
|
||||
} else {
|
||||
return `<details ${partClass}>\n`
|
||||
}
|
||||
} else {
|
||||
// closing tag
|
||||
return '</details>\n'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
window.preventSyncScrollToEdit = false
|
||||
window.preventSyncScrollToView = false
|
||||
|
@ -155,12 +180,12 @@ const buildMap = _.throttle(buildMapInner, buildMapThrottle)
|
|||
// Optimizations are required only for big texts.
|
||||
function buildMapInner (callback) {
|
||||
if (!viewArea || !markdownArea) return
|
||||
let i, offset, nonEmptyList, pos, a, b, _lineHeightMap, linesCount, acc, _scrollMap
|
||||
let i, pos, a, b, acc
|
||||
|
||||
offset = viewArea.scrollTop() - viewArea.offset().top
|
||||
_scrollMap = []
|
||||
nonEmptyList = []
|
||||
_lineHeightMap = []
|
||||
const offset = viewArea.scrollTop() - viewArea.offset().top
|
||||
const _scrollMap = []
|
||||
const nonEmptyList = []
|
||||
const _lineHeightMap = []
|
||||
viewTop = 0
|
||||
viewBottom = viewArea[0].scrollHeight - viewArea.height()
|
||||
|
||||
|
@ -181,7 +206,7 @@ function buildMapInner (callback) {
|
|||
acc += Math.round(h / lineHeight)
|
||||
}
|
||||
_lineHeightMap.push(acc)
|
||||
linesCount = acc
|
||||
const linesCount = acc
|
||||
|
||||
for (i = 0; i < linesCount; i++) {
|
||||
_scrollMap.push(-1)
|
||||
|
@ -331,11 +356,11 @@ export function syncScrollToView (event, preventAnimate) {
|
|||
}
|
||||
if (viewScrolling) return
|
||||
|
||||
let lineNo, posTo
|
||||
let posTo
|
||||
let topDiffPercent, posToNextDiff
|
||||
const scrollInfo = editor.getScrollInfo()
|
||||
const textHeight = editor.defaultTextHeight()
|
||||
lineNo = Math.floor(scrollInfo.top / textHeight)
|
||||
const lineNo = Math.floor(scrollInfo.top / textHeight)
|
||||
// if reach the last line, will start lerp to the bottom
|
||||
const diffToBottom = (scrollInfo.top + scrollInfo.clientHeight) - (scrollInfo.height - textHeight)
|
||||
if (scrollInfo.height > scrollInfo.clientHeight && diffToBottom > 0) {
|
||||
|
|
|
@ -20,7 +20,7 @@ whiteList['style'] = []
|
|||
// allow kbd tag
|
||||
whiteList['kbd'] = []
|
||||
// allow ifram tag with some safe attributes
|
||||
whiteList['iframe'] = ['allowfullscreen', 'name', 'referrerpolicy', 'sandbox', 'src', 'width', 'height']
|
||||
whiteList['iframe'] = ['allowfullscreen', 'name', 'referrerpolicy', 'src', 'width', 'height']
|
||||
// allow summary tag
|
||||
whiteList['summary'] = []
|
||||
// allow ruby tag
|
||||
|
|
|
@ -26,7 +26,7 @@ function extend () {
|
|||
|
||||
for (const source of arguments) {
|
||||
for (const key in source) {
|
||||
if (source.hasOwnProperty(key)) {
|
||||
if (Object.hasOwnProperty.call(source, key)) {
|
||||
target[key] = source[key]
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,15 @@ const deps = [{
|
|||
}
|
||||
}]
|
||||
|
||||
// options from yaml meta
|
||||
const meta = JSON.parse($('#meta').text())
|
||||
// breaks
|
||||
if (typeof meta.breaks === 'boolean') {
|
||||
md.options.breaks = meta.breaks
|
||||
} else {
|
||||
md.options.breaks = window.defaultUseHardbreak
|
||||
}
|
||||
|
||||
const slideOptions = {
|
||||
separator: '^(\r\n?|\n)---(\r\n?|\n)$',
|
||||
verticalSeparator: '^(\r\n?|\n)----(\r\n?|\n)$'
|
||||
|
@ -70,21 +79,19 @@ const defaultOptions = {
|
|||
dependencies: deps
|
||||
}
|
||||
|
||||
// options from yaml meta
|
||||
const meta = JSON.parse($('#meta').text())
|
||||
var options = meta.slideOptions || {}
|
||||
|
||||
if (options.hasOwnProperty('spotlight')) {
|
||||
if (Object.hasOwnProperty.call(options, 'spotlight')) {
|
||||
defaultOptions.dependencies.push({
|
||||
src: `${serverurl}/build/reveal.js/plugin/spotlight/spotlight.js`
|
||||
})
|
||||
}
|
||||
|
||||
if (options.hasOwnProperty('allottedTime') || options.hasOwnProperty('allottedMinutes')) {
|
||||
if (Object.hasOwnProperty.call(options, 'allottedTime') || Object.hasOwnProperty.call(options, 'allottedMinutes')) {
|
||||
defaultOptions.dependencies.push({
|
||||
src: `${serverurl}/build/reveal.js/plugin/elapsed-time-bar/elapsed-time-bar.js`
|
||||
})
|
||||
if (options.hasOwnProperty('allottedMinutes')) {
|
||||
if (Object.hasOwnProperty.call(options, 'allottedMinutes')) {
|
||||
options.allottedTime = options.allottedMinutes * 60 * 1000
|
||||
}
|
||||
}
|
||||
|
@ -103,12 +110,6 @@ if (meta.dir && typeof meta.dir === 'string' && meta.dir === 'rtl') {
|
|||
} else {
|
||||
options.rtl = false
|
||||
}
|
||||
// breaks
|
||||
if (typeof meta.breaks === 'boolean' && !meta.breaks) {
|
||||
md.options.breaks = false
|
||||
} else {
|
||||
md.options.breaks = true
|
||||
}
|
||||
|
||||
// options from URL query string
|
||||
const queryOptions = Reveal.getQueryHash() || {}
|
||||
|
@ -123,14 +124,14 @@ window.viewAjaxCallback = () => {
|
|||
function renderSlide (event) {
|
||||
if (window.location.search.match(/print-pdf/gi)) {
|
||||
const slides = $('.slides')
|
||||
let title = document.title
|
||||
const title = document.title
|
||||
finishView(slides)
|
||||
document.title = title
|
||||
Reveal.layout()
|
||||
} else {
|
||||
const markdown = $(event.currentSlide)
|
||||
if (!markdown.attr('data-rendered')) {
|
||||
let title = document.title
|
||||
const title = document.title
|
||||
finishView(markdown)
|
||||
markdown.attr('data-rendered', 'true')
|
||||
document.title = title
|
||||
|
|
|
@ -1,28 +1,24 @@
|
|||
import base64url from 'base64url'
|
||||
|
||||
let uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
|
||||
export function checkNoteIdValid (id) {
|
||||
let result = id.match(uuidRegex)
|
||||
if (result && result.length === 1) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
const result = id.match(uuidRegex)
|
||||
return !!(result && result.length === 1)
|
||||
}
|
||||
|
||||
export function encodeNoteId (id) {
|
||||
// remove dashes in UUID and encode in url-safe base64
|
||||
let str = id.replace(/-/g, '')
|
||||
let hexStr = Buffer.from(str, 'hex')
|
||||
const str = id.replace(/-/g, '')
|
||||
const hexStr = Buffer.from(str, 'hex')
|
||||
return base64url.encode(hexStr)
|
||||
}
|
||||
|
||||
export function decodeNoteId (encodedId) {
|
||||
// decode from url-safe base64
|
||||
let id = base64url.toBuffer(encodedId).toString('hex')
|
||||
const id = base64url.toBuffer(encodedId).toString('hex')
|
||||
// add dashes between the UUID string parts
|
||||
let idParts = []
|
||||
const idParts = []
|
||||
idParts.push(id.substr(0, 8))
|
||||
idParts.push(id.substr(8, 4))
|
||||
idParts.push(id.substr(12, 4))
|
||||
|
|
Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 358 KiB |
|
@ -15,12 +15,12 @@
|
|||
<a id="permissionLabel" class="ui-permission-label text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="permissionLabel">
|
||||
<li class="ui-permission-freely"<% if(!allowAnonymous && !allowAnonymousEdits) { %> style="display: none;"<% } %>><a><i class="fa fa-leaf fa-fw"></i> Freely - Anyone can edit</a></li>
|
||||
<li class="ui-permission-editable"><a><i class="fa fa-shield fa-fw"></i> Editable - Signed-in people can edit</a></li>
|
||||
<li class="ui-permission-limited"><a><i class="fa fa-id-card fa-fw"></i> Limited - Signed-in people can edit (forbid guests)</a></li>
|
||||
<li class="ui-permission-locked"><a><i class="fa fa-lock fa-fw"></i> Locked - Only owner can edit</a></li>
|
||||
<li class="ui-permission-protected"><a><i class="fa fa-umbrella fa-fw"></i> Protected - Only owner can edit (forbid guests)</a></li>
|
||||
<li class="ui-permission-private"><a><i class="fa fa-hand-stop-o fa-fw"></i> Private - Only owner can view & edit</a></li>
|
||||
<li class="ui-permission-freely"<% if(!('freely' in permission)) { %> style="display: none;"<% } %>><a><i class="fa fa-leaf fa-fw"></i> Freely - Anyone can edit</a></li>
|
||||
<li class="ui-permission-editable"<% if(!('editable' in permission)) { %> style="display: none;"<% } %>><a><i class="fa fa-shield fa-fw"></i> Editable - Signed-in people can edit</a></li>
|
||||
<li class="ui-permission-limited"<% if(!('limited' in permission)) { %> style="display: none;"<% } %>><a><i class="fa fa-id-card fa-fw"></i> Limited - Signed-in people can edit (forbid guests)</a></li>
|
||||
<li class="ui-permission-locked"<% if(!('locked' in permission)) { %> style="display: none;"<% } %>><a><i class="fa fa-lock fa-fw"></i> Locked - Only owner can edit</a></li>
|
||||
<li class="ui-permission-protected"<% if(!('protected' in permission)) { %> style="display: none;"<% } %>><a><i class="fa fa-umbrella fa-fw"></i> Protected - Only owner can edit (forbid guests)</a></li>
|
||||
<li class="ui-permission-private"<% if(!('private' in permission)) { %> style="display: none;"<% } %>><a><i class="fa fa-hand-stop-o fa-fw"></i> Private - Only owner can view & edit</a></li>
|
||||
<li class="divider"></li>
|
||||
<li class="ui-delete-note"><a><i class="fa fa-trash-o fa-fw"></i> Delete this note</a></li>
|
||||
</ul>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script src="<%= serverURL %>/js/mathjax-config-extra.js"></script>
|
||||
<% if(useCDN) { %>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2/spin.min.js" integrity="sha256-PieqE0QdEDMppwXrTzSZQr6tWFX3W5KkyRVyF1zN3eg=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.4.0/velocity.min.js" integrity="sha256-bhm0lgEt6ITaZCDzZpkr/VXVrLa5RP4u9v2AYsbzSUk=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.0/js/bootstrap.min.js" integrity="sha256-kJrlY+s09+QoWjpkOrXXwhxeaoDz9FW5SaxF8I0DibQ=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-mousewheel/3.1.13/jquery.mousewheel.min.js" integrity="sha256-jnOjDTXIPqall8M0MyTSt98JetJuZ7Yu+1Jm7hLTF7U=" crossorigin="anonymous" defer></script>
|
||||
|
@ -9,17 +9,21 @@
|
|||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js" integrity="sha256-yYfngbEKv4RENfGDvNUqJTqGFcKf31NJEe9OTnnMH3Y=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/config/TeX-AMS-MML_HTMLorMML.js" integrity="sha256-immzXfCGLhnx3Zfi9F/dUcqxEM8K3o3oTFy9Bh6HCwg=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/config/Safe.js" integrity="sha256-0ygBUDksNDXZS4vm5HMNH1a33KUu6QT1cdNTN+ZLF+4=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment-with-locales.min.js" integrity="sha256-vvT7Ok9u6GbfnBPXnbM6FVDEO8E1kTdgHOFZOAXrktA=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/7.1.0/mermaid.min.js" integrity="sha256-M3OC0Q6g4/+Q4j73OvnsnA+lMkdAE5KgupRHqTiPbnI=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment-with-locales.min.js" integrity="sha256-AdQN98MVZs44Eq2yTwtoKufhnU+uZ7v2kXnD5vqzZVo=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.2.3/mermaid.min.js" integrity="sha256-4s3fF5e1iWRLtiV7mRev7n17oALqqDHbWrNqF3/r7jU=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/emojify.js/1.1.0/js/emojify.min.js" integrity="sha256-VAB5tAlKBvgaxw8oJ1crWMVbdmBVl4mP/2M8MNRl+4E=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.2/lodash.min.js" integrity="sha256-Cv5v4i4SuYvwRYzIONifZjoc99CkwfncROMSWat1cVA=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js" integrity="sha256-ji09tECORKvr8xB9iCl8DJ8iNMLriDchC1+p+yt1hSs=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.6/handlebars.min.js" integrity="sha256-1O3BtOwnPyyRzOszK6P+gqaRoXHV6JXj8HkjZmPYhCI=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js" integrity="sha256-/BfiIkHlHoVihZdc6TFuj7MmJ0TWcWsMXkeDFwhi0zw=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.1.2/handlebars.min.js" integrity="sha256-ngJY93C4H39YbmrWhnLzSyiepRuQDVKDNCWO2iyMzFw=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/highlight.min.js" integrity="sha256-1zu+3BnLYV9LdiY85uXMzii3bdrkelyp37e0ZyTAQh0=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gist-embed/2.6.0/gist-embed.min.js" integrity="sha256-KyF2D6xPIJUW5sUDSs93vWyZm+1RzIpKCexxElmxl8g=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/1.7.0/viz.js" integrity="sha256-8t+rndrF+TU4JtelmOH1lDHTMe2ovhO2UbzDArp5lY8=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/1000hz-bootstrap-validator/0.11.8/validator.min.js" integrity="sha256-LHeY7YoYJ0SSXbCx7sR14Pqna+52moaH3bhv0Mjzd/M=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/abcjs/3.1.1/abcjs_basic-min.js" integrity="sha256-Sq1r2XXWXQoShQKsS0Wrf5r7fRkErd9Fat9vHYeU68s=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/2.1.2/viz.js" integrity="sha256-8RHyK+AFzq9iXwbFo2unqidwPbwHU5FFWe3RwkcVtuU=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/2.1.2/full.render.js" integrity="sha256-Ogqs510LFnekr9o7OLdpelaaAmNss9egQRTyzCqV2NQ=" crossorigin="anonymous" defer></script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/abcjs/3.1.1/abcjs_basic-min.js" integrity="sha256-Sq1r2XXWXQoShQKsS0Wrf5r7fRkErd9Fat9vHYeU68s=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vega/5.4.0/vega.min.js" integrity="sha256-PrkRj4B3I5V9yHBLdO3jyyqNUwSKS1CXXIh3VrnFPEU=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vega-lite/3.4.0/vega-lite.min.js" integrity="sha256-ro+FWr16NboXJ5rSwInNli1P16ObUXnWUJMgKc8KnHI=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vega-embed/4.2.2/vega-embed.min.js" integrity="sha256-FvIpRBgWEczIjFNpbshtVJbx3QlxqxkBkf+xqZeOxUU=" crossorigin="anonymous" defer></script>
|
||||
<%- include ../build/index-scripts %>
|
||||
<% } else { %>
|
||||
<script src="<%- serverURL %>/build/MathJax/MathJax.js" defer></script>
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
<div id="ui-toc-affix" class="ui-affix-toc ui-toc-dropdown unselectable hidden-print" data-spy="affix" style="top:17px;display:none;" {{{lang}}} {{{dir}}}>
|
||||
{{{ui-toc-affix}}}
|
||||
</div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.0/js/bootstrap.min.js" integrity="sha256-kJrlY+s09+QoWjpkOrXXwhxeaoDz9FW5SaxF8I0DibQ=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gist-embed/2.6.0/gist-embed.min.js" integrity="sha256-KyF2D6xPIJUW5sUDSs93vWyZm+1RzIpKCexxElmxl8g=" crossorigin="anonymous" defer></script>
|
||||
<script>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<% if(useCDN) { %>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.4.0/velocity.min.js" integrity="sha256-bhm0lgEt6ITaZCDzZpkr/VXVrLa5RP4u9v2AYsbzSUk=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.5.2/velocity.min.js" integrity="sha256-1HqoI76JGKA17K0C0s9K8L/iy8PAC43KVLt1hRD/Ojc=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.0/js/bootstrap.min.js" integrity="sha256-kJrlY+s09+QoWjpkOrXXwhxeaoDz9FW5SaxF8I0DibQ=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/list.pagination.js/0.1.1/list.pagination.min.js" integrity="sha256-WwTza96H3BgcQTfEfxX7MFaFc/dZA0QrPRKDRLdFHJo=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.min.js" integrity="sha256-HzzZFiY4t0PIv02Tm8/R3CVvLpcjHhO1z/YAUCp4oQ4=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment-with-locales.min.js" integrity="sha256-vvT7Ok9u6GbfnBPXnbM6FVDEO8E1kTdgHOFZOAXrktA=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment-with-locales.min.js" integrity="sha256-AdQN98MVZs44Eq2yTwtoKufhnU+uZ7v2kXnD5vqzZVo=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-url/2.3.0/url.min.js" integrity="sha256-HOZJz4x+1mn1Si84WT5XKXPtOlTytmZLnMb6n1v4+5Q=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/1000hz-bootstrap-validator/0.11.8/validator.min.js" integrity="sha256-LHeY7YoYJ0SSXbCx7sR14Pqna+52moaH3bhv0Mjzd/M=" crossorigin="anonymous" defer></script>
|
||||
<%- include ../build/cover-scripts %>
|
||||
|
|
|
@ -11,10 +11,10 @@
|
|||
<link rel="apple-touch-icon" href="<%- serverURL %>/apple-touch-icon.png">
|
||||
<% if(useCDN) { %>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.0/css/bootstrap.min.css" integrity="sha256-H0KfTigpUV+0/5tn2HXC0CPwhhDhWgSawJdnFd0CGCo=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fork-awesome/1.1.3/css/fork-awesome.min.css" integrity="sha256-ZhApazu+kejqTYhMF+1DzNKjIzP7KXu6AzyXcC1gMus=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fork-awesome/1.1.7/css/fork-awesome.min.css" integrity="sha256-gsmEoJAws/Kd3CjuOQzLie5Q3yshhvmo7YNtBG7aaEY=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-social/4.9.0/bootstrap-social.min.css" integrity="sha256-02JtFTurpwBjQJ6q13iJe82/NF0RbZlJroDegK5g87Y=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/3.5.4/select2.min.css" integrity="sha256-ijlUKKj3hJCiiT2HWo1kqkI79NTEYpzOsw5Rs3k42dI=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/3.5.4/select2-bootstrap.min.css" integrity="sha256-NAWFcNIZdH+TS1xpWujF/EB/Y8gwBbEOCoaK/eqaer8=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.min.css" integrity="sha256-k5tPXFBQl+dOk8OmqCtptRa7bRYNRJuvs37bcqsmDB0=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2-bootstrap.min.css" integrity="sha256-HbewCP50syA/2d3zPv+/CdQ4ufX6bI2ntjD3MwsA0UE=" crossorigin="anonymous" />
|
||||
<%- include ../build/cover-header %>
|
||||
<%- include ../shared/polyfill %>
|
||||
<% } else { %>
|
||||
|
|
|
@ -73,22 +73,26 @@
|
|||
</html>
|
||||
<script src="<%= serverURL %>/js/mathjax-config-extra.js"></script>
|
||||
<% if(useCDN) { %>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.4.0/velocity.min.js" integrity="sha256-bhm0lgEt6ITaZCDzZpkr/VXVrLa5RP4u9v2AYsbzSUk=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.5.2/velocity.min.js" integrity="sha256-1HqoI76JGKA17K0C0s9K8L/iy8PAC43KVLt1hRD/Ojc=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.0/js/bootstrap.min.js" integrity="sha256-kJrlY+s09+QoWjpkOrXXwhxeaoDz9FW5SaxF8I0DibQ=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-mousewheel/3.1.13/jquery.mousewheel.min.js" integrity="sha256-jnOjDTXIPqall8M0MyTSt98JetJuZ7Yu+1Jm7hLTF7U=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/3.7.0/js-yaml.min.js" integrity="sha256-8PanqYAVOGlOct+i65R+HqibK3KPsXINnrSfxN+Y/J0=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js" integrity="sha256-yYfngbEKv4RENfGDvNUqJTqGFcKf31NJEe9OTnnMH3Y=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/config/TeX-AMS-MML_HTMLorMML.js" integrity="sha256-immzXfCGLhnx3Zfi9F/dUcqxEM8K3o3oTFy9Bh6HCwg=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/config/Safe.js" integrity="sha256-0ygBUDksNDXZS4vm5HMNH1a33KUu6QT1cdNTN+ZLF+4=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment-with-locales.min.js" integrity="sha256-vvT7Ok9u6GbfnBPXnbM6FVDEO8E1kTdgHOFZOAXrktA=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/7.1.0/mermaid.min.js" integrity="sha256-M3OC0Q6g4/+Q4j73OvnsnA+lMkdAE5KgupRHqTiPbnI=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment-with-locales.min.js" integrity="sha256-AdQN98MVZs44Eq2yTwtoKufhnU+uZ7v2kXnD5vqzZVo=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.2.3/mermaid.min.js" integrity="sha256-4s3fF5e1iWRLtiV7mRev7n17oALqqDHbWrNqF3/r7jU=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/emojify.js/1.1.0/js/emojify.min.js" integrity="sha256-VAB5tAlKBvgaxw8oJ1crWMVbdmBVl4mP/2M8MNRl+4E=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.6/handlebars.min.js" integrity="sha256-1O3BtOwnPyyRzOszK6P+gqaRoXHV6JXj8HkjZmPYhCI=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js" integrity="sha256-/BfiIkHlHoVihZdc6TFuj7MmJ0TWcWsMXkeDFwhi0zw=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.1.2/handlebars.min.js" integrity="sha256-ngJY93C4H39YbmrWhnLzSyiepRuQDVKDNCWO2iyMzFw=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/highlight.min.js" integrity="sha256-1zu+3BnLYV9LdiY85uXMzii3bdrkelyp37e0ZyTAQh0=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gist-embed/2.6.0/gist-embed.min.js" integrity="sha256-KyF2D6xPIJUW5sUDSs93vWyZm+1RzIpKCexxElmxl8g=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/1.7.0/viz.js" integrity="sha256-8t+rndrF+TU4JtelmOH1lDHTMe2ovhO2UbzDArp5lY8=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/abcjs/3.1.1/abcjs_basic-min.js" integrity="sha256-Sq1r2XXWXQoShQKsS0Wrf5r7fRkErd9Fat9vHYeU68s=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/2.1.2/viz.js" integrity="sha256-8RHyK+AFzq9iXwbFo2unqidwPbwHU5FFWe3RwkcVtuU=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/2.1.2/full.render.js" integrity="sha256-Ogqs510LFnekr9o7OLdpelaaAmNss9egQRTyzCqV2NQ=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/abcjs/3.1.1/abcjs_basic-min.js" integrity="sha256-Sq1r2XXWXQoShQKsS0Wrf5r7fRkErd9Fat9vHYeU68s=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vega/5.4.0/vega.min.js" integrity="sha256-PrkRj4B3I5V9yHBLdO3jyyqNUwSKS1CXXIh3VrnFPEU=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vega-lite/3.4.0/vega-lite.min.js" integrity="sha256-ro+FWr16NboXJ5rSwInNli1P16ObUXnWUJMgKc8KnHI=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vega-embed/4.2.2/vega-embed.min.js" integrity="sha256-FvIpRBgWEczIjFNpbshtVJbx3QlxqxkBkf+xqZeOxUU=" crossorigin="anonymous" defer></script>
|
||||
<%- include build/pretty-scripts %>
|
||||
<% } else { %>
|
||||
<script src="<%- serverURL %>/build/MathJax/MathJax.js" defer></script>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<link rel="apple-touch-icon" href="<%- serverURL %>/apple-touch-icon.png">
|
||||
|
||||
<% if(useCDN) { %>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fork-awesome/1.1.3/css/fork-awesome.min.css" integrity="sha256-ZhApazu+kejqTYhMF+1DzNKjIzP7KXu6AzyXcC1gMus=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fork-awesome/1.1.7/css/fork-awesome.min.css" integrity="sha256-gsmEoJAws/Kd3CjuOQzLie5Q3yshhvmo7YNtBG7aaEY=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ionicons/2.0.1/css/ionicons.min.css" integrity="sha256-3iu9jgsy9TpTwXKb7bNQzqWekRX7pPK+2OLj3R922fo=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.7.0/css/reveal.min.css" integrity="sha256-9+Wg2bcNeiOMGXOUNqBdceY2lAH/eCiTDcdzHhHIl48=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/emojify.js/1.1.0/css/basic/emojify.min.css" integrity="sha256-UOrvMOsSDSrW6szVLe8ZDZezBxh5IoIfgTwdNDgTjiU=" crossorigin="anonymous" />
|
||||
|
@ -90,21 +90,25 @@
|
|||
<% if(useCDN) { %>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.7.0/lib/js/head.min.js" integrity="sha256-CTcwyen1cxIrm4hlqdxe0y7Hq6B0rpxAKLiXMD3dJv0=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.7.0/js/reveal.min.js" integrity="sha256-Xr6ZH+/kc7hDVReZLO5khBknteLqu5oen/xnSraXrVk=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.4.0/velocity.min.js" integrity="sha256-bhm0lgEt6ITaZCDzZpkr/VXVrLa5RP4u9v2AYsbzSUk=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.5.2/velocity.min.js" integrity="sha256-1HqoI76JGKA17K0C0s9K8L/iy8PAC43KVLt1hRD/Ojc=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-mousewheel/3.1.13/jquery.mousewheel.min.js" integrity="sha256-jnOjDTXIPqall8M0MyTSt98JetJuZ7Yu+1Jm7hLTF7U=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/3.7.0/js-yaml.min.js" integrity="sha256-8PanqYAVOGlOct+i65R+HqibK3KPsXINnrSfxN+Y/J0=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/3.13.1/js-yaml.min.js" integrity="sha256-ry6nlLQ1JmRl5RUPQ4eSuaSp/rGNPvl144WHHs7BiNE=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js" integrity="sha256-yYfngbEKv4RENfGDvNUqJTqGFcKf31NJEe9OTnnMH3Y=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/config/TeX-AMS-MML_HTMLorMML.js" integrity="sha256-immzXfCGLhnx3Zfi9F/dUcqxEM8K3o3oTFy9Bh6HCwg=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/config/Safe.js" integrity="sha256-0ygBUDksNDXZS4vm5HMNH1a33KUu6QT1cdNTN+ZLF+4=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment-with-locales.min.js" integrity="sha256-vvT7Ok9u6GbfnBPXnbM6FVDEO8E1kTdgHOFZOAXrktA=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/7.1.0/mermaid.min.js" integrity="sha256-M3OC0Q6g4/+Q4j73OvnsnA+lMkdAE5KgupRHqTiPbnI=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment-with-locales.min.js" integrity="sha256-AdQN98MVZs44Eq2yTwtoKufhnU+uZ7v2kXnD5vqzZVo=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.2.3/mermaid.min.js" integrity="sha256-4s3fF5e1iWRLtiV7mRev7n17oALqqDHbWrNqF3/r7jU=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/emojify.js/1.1.0/js/emojify.min.js" integrity="sha256-VAB5tAlKBvgaxw8oJ1crWMVbdmBVl4mP/2M8MNRl+4E=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.6/handlebars.min.js" integrity="sha256-1O3BtOwnPyyRzOszK6P+gqaRoXHV6JXj8HkjZmPYhCI=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js" integrity="sha256-/BfiIkHlHoVihZdc6TFuj7MmJ0TWcWsMXkeDFwhi0zw=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.1.2/handlebars.min.js" integrity="sha256-ngJY93C4H39YbmrWhnLzSyiepRuQDVKDNCWO2iyMzFw=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/highlight.min.js" integrity="sha256-1zu+3BnLYV9LdiY85uXMzii3bdrkelyp37e0ZyTAQh0=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gist-embed/2.6.0/gist-embed.min.js" integrity="sha256-KyF2D6xPIJUW5sUDSs93vWyZm+1RzIpKCexxElmxl8g=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/1.7.0/viz.js" integrity="sha256-8t+rndrF+TU4JtelmOH1lDHTMe2ovhO2UbzDArp5lY8=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/abcjs/3.1.1/abcjs_basic-min.js" integrity="sha256-Sq1r2XXWXQoShQKsS0Wrf5r7fRkErd9Fat9vHYeU68s=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/2.1.2/viz.js" integrity="sha256-8RHyK+AFzq9iXwbFo2unqidwPbwHU5FFWe3RwkcVtuU=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/2.1.2/full.render.js" integrity="sha256-Ogqs510LFnekr9o7OLdpelaaAmNss9egQRTyzCqV2NQ=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/abcjs/3.1.1/abcjs_basic-min.js" integrity="sha256-Sq1r2XXWXQoShQKsS0Wrf5r7fRkErd9Fat9vHYeU68s=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vega/5.4.0/vega.min.js" integrity="sha256-PrkRj4B3I5V9yHBLdO3jyyqNUwSKS1CXXIh3VrnFPEU=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vega-lite/3.4.0/vega-lite.min.js" integrity="sha256-ro+FWr16NboXJ5rSwInNli1P16ObUXnWUJMgKc8KnHI=" crossorigin="anonymous" defer></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vega-embed/4.2.2/vega-embed.min.js" integrity="sha256-FvIpRBgWEczIjFNpbshtVJbx3QlxqxkBkf+xqZeOxUU=" crossorigin="anonymous" defer></script>
|
||||
<%- include build/slide-scripts %>
|
||||
<% } else { %>
|
||||
<script src="<%- serverURL %>/build/MathJax/MathJax.js" defer></script>
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
/* eslint-env node, mocha */
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const sinon = require('sinon')
|
||||
|
||||
const { ProcessQueue } = require('../lib/processQueue')
|
||||
|
||||
describe('ProcessQueue', function () {
|
||||
let clock
|
||||
const waitTimeForCheckResult = 50
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers({
|
||||
toFake: ['setInterval']
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore()
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('should not accept more than maximum task', () => {
|
||||
const queue = new ProcessQueue({ maximumLength: 2 })
|
||||
|
||||
queue.start()
|
||||
assert(queue.push(1, () => (Promise.resolve())))
|
||||
assert(queue.push(1, () => (Promise.resolve())) === false)
|
||||
})
|
||||
|
||||
it('should run task every interval', (done) => {
|
||||
const runningClock = []
|
||||
const queue = new ProcessQueue({ maximumLength: 2 })
|
||||
const task = async () => {
|
||||
runningClock.push(clock.now)
|
||||
}
|
||||
queue.start()
|
||||
assert(queue.push(1, task))
|
||||
assert(queue.push(2, task))
|
||||
clock.tick(5)
|
||||
setTimeout(() => {
|
||||
clock.tick(5)
|
||||
}, 1)
|
||||
setTimeout(() => {
|
||||
clock.tick(5)
|
||||
}, 2)
|
||||
setTimeout(() => {
|
||||
clock.tick(5)
|
||||
}, 3)
|
||||
|
||||
setTimeout(() => {
|
||||
queue.stop()
|
||||
assert(runningClock.length === 2)
|
||||
done()
|
||||
}, waitTimeForCheckResult)
|
||||
})
|
||||
|
||||
it('should not crash when repeat stop queue', () => {
|
||||
const queue = new ProcessQueue({ maximumLength: 2, triggerTimeInterval: 10 })
|
||||
try {
|
||||
queue.stop()
|
||||
queue.stop()
|
||||
queue.stop()
|
||||
assert.ok(true)
|
||||
} catch (e) {
|
||||
assert.fail(e)
|
||||
}
|
||||
})
|
||||
|
||||
it('should run process when queue is empty', (done) => {
|
||||
const queue = new ProcessQueue({ maximumLength: 2, triggerTimeInterval: 100 })
|
||||
const processSpy = sinon.spy(queue, 'process')
|
||||
queue.start()
|
||||
clock.tick(100)
|
||||
setTimeout(() => {
|
||||
assert(processSpy.called)
|
||||
done()
|
||||
}, waitTimeForCheckResult)
|
||||
})
|
||||
|
||||
it('should run process although error occurred', (done) => {
|
||||
const queue = new ProcessQueue({ maximumLength: 2, triggerTimeInterval: 100 })
|
||||
const failedTask = sinon.spy(async () => {
|
||||
throw new Error('error')
|
||||
})
|
||||
const normalTask = sinon.spy(async () => {
|
||||
})
|
||||
queue.start()
|
||||
assert(queue.push(1, failedTask))
|
||||
assert(queue.push(2, normalTask))
|
||||
clock.tick(100)
|
||||
setTimeout(() => {
|
||||
clock.tick(100)
|
||||
}, 1)
|
||||
setTimeout(() => {
|
||||
// assert(queue.queue.length === 0)
|
||||
assert(failedTask.called)
|
||||
assert(normalTask.called)
|
||||
done()
|
||||
}, waitTimeForCheckResult)
|
||||
})
|
||||
|
||||
it('should ignore trigger when event not complete', (done) => {
|
||||
const queue = new ProcessQueue({ maximumLength: 2, triggerTimeInterval: 10 })
|
||||
const processSpy = sinon.spy(queue, 'process')
|
||||
const longTask = async () => {
|
||||
return new Promise((resolve) => {
|
||||
setInterval(() => {
|
||||
resolve()
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
queue.start()
|
||||
queue.push(1, longTask)
|
||||
clock.tick(10)
|
||||
setTimeout(() => {
|
||||
clock.tick(10)
|
||||
}, 0)
|
||||
setTimeout(() => {
|
||||
clock.tick(10)
|
||||
}, 1)
|
||||
setTimeout(() => {
|
||||
assert(processSpy.callCount === 1)
|
||||
assert(processSpy.calledOnce)
|
||||
done()
|
||||
}, waitTimeForCheckResult)
|
||||
})
|
||||
})
|
10
test/csp.js
|
@ -43,7 +43,7 @@ describe('Content security policies', function () {
|
|||
|
||||
// beginnging Tests
|
||||
it('Disable CDN', function () {
|
||||
let testconfig = defaultConfig
|
||||
const testconfig = defaultConfig
|
||||
testconfig.useCDN = false
|
||||
mock('../lib/config', testconfig)
|
||||
csp = mock.reRequire('../lib/csp')
|
||||
|
@ -57,7 +57,7 @@ describe('Content security policies', function () {
|
|||
})
|
||||
|
||||
it('Disable Google Analytics', function () {
|
||||
let testconfig = defaultConfig
|
||||
const testconfig = defaultConfig
|
||||
testconfig.csp.addGoogleAnalytics = false
|
||||
mock('../lib/config', testconfig)
|
||||
csp = mock.reRequire('../lib/csp')
|
||||
|
@ -66,7 +66,7 @@ describe('Content security policies', function () {
|
|||
})
|
||||
|
||||
it('Disable Disqus', function () {
|
||||
let testconfig = defaultConfig
|
||||
const testconfig = defaultConfig
|
||||
testconfig.csp.addDisqus = false
|
||||
mock('../lib/config', testconfig)
|
||||
csp = mock.reRequire('../lib/csp')
|
||||
|
@ -79,7 +79,7 @@ describe('Content security policies', function () {
|
|||
})
|
||||
|
||||
it('Set ReportURI', function () {
|
||||
let testconfig = defaultConfig
|
||||
const testconfig = defaultConfig
|
||||
testconfig.csp.reportURI = 'https://example.com/reportURI'
|
||||
mock('../lib/config', testconfig)
|
||||
csp = mock.reRequire('../lib/csp')
|
||||
|
@ -88,7 +88,7 @@ describe('Content security policies', function () {
|
|||
})
|
||||
|
||||
it('Set own directives', function () {
|
||||
let testconfig = defaultConfig
|
||||
const testconfig = defaultConfig
|
||||
mock('../lib/config', defaultConfig)
|
||||
csp = mock.reRequire('../lib/csp')
|
||||
const unextendedCSP = csp.computeDirectives()
|
||||
|
|
|
@ -9,7 +9,7 @@ describe('generateAvatarURL() gravatar enabled', function () {
|
|||
let avatars
|
||||
beforeEach(function () {
|
||||
// Reset config to make sure we don't influence other tests
|
||||
let testconfig = {
|
||||
const testconfig = {
|
||||
allowGravatar: true,
|
||||
serverURL: 'http://localhost:3000',
|
||||
port: 3000
|
||||
|
@ -32,7 +32,7 @@ describe('generateAvatarURL() gravatar disabled', function () {
|
|||
let avatars
|
||||
beforeEach(function () {
|
||||
// Reset config to make sure we don't influence other tests
|
||||
let testconfig = {
|
||||
const testconfig = {
|
||||
allowGravatar: false,
|
||||
serverURL: 'http://localhost:3000',
|
||||
port: 3000
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/* eslint-env node, mocha */
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const mock = require('mock-require')
|
||||
const sinon = require('sinon')
|
||||
const { removeModuleFromRequireCache, makeMockSocket } = require('./utils')
|
||||
|
||||
describe('cleanDanglingUser', function () {
|
||||
let clock
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers()
|
||||
mock('../../lib/processQueue', require('../testDoubles/ProcessQueueFake'))
|
||||
mock('../../lib/logger', {
|
||||
error: () => {},
|
||||
info: () => {}
|
||||
})
|
||||
mock('../../lib/history', {})
|
||||
mock('../../lib/models', {
|
||||
Revision: {
|
||||
saveAllNotesRevision: () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
mock('../../lib/config', {
|
||||
debug: true
|
||||
})
|
||||
mock('../../lib/realtimeUpdateDirtyNoteJob', require('../testDoubles/realtimeJobStub'))
|
||||
mock('../../lib/realtimeSaveRevisionJob', require('../testDoubles/realtimeJobStub'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore()
|
||||
removeModuleFromRequireCache('../../lib/realtime')
|
||||
mock.stopAll()
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('should call queueForDisconnectSpy when user is dangling', (done) => {
|
||||
const realtime = require('../../lib/realtime')
|
||||
const queueForDisconnectSpy = sinon.spy(realtime, 'queueForDisconnect')
|
||||
realtime.io = {
|
||||
to: sinon.stub().callsFake(function () {
|
||||
return {
|
||||
emit: sinon.fake()
|
||||
}
|
||||
}),
|
||||
sockets: {
|
||||
connected: {}
|
||||
}
|
||||
}
|
||||
const user1Socket = makeMockSocket()
|
||||
const user2Socket = makeMockSocket()
|
||||
|
||||
user1Socket.rooms.push('room1')
|
||||
|
||||
realtime.io.sockets.connected[user1Socket.id] = user1Socket
|
||||
realtime.io.sockets.connected[user2Socket.id] = user2Socket
|
||||
|
||||
realtime.users[user1Socket.id] = user1Socket
|
||||
realtime.users[user2Socket.id] = user2Socket
|
||||
clock.tick(60000)
|
||||
clock.restore()
|
||||
setTimeout(() => {
|
||||
assert(queueForDisconnectSpy.called)
|
||||
done()
|
||||
}, 50)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,193 @@
|
|||
/* eslint-env node, mocha */
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const mock = require('mock-require')
|
||||
const sinon = require('sinon')
|
||||
|
||||
const { createFakeLogger } = require('../testDoubles/loggerFake')
|
||||
const { removeLibModuleCache, makeMockSocket } = require('./utils')
|
||||
const realtimeJobStub = require('../testDoubles/realtimeJobStub')
|
||||
|
||||
describe('realtime#connection', function () {
|
||||
describe('connection', function () {
|
||||
let realtime
|
||||
let modelStub
|
||||
|
||||
beforeEach(() => {
|
||||
removeLibModuleCache()
|
||||
modelStub = {
|
||||
Note: {
|
||||
findOne: sinon.stub()
|
||||
},
|
||||
User: {},
|
||||
Author: {}
|
||||
}
|
||||
mock('../../lib/logger', createFakeLogger())
|
||||
mock('../../lib/history', {})
|
||||
mock('../../lib/models', modelStub)
|
||||
mock('../../lib/config', {})
|
||||
mock('../../lib/realtimeUpdateDirtyNoteJob', realtimeJobStub)
|
||||
mock('../../lib/realtimeCleanDanglingUserJob', realtimeJobStub)
|
||||
mock('../../lib/realtimeSaveRevisionJob', realtimeJobStub)
|
||||
mock('../../lib/ot', require('../testDoubles/otFake'))
|
||||
realtime = require('../../lib/realtime')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.stopAll()
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
describe('fail', function () {
|
||||
it('should fast return when server not start', () => {
|
||||
const mockSocket = makeMockSocket()
|
||||
realtime.maintenance = true
|
||||
const spy = sinon.spy(realtime, 'parseNoteIdFromSocketAsync')
|
||||
realtime.connection(mockSocket)
|
||||
assert(!spy.called)
|
||||
})
|
||||
|
||||
it('should failed when parse noteId occur error', (done) => {
|
||||
const mockSocket = makeMockSocket()
|
||||
realtime.maintenance = false
|
||||
const parseNoteIdFromSocketSpy = sinon.stub(realtime, 'parseNoteIdFromSocketAsync').callsFake(async (socket) => {
|
||||
/* eslint-disable-next-line */
|
||||
throw 'error'
|
||||
})
|
||||
|
||||
const failConnectionSpy = sinon.stub(realtime, 'failConnection')
|
||||
|
||||
realtime.connection(mockSocket)
|
||||
|
||||
setTimeout(() => {
|
||||
assert(parseNoteIdFromSocketSpy.called)
|
||||
assert(failConnectionSpy.calledOnce)
|
||||
assert.deepStrictEqual(failConnectionSpy.lastCall.args, [500, 'error', mockSocket])
|
||||
done()
|
||||
}, 50)
|
||||
})
|
||||
|
||||
it('should failed when noteId not exists', (done) => {
|
||||
const mockSocket = makeMockSocket()
|
||||
realtime.maintenance = false
|
||||
const parseNoteIdFromSocketSpy = sinon.stub(realtime, 'parseNoteIdFromSocketAsync').callsFake(async (socket) => {
|
||||
return null
|
||||
})
|
||||
|
||||
const failConnectionSpy = sinon.stub(realtime, 'failConnection')
|
||||
|
||||
realtime.connection(mockSocket)
|
||||
|
||||
setTimeout(() => {
|
||||
assert(parseNoteIdFromSocketSpy.called)
|
||||
assert(failConnectionSpy.calledOnce)
|
||||
assert.deepStrictEqual(failConnectionSpy.lastCall.args, [404, 'note id not found', mockSocket])
|
||||
done()
|
||||
}, 50)
|
||||
})
|
||||
})
|
||||
|
||||
it('should success connect', function (done) {
|
||||
const mockSocket = makeMockSocket()
|
||||
const noteId = 'note123'
|
||||
realtime.maintenance = false
|
||||
const parseNoteIdFromSocketSpy = sinon.stub(realtime, 'parseNoteIdFromSocketAsync').callsFake(async (socket) => {
|
||||
return noteId
|
||||
})
|
||||
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
|
||||
|
||||
realtime.connection(mockSocket)
|
||||
|
||||
setTimeout(() => {
|
||||
assert.ok(parseNoteIdFromSocketSpy.calledOnce)
|
||||
assert(updateUserDataStub.calledOnce)
|
||||
done()
|
||||
}, 50)
|
||||
})
|
||||
|
||||
describe('flow', function () {
|
||||
it('should establish connection', function (done) {
|
||||
const noteId = 'note123'
|
||||
const mockSocket = makeMockSocket(null, {
|
||||
noteId: noteId
|
||||
})
|
||||
mockSocket.request.user.logged_in = true
|
||||
mockSocket.request.user.id = 'user1'
|
||||
mockSocket.noteId = noteId
|
||||
realtime.maintenance = false
|
||||
sinon.stub(realtime, 'parseNoteIdFromSocketAsync').callsFake(async (socket) => {
|
||||
return noteId
|
||||
})
|
||||
const updateHistoryStub = sinon.stub(realtime, 'updateHistory')
|
||||
const emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers')
|
||||
const emitRefreshStub = sinon.stub(realtime, 'emitRefresh')
|
||||
const failConnectionSpy = sinon.spy(realtime, 'failConnection')
|
||||
|
||||
const note = {
|
||||
id: noteId,
|
||||
authors: [
|
||||
{
|
||||
userId: 'user1',
|
||||
color: 'red',
|
||||
user: {
|
||||
id: 'user1',
|
||||
name: 'Alice'
|
||||
}
|
||||
},
|
||||
{
|
||||
userId: 'user2',
|
||||
color: 'blue',
|
||||
user: {
|
||||
id: 'user2',
|
||||
name: 'Bob'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
modelStub.Note.findOne.returns(Promise.resolve(note))
|
||||
modelStub.User.getProfile = sinon.stub().callsFake((user) => {
|
||||
return user
|
||||
})
|
||||
sinon.stub(realtime, 'checkViewPermission').returns(true)
|
||||
realtime.connection(mockSocket)
|
||||
setTimeout(() => {
|
||||
assert(modelStub.Note.findOne.calledOnce)
|
||||
assert.deepStrictEqual(modelStub.Note.findOne.lastCall.args[0].include, [
|
||||
{
|
||||
model: modelStub.User,
|
||||
as: 'owner'
|
||||
}, {
|
||||
model: modelStub.User,
|
||||
as: 'lastchangeuser'
|
||||
}, {
|
||||
model: modelStub.Author,
|
||||
as: 'authors',
|
||||
include: [{
|
||||
model: modelStub.User,
|
||||
as: 'user'
|
||||
}]
|
||||
}
|
||||
])
|
||||
assert(modelStub.Note.findOne.lastCall.args[0].where.id === noteId)
|
||||
assert(updateHistoryStub.calledOnce)
|
||||
assert(emitOnlineUsersStub.calledOnce)
|
||||
assert(emitRefreshStub.calledOnce)
|
||||
assert(failConnectionSpy.callCount === 0)
|
||||
assert(realtime.getNotePool()[noteId].id === noteId)
|
||||
assert(realtime.getNotePool()[noteId].socks.length === 1)
|
||||
assert.deepStrictEqual(realtime.getNotePool()[noteId].authors, {
|
||||
user1: {
|
||||
userid: 'user1', color: 'red', photo: undefined, name: 'Alice'
|
||||
},
|
||||
user2: {
|
||||
userid: 'user2', color: 'blue', photo: undefined, name: 'Bob'
|
||||
}
|
||||
})
|
||||
assert(Object.keys(realtime.getNotePool()[noteId].users).length === 1)
|
||||
done()
|
||||
}, 50)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,129 @@
|
|||
/* eslint-env node, mocha */
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const mock = require('mock-require')
|
||||
const sinon = require('sinon')
|
||||
const { removeModuleFromRequireCache, makeMockSocket, removeLibModuleCache } = require('./utils')
|
||||
|
||||
describe('realtime#update note is dirty timer', function () {
|
||||
let realtime
|
||||
let clock
|
||||
|
||||
beforeEach(() => {
|
||||
removeLibModuleCache()
|
||||
clock = sinon.useFakeTimers({
|
||||
toFake: ['setInterval']
|
||||
})
|
||||
mock('../../lib/logger', {
|
||||
error: () => {
|
||||
}
|
||||
})
|
||||
mock('../../lib/history', {})
|
||||
mock('../../lib/models', {
|
||||
Revision: {
|
||||
saveAllNotesRevision: () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
mock('../../lib/config', {})
|
||||
realtime = require('../../lib/realtime')
|
||||
|
||||
realtime.io = {
|
||||
to: sinon.stub().callsFake(function () {
|
||||
return {
|
||||
emit: sinon.fake()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
removeModuleFromRequireCache('../../lib/realtimeUpdateDirtyNoteJob')
|
||||
removeModuleFromRequireCache('../../lib/realtime')
|
||||
mock.stopAll()
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
it('should update note when note is dirty', (done) => {
|
||||
sinon.stub(realtime, 'updateNote').callsFake(function (note, callback) {
|
||||
callback(null, note)
|
||||
})
|
||||
|
||||
realtime.notes['note1'] = {
|
||||
server: {
|
||||
isDirty: false
|
||||
},
|
||||
socks: []
|
||||
}
|
||||
|
||||
const note2 = {
|
||||
server: {
|
||||
isDirty: true
|
||||
},
|
||||
socks: []
|
||||
}
|
||||
|
||||
realtime.notes['note2'] = note2
|
||||
|
||||
clock.tick(1000)
|
||||
setTimeout(() => {
|
||||
assert(note2.server.isDirty === false)
|
||||
done()
|
||||
}, 10)
|
||||
})
|
||||
|
||||
it('should not do anything when note missing', function (done) {
|
||||
sinon.stub(realtime, 'updateNote').callsFake(function (note, callback) {
|
||||
delete realtime.notes['note']
|
||||
callback(null, note)
|
||||
})
|
||||
|
||||
const note = {
|
||||
server: {
|
||||
isDirty: true
|
||||
},
|
||||
socks: [makeMockSocket()]
|
||||
}
|
||||
realtime.notes['note'] = note
|
||||
|
||||
clock.tick(1000)
|
||||
|
||||
setTimeout(() => {
|
||||
assert(note.server.isDirty === false)
|
||||
assert(note.socks[0].disconnect.called === false)
|
||||
done()
|
||||
}, 50)
|
||||
})
|
||||
|
||||
it('should disconnect all clients when update note error', function (done) {
|
||||
sinon.stub(realtime, 'updateNote').callsFake(function (note, callback) {
|
||||
callback(new Error('some error'), null)
|
||||
})
|
||||
|
||||
realtime.io = {
|
||||
to: sinon.stub().callsFake(function () {
|
||||
return {
|
||||
emit: sinon.fake()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const note = {
|
||||
server: {
|
||||
isDirty: true
|
||||
},
|
||||
socks: [makeMockSocket(), undefined, makeMockSocket()]
|
||||
}
|
||||
realtime.notes['note'] = note
|
||||
|
||||
clock.tick(1000)
|
||||
|
||||
setTimeout(() => {
|
||||
assert(note.server.isDirty === false)
|
||||
assert(note.socks[0].disconnect.called)
|
||||
assert(note.socks[2].disconnect.called)
|
||||
done()
|
||||
}, 50)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,94 @@
|
|||
/* eslint-env node, mocha */
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const mock = require('mock-require')
|
||||
const sinon = require('sinon')
|
||||
|
||||
const { makeMockSocket, removeModuleFromRequireCache } = require('./utils')
|
||||
|
||||
describe('realtime#disconnect', function () {
|
||||
const noteId = 'note1_id'
|
||||
let realtime
|
||||
let updateNoteStub
|
||||
let emitOnlineUsersStub
|
||||
let client
|
||||
|
||||
beforeEach(() => {
|
||||
mock('../../lib/logger', {
|
||||
error: () => {
|
||||
}
|
||||
})
|
||||
mock('../../lib/history', {})
|
||||
mock('../../lib/models', {
|
||||
Revision: {
|
||||
saveAllNotesRevision: () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
mock('../../lib/config', {})
|
||||
|
||||
realtime = require('../../lib/realtime')
|
||||
updateNoteStub = sinon.stub(realtime, 'updateNote').callsFake((note, callback) => {
|
||||
callback(null, note)
|
||||
})
|
||||
emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers')
|
||||
client = makeMockSocket()
|
||||
client.noteId = noteId
|
||||
|
||||
realtime.users[client.id] = {
|
||||
id: client.id,
|
||||
color: '#ff0000',
|
||||
cursor: null,
|
||||
login: false,
|
||||
userid: null,
|
||||
name: null,
|
||||
idle: false,
|
||||
type: null
|
||||
}
|
||||
|
||||
realtime.getNotePool()[noteId] = {
|
||||
id: noteId,
|
||||
server: {
|
||||
isDirty: true
|
||||
},
|
||||
users: {
|
||||
[client.id]: realtime.users[client.id]
|
||||
},
|
||||
socks: [client]
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
removeModuleFromRequireCache('../../lib/realtime')
|
||||
mock.stopAll()
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('should disconnect success', function (done) {
|
||||
realtime.queueForDisconnect(client)
|
||||
|
||||
setTimeout(() => {
|
||||
assert(typeof realtime.users[client.id] === 'undefined')
|
||||
assert(emitOnlineUsersStub.called)
|
||||
assert(updateNoteStub.called)
|
||||
assert(Object.keys(realtime.users).length === 0)
|
||||
assert(Object.keys(realtime.notes).length === 0)
|
||||
done()
|
||||
}, 5)
|
||||
})
|
||||
|
||||
it('should disconnect success when note is not dirty', function (done) {
|
||||
realtime.notes[noteId].server.isDirty = false
|
||||
realtime.queueForDisconnect(client)
|
||||
|
||||
setTimeout(() => {
|
||||
assert(typeof realtime.users[client.id] === 'undefined')
|
||||
assert(emitOnlineUsersStub.called)
|
||||
assert(updateNoteStub.called === false)
|
||||
assert(Object.keys(realtime.users).length === 0)
|
||||
assert(Object.keys(realtime.notes).length === 0)
|
||||
done()
|
||||
}, 5)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,91 @@
|
|||
/* eslint-env node, mocha */
|
||||
'use strict'
|
||||
|
||||
const mock = require('mock-require')
|
||||
const assert = require('assert')
|
||||
|
||||
const { makeMockSocket } = require('./utils')
|
||||
|
||||
describe('realtime#extractNoteIdFromSocket', function () {
|
||||
beforeEach(() => {
|
||||
mock('../../lib/logger', {})
|
||||
mock('../../lib/history', {})
|
||||
mock('../../lib/models', {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
delete require.cache[require.resolve('../../lib/realtime')]
|
||||
mock.stopAll()
|
||||
})
|
||||
|
||||
describe('urlPath not set', function () {
|
||||
beforeEach(() => {
|
||||
mock('../../lib/config', {})
|
||||
realtime = require('../../lib/realtime')
|
||||
})
|
||||
|
||||
let realtime
|
||||
|
||||
it('return false if socket or socket.handshake not exists', function () {
|
||||
let noteId = realtime.extractNoteIdFromSocket()
|
||||
assert.strictEqual(false, noteId)
|
||||
|
||||
noteId = realtime.extractNoteIdFromSocket({})
|
||||
assert.strictEqual(false, noteId)
|
||||
})
|
||||
|
||||
it('return false if query not set and referer not set', function () {
|
||||
const noteId = realtime.extractNoteIdFromSocket(makeMockSocket({
|
||||
otherHeader: 1
|
||||
}, {
|
||||
otherQuery: 1
|
||||
}))
|
||||
assert.strictEqual(false, noteId)
|
||||
})
|
||||
|
||||
it('return noteId from query', function () {
|
||||
// Arrange
|
||||
const incomingNoteId = 'myNoteId'
|
||||
const incomingSocket = makeMockSocket(undefined, { noteId: incomingNoteId })
|
||||
|
||||
// Act
|
||||
const noteId = realtime.extractNoteIdFromSocket(incomingSocket)
|
||||
// Assert
|
||||
assert.strictEqual(noteId, incomingNoteId)
|
||||
})
|
||||
|
||||
it('return noteId from old method (referer)', function () {
|
||||
// Arrange
|
||||
const incomingNoteId = 'myNoteId'
|
||||
const incomingSocket = makeMockSocket({
|
||||
referer: `https://localhost:3000/${incomingNoteId}`
|
||||
})
|
||||
|
||||
// Act
|
||||
const noteId = realtime.extractNoteIdFromSocket(incomingSocket)
|
||||
// Assert
|
||||
assert.strictEqual(noteId, incomingNoteId)
|
||||
})
|
||||
})
|
||||
|
||||
describe('urlPath is set', function () {
|
||||
let realtime
|
||||
it('return noteId from old method (referer) and urlPath set', function () {
|
||||
// Arrange
|
||||
const urlPath = 'hello'
|
||||
mock('../../lib/config', {
|
||||
urlPath: urlPath
|
||||
})
|
||||
realtime = require('../../lib/realtime')
|
||||
const incomingNoteId = 'myNoteId'
|
||||
const incomingSocket = makeMockSocket({
|
||||
referer: `https://localhost:3000/${urlPath}/${incomingNoteId}`
|
||||
})
|
||||
|
||||
// Act
|
||||
const noteId = realtime.extractNoteIdFromSocket(incomingSocket)
|
||||
// Assert
|
||||
assert.strictEqual(noteId, incomingNoteId)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,126 @@
|
|||
/* eslint-env node, mocha */
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const mock = require('mock-require')
|
||||
const sinon = require('sinon')
|
||||
|
||||
const { createFakeLogger } = require('../testDoubles/loggerFake')
|
||||
const realtimeJobStub = require('../testDoubles/realtimeJobStub')
|
||||
const { removeLibModuleCache, makeMockSocket } = require('./utils')
|
||||
|
||||
describe('realtime#ifMayEdit', function () {
|
||||
let modelsStub
|
||||
beforeEach(() => {
|
||||
removeLibModuleCache()
|
||||
mock('../../lib/config', {})
|
||||
mock('../../lib/logger', createFakeLogger())
|
||||
mock('../../lib/models', modelsStub)
|
||||
mock('../../lib/realtimeUpdateDirtyNoteJob', realtimeJobStub)
|
||||
mock('../../lib/realtimeCleanDanglingUserJob', realtimeJobStub)
|
||||
mock('../../lib/realtimeSaveRevisionJob', realtimeJobStub)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.stopAll()
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
const Role = {
|
||||
Guest: 'guest',
|
||||
LoggedIn: 'LoggedIn',
|
||||
Owner: 'Owner'
|
||||
}
|
||||
|
||||
const Permission = {
|
||||
Freely: 'freely',
|
||||
Editable: 'editable',
|
||||
Limited: 'limited',
|
||||
Locked: 'locked',
|
||||
Protected: 'protected',
|
||||
Private: 'private'
|
||||
}
|
||||
|
||||
const testcases = [
|
||||
{ role: Role.Guest, permission: Permission.Freely, canEdit: true },
|
||||
{ role: Role.LoggedIn, permission: Permission.Freely, canEdit: true },
|
||||
{ role: Role.Owner, permission: Permission.Freely, canEdit: true },
|
||||
{ role: Role.Guest, permission: Permission.Editable, canEdit: false },
|
||||
{ role: Role.LoggedIn, permission: Permission.Editable, canEdit: true },
|
||||
{ role: Role.Owner, permission: Permission.Editable, canEdit: true },
|
||||
{ role: Role.Guest, permission: Permission.Limited, canEdit: false },
|
||||
{ role: Role.LoggedIn, permission: Permission.Limited, canEdit: true },
|
||||
{ role: Role.Owner, permission: Permission.Limited, canEdit: true },
|
||||
{ role: Role.Guest, permission: Permission.Locked, canEdit: false },
|
||||
{ role: Role.LoggedIn, permission: Permission.Locked, canEdit: false },
|
||||
{ role: Role.Owner, permission: Permission.Locked, canEdit: true },
|
||||
{ role: Role.Guest, permission: Permission.Protected, canEdit: false },
|
||||
{ role: Role.LoggedIn, permission: Permission.Protected, canEdit: false },
|
||||
{ role: Role.Owner, permission: Permission.Protected, canEdit: true },
|
||||
{ role: Role.Guest, permission: Permission.Private, canEdit: false },
|
||||
{ role: Role.LoggedIn, permission: Permission.Private, canEdit: false },
|
||||
{ role: Role.Owner, permission: Permission.Private, canEdit: true }
|
||||
]
|
||||
|
||||
const noteOwnerId = 'owner'
|
||||
const loggedInUserId = 'user1'
|
||||
const noteId = 'noteId'
|
||||
|
||||
testcases.forEach((tc) => {
|
||||
it(`${tc.role} ${tc.canEdit ? 'can' : 'can\'t'} edit note with permission ${tc.permission}`, function () {
|
||||
const client = makeMockSocket()
|
||||
const note = {
|
||||
permission: tc.permission,
|
||||
owner: noteOwnerId
|
||||
}
|
||||
if (tc.role === Role.LoggedIn) {
|
||||
client.request.user.logged_in = true
|
||||
client.request.user.id = loggedInUserId
|
||||
} else if (tc.role === Role.Owner) {
|
||||
client.request.user.logged_in = true
|
||||
client.request.user.id = noteOwnerId
|
||||
}
|
||||
client.noteId = noteId
|
||||
const realtime = require('../../lib/realtime')
|
||||
realtime.getNotePool()[noteId] = note
|
||||
const callback = sinon.stub()
|
||||
realtime.ifMayEdit(client, callback)
|
||||
assert(callback.calledOnce)
|
||||
assert(callback.lastCall.args[0] === tc.canEdit)
|
||||
})
|
||||
})
|
||||
|
||||
it('should set lsatchangeuser to null if guest edit operation', function () {
|
||||
const note = {
|
||||
permission: Permission.Freely
|
||||
}
|
||||
const client = makeMockSocket()
|
||||
client.noteId = noteId
|
||||
const callback = sinon.stub()
|
||||
client.origin = 'operation'
|
||||
const realtime = require('../../lib/realtime')
|
||||
realtime.getNotePool()[noteId] = note
|
||||
realtime.ifMayEdit(client, callback)
|
||||
assert(callback.calledOnce)
|
||||
assert(callback.lastCall.args[0])
|
||||
assert(note.lastchangeuser === null)
|
||||
})
|
||||
|
||||
it('should set lastchangeuser to logged_in user id if user edit', function () {
|
||||
const note = {
|
||||
permission: Permission.Freely
|
||||
}
|
||||
const client = makeMockSocket()
|
||||
client.noteId = noteId
|
||||
client.request.user.logged_in = true
|
||||
client.request.user.id = loggedInUserId
|
||||
const callback = sinon.stub()
|
||||
client.origin = 'operation'
|
||||
const realtime = require('../../lib/realtime')
|
||||
realtime.getNotePool()[noteId] = note
|
||||
realtime.ifMayEdit(client, callback)
|
||||
assert(callback.calledOnce)
|
||||
assert(callback.lastCall.args[0])
|
||||
assert(note.lastchangeuser === loggedInUserId)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,115 @@
|
|||
/* eslint-env node, mocha */
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const mock = require('mock-require')
|
||||
|
||||
const { makeMockSocket, removeModuleFromRequireCache } = require('./utils')
|
||||
|
||||
describe('realtime#parseNoteIdFromSocketAsync', function () {
|
||||
let realtime
|
||||
|
||||
beforeEach(() => {
|
||||
mock('../../lib/logger', {})
|
||||
mock('../../lib/history', {})
|
||||
mock('../../lib/models', {
|
||||
Note: {
|
||||
parseNoteId: function (noteId, callback) {
|
||||
callback(null, noteId)
|
||||
}
|
||||
}
|
||||
})
|
||||
mock('../../lib/config', {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
removeModuleFromRequireCache('../../lib/realtime')
|
||||
mock.stopAll()
|
||||
})
|
||||
|
||||
it('should return null when socket not send noteId', async function () {
|
||||
realtime = require('../../lib/realtime')
|
||||
const mockSocket = makeMockSocket()
|
||||
try {
|
||||
const notes = await realtime.parseNoteIdFromSocketAsync(mockSocket)
|
||||
assert(notes === null)
|
||||
} catch (err) {
|
||||
assert.fail('should not occur any error')
|
||||
}
|
||||
})
|
||||
|
||||
describe('noteId exists', function () {
|
||||
beforeEach(() => {
|
||||
mock('../../lib/models', {
|
||||
Note: {
|
||||
parseNoteId: function (noteId, callback) {
|
||||
callback(null, noteId)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
it('should return noteId when noteId exists', async function () {
|
||||
realtime = require('../../lib/realtime')
|
||||
const noteId = '123456'
|
||||
const mockSocket = makeMockSocket(undefined, {
|
||||
noteId: noteId
|
||||
})
|
||||
realtime = require('../../lib/realtime')
|
||||
let parsedNoteId
|
||||
try {
|
||||
parsedNoteId = await realtime.parseNoteIdFromSocketAsync(mockSocket)
|
||||
} catch (err) {
|
||||
assert.fail(`should not occur any error ${err} `)
|
||||
}
|
||||
assert(parsedNoteId === noteId)
|
||||
})
|
||||
})
|
||||
|
||||
describe('noteId not exists', function () {
|
||||
beforeEach(() => {
|
||||
mock('../../lib/models', {
|
||||
Note: {
|
||||
parseNoteId: function (noteId, callback) {
|
||||
callback(null, null)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
it('should return null when noteId not exists', async function () {
|
||||
realtime = require('../../lib/realtime')
|
||||
const noteId = '123456'
|
||||
const mockSocket = makeMockSocket(undefined, {
|
||||
noteId: noteId
|
||||
})
|
||||
realtime = require('../../lib/realtime')
|
||||
const parsedNoteId = await realtime.parseNoteIdFromSocketAsync(mockSocket)
|
||||
assert(parsedNoteId === null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parse note error', function () {
|
||||
beforeEach(() => {
|
||||
mock('../../lib/models', {
|
||||
Note: {
|
||||
parseNoteId: function (noteId, callback) {
|
||||
/* eslint-disable-next-line */
|
||||
callback('error', null)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
it('should return error when noteId parse error', async function () {
|
||||
realtime = require('../../lib/realtime')
|
||||
const noteId = '123456'
|
||||
const mockSocket = makeMockSocket(undefined, {
|
||||
noteId: noteId
|
||||
})
|
||||
realtime = require('../../lib/realtime')
|
||||
try {
|
||||
await realtime.parseNoteIdFromSocketAsync(mockSocket)
|
||||
} catch (err) {
|
||||
assert(err === 'error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,73 @@
|
|||
'use strict'
|
||||
|
||||
/* eslint-env node, mocha */
|
||||
|
||||
const mock = require('mock-require')
|
||||
const assert = require('assert')
|
||||
|
||||
describe('realtime', function () {
|
||||
describe('checkViewPermission', function () {
|
||||
// role -> guest, loggedInUser, loggedInOwner
|
||||
const viewPermission = {
|
||||
freely: [true, true, true],
|
||||
editable: [true, true, true],
|
||||
limited: [false, true, true],
|
||||
locked: [true, true, true],
|
||||
protected: [false, true, true],
|
||||
private: [false, false, true]
|
||||
}
|
||||
const loggedInUserId = 'user1_id'
|
||||
const ownerUserId = 'user2_id'
|
||||
const guestReq = {}
|
||||
const loggedInUserReq = {
|
||||
user: {
|
||||
id: loggedInUserId,
|
||||
logged_in: true
|
||||
}
|
||||
}
|
||||
const loggedInOwnerReq = {
|
||||
user: {
|
||||
id: ownerUserId,
|
||||
logged_in: true
|
||||
}
|
||||
}
|
||||
|
||||
const note = {
|
||||
owner: ownerUserId
|
||||
}
|
||||
|
||||
let realtime
|
||||
|
||||
beforeEach(() => {
|
||||
mock('../../lib/logger', {
|
||||
error: () => {
|
||||
}
|
||||
})
|
||||
mock('../../lib/history', {})
|
||||
mock('../../lib/models', {
|
||||
Note: {
|
||||
parseNoteTitle: (data) => (data)
|
||||
}
|
||||
})
|
||||
mock('../../lib/config', {})
|
||||
realtime = require('../../lib/realtime')
|
||||
})
|
||||
|
||||
Object.keys(viewPermission).forEach(function (permission) {
|
||||
describe(permission, function () {
|
||||
beforeEach(() => {
|
||||
note.permission = permission
|
||||
})
|
||||
it('guest permission test', function () {
|
||||
assert(realtime.checkViewPermission(guestReq, note) === viewPermission[permission][0])
|
||||
})
|
||||
it('loggedIn User permission test', function () {
|
||||
assert(realtime.checkViewPermission(loggedInUserReq, note) === viewPermission[permission][1])
|
||||
})
|
||||
it('loggedIn Owner permission test', function () {
|
||||
assert(realtime.checkViewPermission(loggedInOwnerReq, note) === viewPermission[permission][2])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,70 @@
|
|||
/* eslint-env node, mocha */
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const mock = require('mock-require')
|
||||
const sinon = require('sinon')
|
||||
const { removeModuleFromRequireCache, removeLibModuleCache } = require('./utils')
|
||||
|
||||
describe('save revision job', function () {
|
||||
let clock
|
||||
let mockModels
|
||||
let realtime
|
||||
beforeEach(() => {
|
||||
removeLibModuleCache()
|
||||
mockModels = {
|
||||
Revision: {
|
||||
saveAllNotesRevision: sinon.stub()
|
||||
}
|
||||
}
|
||||
clock = sinon.useFakeTimers()
|
||||
mock('../../lib/processQueue', require('../testDoubles/ProcessQueueFake'))
|
||||
mock('../../lib/logger', {
|
||||
error: () => {},
|
||||
info: () => {}
|
||||
})
|
||||
mock('../../lib/history', {})
|
||||
mock('../../lib/models', mockModels)
|
||||
mock('../../lib/config', {
|
||||
debug: true
|
||||
})
|
||||
mock('../../lib/realtimeUpdateDirtyNoteJob', require('../testDoubles/realtimeJobStub'))
|
||||
mock('../../lib/realtimeCleanDanglingUserJob', require('../testDoubles/realtimeJobStub'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore()
|
||||
removeModuleFromRequireCache('../../lib/realtime')
|
||||
removeModuleFromRequireCache('../../lib/realtimeSaveRevisionJob')
|
||||
mock.stopAll()
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('should execute save revision job every 5 min', (done) => {
|
||||
mockModels.Revision.saveAllNotesRevision.callsFake((callback) => {
|
||||
callback(null, [])
|
||||
})
|
||||
realtime = require('../../lib/realtime')
|
||||
clock.tick(5 * 60 * 1000)
|
||||
clock.restore()
|
||||
setTimeout(() => {
|
||||
assert(mockModels.Revision.saveAllNotesRevision.called)
|
||||
assert(realtime.saveRevisionJob.getSaverSleep() === true)
|
||||
done()
|
||||
}, 50)
|
||||
})
|
||||
|
||||
it('should not set saverSleep when more than 1 note save revision', (done) => {
|
||||
mockModels.Revision.saveAllNotesRevision.callsFake((callback) => {
|
||||
callback(null, [1])
|
||||
})
|
||||
realtime = require('../../lib/realtime')
|
||||
clock.tick(5 * 60 * 1000)
|
||||
clock.restore()
|
||||
setTimeout(() => {
|
||||
assert(mockModels.Revision.saveAllNotesRevision.called)
|
||||
assert(realtime.saveRevisionJob.getSaverSleep() === false)
|
||||
done()
|
||||
}, 50)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,668 @@
|
|||
/* eslint-env node, mocha */
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const mock = require('mock-require')
|
||||
const sinon = require('sinon')
|
||||
|
||||
const { makeMockSocket, removeModuleFromRequireCache } = require('./utils')
|
||||
|
||||
describe('realtime#socket event', function () {
|
||||
const noteId = 'note123'
|
||||
const note = {
|
||||
id: noteId,
|
||||
authors: [
|
||||
{
|
||||
userId: 'user1',
|
||||
color: 'red',
|
||||
user: {
|
||||
id: 'user1',
|
||||
name: 'Alice'
|
||||
}
|
||||
},
|
||||
{
|
||||
userId: 'user2',
|
||||
color: 'blue',
|
||||
user: {
|
||||
id: 'user2',
|
||||
name: 'Bob'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
let realtime
|
||||
let clientSocket
|
||||
let modelsMock
|
||||
let eventFuncMap
|
||||
let configMock
|
||||
let clock
|
||||
|
||||
beforeEach(function (done) {
|
||||
clock = sinon.useFakeTimers({
|
||||
toFake: ['setInterval']
|
||||
})
|
||||
eventFuncMap = new Map()
|
||||
modelsMock = {
|
||||
Note: {
|
||||
parseNoteTitle: (data) => (data),
|
||||
destroy: sinon.stub().returns(Promise.resolve(1)),
|
||||
update: sinon.stub().returns(Promise.resolve([1])),
|
||||
findOne: sinon.stub().returns(Promise.resolve(note))
|
||||
},
|
||||
User: {}
|
||||
}
|
||||
modelsMock.User.getProfile = sinon.stub().callsFake((user) => {
|
||||
return user
|
||||
})
|
||||
configMock = {
|
||||
fullversion: '1.5.0',
|
||||
minimumCompatibleVersion: '1.0.0',
|
||||
permission: {
|
||||
freely: 'freely',
|
||||
editable: 'editable',
|
||||
limited: 'limited',
|
||||
locked: 'locked',
|
||||
protected: 'protected',
|
||||
private: 'private'
|
||||
}
|
||||
}
|
||||
mock('../../lib/logger', {
|
||||
error: () => {
|
||||
},
|
||||
info: () => {
|
||||
}
|
||||
})
|
||||
mock('../../lib/history', {})
|
||||
mock('../../lib/models', modelsMock)
|
||||
mock('../../lib/config', configMock)
|
||||
mock('../../lib/ot', require('../testDoubles/otFake'))
|
||||
realtime = require('../../lib/realtime')
|
||||
|
||||
// get all socket event handler
|
||||
clientSocket = makeMockSocket(null, {
|
||||
noteId: noteId
|
||||
})
|
||||
clientSocket.request.user.logged_in = true
|
||||
clientSocket.request.user.id = 'user1'
|
||||
// clientSocket.noteId = noteId
|
||||
clientSocket.on = function (event, func) {
|
||||
eventFuncMap.set(event, func)
|
||||
}
|
||||
realtime.maintenance = false
|
||||
|
||||
realtime.io = (function () {
|
||||
const roomMap = new Map()
|
||||
return {
|
||||
to: function (roomId) {
|
||||
if (!roomMap.has(roomId)) {
|
||||
roomMap.set(roomId, {
|
||||
emit: sinon.stub()
|
||||
})
|
||||
}
|
||||
return roomMap.get(roomId)
|
||||
}
|
||||
}
|
||||
}())
|
||||
|
||||
const wrappedFuncs = []
|
||||
wrappedFuncs.push(sinon.stub(realtime, 'updateUserData'))
|
||||
wrappedFuncs.push(sinon.stub(realtime, 'emitOnlineUsers'))
|
||||
wrappedFuncs.push(sinon.stub(realtime, 'parseNoteIdFromSocketAsync').returns(Promise.resolve(noteId)))
|
||||
wrappedFuncs.push(sinon.stub(realtime, 'updateHistory'))
|
||||
wrappedFuncs.push(sinon.stub(realtime, 'emitRefresh'))
|
||||
|
||||
realtime.connection(clientSocket)
|
||||
|
||||
setTimeout(() => {
|
||||
wrappedFuncs.forEach((wrappedFunc) => {
|
||||
wrappedFunc.restore()
|
||||
})
|
||||
done()
|
||||
}, 50)
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
removeModuleFromRequireCache('../../lib/realtime')
|
||||
removeModuleFromRequireCache('../../lib/realtimeClientConnection')
|
||||
mock.stopAll()
|
||||
sinon.restore()
|
||||
clock.restore()
|
||||
clientSocket = null
|
||||
})
|
||||
|
||||
describe('refresh', function () {
|
||||
it('should call refresh', () => {
|
||||
const refreshFunc = eventFuncMap.get('refresh')
|
||||
const emitRefreshStub = sinon.stub(realtime, 'emitRefresh')
|
||||
refreshFunc()
|
||||
assert(emitRefreshStub.calledOnce)
|
||||
assert.deepStrictEqual(emitRefreshStub.lastCall.args[0], clientSocket)
|
||||
})
|
||||
})
|
||||
|
||||
describe('user status', function () {
|
||||
it('should call emitUserStatus and update user data', () => {
|
||||
const userStatusFunc = eventFuncMap.get('user status')
|
||||
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
|
||||
realtime.notes[noteId] = {}
|
||||
|
||||
const userData = {
|
||||
idle: true,
|
||||
type: 'xs'
|
||||
}
|
||||
userStatusFunc(userData)
|
||||
assert(emitUserStatusStub.calledOnce)
|
||||
assert.deepStrictEqual(emitUserStatusStub.lastCall.args[0], clientSocket)
|
||||
assert(realtime.users[clientSocket.id].idle === true)
|
||||
assert(realtime.users[clientSocket.id].type === 'xs')
|
||||
})
|
||||
|
||||
it('should call emitUserStatus without userdata', () => {
|
||||
const userStatusFunc = eventFuncMap.get('user status')
|
||||
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
|
||||
realtime.notes[noteId] = {}
|
||||
userStatusFunc()
|
||||
assert(emitUserStatusStub.calledOnce)
|
||||
assert.deepStrictEqual(emitUserStatusStub.lastCall.args[0], clientSocket)
|
||||
assert(realtime.users[clientSocket.id].idle === false)
|
||||
assert(realtime.users[clientSocket.id].type === null)
|
||||
})
|
||||
|
||||
it('should not call emitUserStatus when user not exists', () => {
|
||||
const userStatusFunc = eventFuncMap.get('user status')
|
||||
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
|
||||
realtime.notes[noteId] = {}
|
||||
delete realtime.users[clientSocket.id]
|
||||
const userData = {
|
||||
idle: true,
|
||||
type: 'xs'
|
||||
}
|
||||
userStatusFunc(userData)
|
||||
assert(emitUserStatusStub.called === false)
|
||||
})
|
||||
|
||||
it('should not call emitUserStatus when note not exists', () => {
|
||||
const userStatusFunc = eventFuncMap.get('user status')
|
||||
const emitUserStatusStub = sinon.stub(realtime, 'emitUserStatus')
|
||||
realtime.deleteAllNoteFromPool()
|
||||
realtime.users[clientSocket.id] = {}
|
||||
const userData = {
|
||||
idle: true,
|
||||
type: 'xs'
|
||||
}
|
||||
userStatusFunc(userData)
|
||||
assert(emitUserStatusStub.called === false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('disconnect', function () {
|
||||
it('should push socket to disconnect queue and call disconnect function', () => {
|
||||
const disconnectFunc = eventFuncMap.get('disconnect')
|
||||
const queueForDisconnectStub = sinon.stub(realtime, 'queueForDisconnect')
|
||||
disconnectFunc()
|
||||
assert(queueForDisconnectStub.calledOnce)
|
||||
})
|
||||
|
||||
it('should quick return when socket is in disconnect queue', () => {
|
||||
const disconnectFunc = eventFuncMap.get('disconnect')
|
||||
const queueForDisconnectStub = sinon.stub(realtime, 'queueForDisconnect')
|
||||
realtime.disconnectProcessQueue.push(clientSocket.id, async () => {})
|
||||
disconnectFunc()
|
||||
assert(queueForDisconnectStub.called === false)
|
||||
})
|
||||
})
|
||||
|
||||
;['cursor focus', 'cursor activity', 'cursor blur'].forEach((event) => {
|
||||
describe(event, function () {
|
||||
let cursorFocusFunc
|
||||
|
||||
const cursorData = {
|
||||
cursor: 10
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cursorFocusFunc = eventFuncMap.get(event)
|
||||
realtime.notes[noteId] = {}
|
||||
})
|
||||
|
||||
it('should broadcast to all client', () => {
|
||||
cursorFocusFunc(cursorData)
|
||||
const broadChannelEmitFake = clientSocket.broadcast.to(noteId).emit
|
||||
assert(broadChannelEmitFake.calledOnce)
|
||||
assert(broadChannelEmitFake.lastCall.args[0] === event)
|
||||
if (event === 'cursor blur') {
|
||||
assert(broadChannelEmitFake.lastCall.args[1].id === clientSocket.id)
|
||||
} else {
|
||||
assert.deepStrictEqual(broadChannelEmitFake.lastCall.args[1].cursor, cursorData)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not broadcast when note not exists', () => {
|
||||
delete realtime.notes[noteId]
|
||||
cursorFocusFunc(cursorData)
|
||||
const broadChannelEmitFake = clientSocket.broadcast.to(noteId).emit
|
||||
assert(broadChannelEmitFake.called === false)
|
||||
})
|
||||
|
||||
it('should not broadcast when user not exists', () => {
|
||||
delete realtime.users[clientSocket.id]
|
||||
cursorFocusFunc(cursorData)
|
||||
const broadChannelEmitFake = clientSocket.broadcast.to(noteId).emit
|
||||
assert(broadChannelEmitFake.called === false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('version', function () {
|
||||
it('should emit server version ', () => {
|
||||
const versionFunc = eventFuncMap.get('version')
|
||||
versionFunc()
|
||||
assert(clientSocket.emit.called)
|
||||
assert(clientSocket.emit.lastCall.args[0], 'version')
|
||||
assert.deepStrictEqual(clientSocket.emit.lastCall.args[1], {
|
||||
version: '1.5.0',
|
||||
minimumCompatibleVersion: '1.0.0'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('online users', function () {
|
||||
it('should return online user list', function () {
|
||||
const onlineUsersFunc = eventFuncMap.get('online users')
|
||||
realtime.notes[noteId] = {
|
||||
users: {
|
||||
10: {
|
||||
id: 10
|
||||
},
|
||||
20: {
|
||||
id: 20
|
||||
}
|
||||
}
|
||||
}
|
||||
onlineUsersFunc()
|
||||
assert(clientSocket.emit.called)
|
||||
assert(clientSocket.emit.lastCall.args[0] === 'online users')
|
||||
const returnUserList = clientSocket.emit.lastCall.args[1].users
|
||||
assert(returnUserList.length === 2)
|
||||
assert(returnUserList[0].id === 10)
|
||||
assert(returnUserList[1].id === 20)
|
||||
})
|
||||
|
||||
it('should not return user list when note not exists', function () {
|
||||
const onlineUsersFunc = eventFuncMap.get('online users')
|
||||
realtime.deleteAllNoteFromPool()
|
||||
onlineUsersFunc()
|
||||
assert(clientSocket.emit.called === false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('user changed', function () {
|
||||
it('should call updateUserData', () => {
|
||||
const userChangedFunc = eventFuncMap.get('user changed')
|
||||
realtime.notes[noteId] = {
|
||||
users: {
|
||||
[clientSocket.id]: {}
|
||||
}
|
||||
}
|
||||
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
|
||||
const emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers')
|
||||
userChangedFunc()
|
||||
assert(updateUserDataStub.calledOnce)
|
||||
assert(emitOnlineUsersStub.calledOnce)
|
||||
})
|
||||
|
||||
it('should direct return when note not exists', function () {
|
||||
const userChangedFunc = eventFuncMap.get('user changed')
|
||||
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
|
||||
const emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers')
|
||||
realtime.deleteAllNoteFromPool()
|
||||
userChangedFunc()
|
||||
assert(updateUserDataStub.called === false)
|
||||
assert(emitOnlineUsersStub.called === false)
|
||||
})
|
||||
|
||||
it('should direct return when note\'s users not exists', function () {
|
||||
const userChangedFunc = eventFuncMap.get('user changed')
|
||||
realtime.notes[noteId] = {
|
||||
users: {}
|
||||
}
|
||||
delete realtime.users[clientSocket.id]
|
||||
const updateUserDataStub = sinon.stub(realtime, 'updateUserData')
|
||||
const emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers')
|
||||
userChangedFunc()
|
||||
assert(updateUserDataStub.called === false)
|
||||
assert(emitOnlineUsersStub.called === false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete', function () {
|
||||
it('should delete note when owner request', function (done) {
|
||||
const currentUserId = 'user1_id'
|
||||
const noteOwnerId = 'user1_id'
|
||||
const otherClient = makeMockSocket()
|
||||
clientSocket.request = {
|
||||
user: {
|
||||
logged_in: true,
|
||||
id: currentUserId
|
||||
}
|
||||
}
|
||||
realtime.notes[noteId] = {
|
||||
owner: noteOwnerId,
|
||||
socks: [clientSocket, undefined, otherClient]
|
||||
}
|
||||
const deleteFunc = eventFuncMap.get('delete')
|
||||
deleteFunc()
|
||||
setTimeout(() => {
|
||||
assert(otherClient.disconnect.calledOnce)
|
||||
assert(otherClient.emit.calledOnce)
|
||||
assert(otherClient.emit.lastCall.args[0] === 'delete')
|
||||
assert(clientSocket.disconnect.calledOnce)
|
||||
assert(clientSocket.emit.calledOnce)
|
||||
assert(clientSocket.emit.lastCall.args[0] === 'delete')
|
||||
assert(modelsMock.Note.destroy.calledOnce)
|
||||
done()
|
||||
}, 10)
|
||||
})
|
||||
|
||||
it('should not do anything when user not login', function (done) {
|
||||
const noteOwnerId = 'user1_id'
|
||||
clientSocket.request = {}
|
||||
realtime.notes[noteId] = {
|
||||
owner: noteOwnerId,
|
||||
socks: [clientSocket]
|
||||
}
|
||||
const deleteFunc = eventFuncMap.get('delete')
|
||||
deleteFunc()
|
||||
setTimeout(() => {
|
||||
assert(modelsMock.Note.destroy.called === false)
|
||||
assert(clientSocket.disconnect.called === false)
|
||||
done()
|
||||
}, 10)
|
||||
})
|
||||
|
||||
it('should not do anything when note not exists', function (done) {
|
||||
const currentUserId = 'user1_id'
|
||||
clientSocket.request = {
|
||||
user: {
|
||||
logged_in: true,
|
||||
id: currentUserId
|
||||
}
|
||||
}
|
||||
const deleteFunc = eventFuncMap.get('delete')
|
||||
deleteFunc()
|
||||
setTimeout(() => {
|
||||
assert(modelsMock.Note.destroy.called === false)
|
||||
assert(clientSocket.disconnect.called === false)
|
||||
done()
|
||||
}, 10)
|
||||
})
|
||||
|
||||
it('should not do anything when note owner is not me', function (done) {
|
||||
const currentUserId = 'user1_id'
|
||||
const noteOwnerId = 'user2_id'
|
||||
const otherClient = makeMockSocket()
|
||||
clientSocket.request = {
|
||||
user: {
|
||||
logged_in: true,
|
||||
id: currentUserId
|
||||
}
|
||||
}
|
||||
realtime.notes[noteId] = {
|
||||
owner: noteOwnerId,
|
||||
socks: [clientSocket, otherClient]
|
||||
}
|
||||
const deleteFunc = eventFuncMap.get('delete')
|
||||
deleteFunc()
|
||||
setTimeout(() => {
|
||||
assert(clientSocket.disconnect.called === false)
|
||||
assert(modelsMock.Note.destroy.called === false)
|
||||
done()
|
||||
}, 10)
|
||||
})
|
||||
|
||||
it('should not do anything when note destroy fail', function (done) {
|
||||
const currentUserId = 'user1_id'
|
||||
const noteOwnerId = 'user1_id'
|
||||
modelsMock.Note.destroy.withArgs({
|
||||
where: {
|
||||
id: noteId
|
||||
}
|
||||
}).returns(Promise.resolve(0))
|
||||
|
||||
const otherClient = makeMockSocket()
|
||||
clientSocket.request = {
|
||||
user: {
|
||||
logged_in: true,
|
||||
id: currentUserId
|
||||
}
|
||||
}
|
||||
realtime.notes[noteId] = {
|
||||
id: noteId,
|
||||
owner: noteOwnerId,
|
||||
socks: [clientSocket, otherClient]
|
||||
}
|
||||
const deleteFunc = eventFuncMap.get('delete')
|
||||
deleteFunc()
|
||||
setTimeout(() => {
|
||||
assert(modelsMock.Note.destroy.calledOnce)
|
||||
assert(clientSocket.disconnect.called === false)
|
||||
done()
|
||||
}, 10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('permission', function () {
|
||||
const ownerId = 'user1_id'
|
||||
const otherSignInUserId = 'user2_id'
|
||||
let otherClient
|
||||
let checkViewPermissionSpy
|
||||
let permissionFunc
|
||||
|
||||
beforeEach(function () {
|
||||
otherClient = makeMockSocket()
|
||||
clientSocket.request = {
|
||||
user: {
|
||||
id: ownerId,
|
||||
logged_in: true
|
||||
}
|
||||
}
|
||||
|
||||
otherClient.request = {
|
||||
user: {
|
||||
id: otherSignInUserId,
|
||||
logged_in: true
|
||||
}
|
||||
}
|
||||
|
||||
realtime.deleteAllNoteFromPool()
|
||||
realtime.addNote({
|
||||
id: noteId,
|
||||
owner: ownerId
|
||||
})
|
||||
|
||||
checkViewPermissionSpy = sinon.spy(realtime, 'checkViewPermission')
|
||||
permissionFunc = eventFuncMap.get('permission')
|
||||
})
|
||||
|
||||
it('should disconnect when lose view permission', function (done) {
|
||||
realtime.getNoteFromNotePool(noteId).permission = 'editable'
|
||||
realtime.getNoteFromNotePool(noteId).socks = [clientSocket, undefined, otherClient]
|
||||
|
||||
permissionFunc('private')
|
||||
|
||||
setTimeout(() => {
|
||||
// should change note permission to private
|
||||
assert(modelsMock.Note.update.calledOnce)
|
||||
assert(modelsMock.Note.update.lastCall.args[0].permission === 'private')
|
||||
assert(modelsMock.Note.update.lastCall.args[1].where.id === noteId)
|
||||
// should check all connected client
|
||||
assert(checkViewPermissionSpy.callCount === 2)
|
||||
assert(otherClient.emit.calledOnce)
|
||||
assert(otherClient.disconnect.calledOnce)
|
||||
done()
|
||||
}, 5)
|
||||
})
|
||||
|
||||
it('should not do anything when user not logged in', function (done) {
|
||||
clientSocket.request = {}
|
||||
permissionFunc('private')
|
||||
setTimeout(() => {
|
||||
assert(modelsMock.Note.update.called === false)
|
||||
done()
|
||||
}, 5)
|
||||
})
|
||||
|
||||
it('should not do anything when note not exists', function (done) {
|
||||
delete realtime.notes[noteId]
|
||||
permissionFunc('private')
|
||||
setTimeout(() => {
|
||||
assert(modelsMock.Note.update.called === false)
|
||||
done()
|
||||
}, 5)
|
||||
})
|
||||
|
||||
it('should not do anything when not note owner', function (done) {
|
||||
clientSocket.request.user.id = 'other_user_id'
|
||||
permissionFunc('private')
|
||||
setTimeout(() => {
|
||||
assert(modelsMock.Note.update.called === false)
|
||||
done()
|
||||
}, 5)
|
||||
})
|
||||
|
||||
it('should change permission to freely when config allowAnonymous, allowAnonymousEdits and allowAnonymousViews are true', function (done) {
|
||||
configMock.allowAnonymous = true
|
||||
configMock.allowAnonymousEdits = true
|
||||
configMock.allowAnonymousViews = true
|
||||
realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
|
||||
|
||||
permissionFunc('freely')
|
||||
|
||||
setTimeout(() => {
|
||||
assert(checkViewPermissionSpy.callCount === 2)
|
||||
assert(otherClient.emit.called === false)
|
||||
assert(otherClient.disconnect.called === false)
|
||||
assert(clientSocket.emit.called === false)
|
||||
assert(clientSocket.disconnect.called === false)
|
||||
done()
|
||||
}, 5)
|
||||
})
|
||||
|
||||
it('should not change permission to freely when config allowAnonymous, allowAnonymousEdits and allowAnonymousViews are false', function (done) {
|
||||
configMock.allowAnonymous = false
|
||||
configMock.allowAnonymousEdits = false
|
||||
configMock.allowAnonymousViews = false
|
||||
realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
|
||||
|
||||
permissionFunc('freely')
|
||||
|
||||
setTimeout(() => {
|
||||
assert(modelsMock.Note.update.called === false)
|
||||
assert(checkViewPermissionSpy.called === false)
|
||||
done()
|
||||
}, 5)
|
||||
})
|
||||
|
||||
it('should change permission to freely when config allowAnonymous is true', function (done) {
|
||||
configMock.allowAnonymous = true
|
||||
configMock.allowAnonymousEdits = false
|
||||
configMock.allowAnonymousViews = false
|
||||
realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
|
||||
|
||||
permissionFunc('freely')
|
||||
|
||||
setTimeout(() => {
|
||||
assert(checkViewPermissionSpy.callCount === 2)
|
||||
assert(otherClient.emit.called === false)
|
||||
assert(otherClient.disconnect.called === false)
|
||||
assert(clientSocket.emit.called === false)
|
||||
assert(clientSocket.disconnect.called === false)
|
||||
done()
|
||||
}, 5)
|
||||
})
|
||||
|
||||
it('should not change permission to freely when config allowAnonymousEdits is true', function (done) {
|
||||
configMock.allowAnonymous = false
|
||||
configMock.allowAnonymousEdits = true
|
||||
configMock.allowAnonymousViews = false
|
||||
realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
|
||||
|
||||
permissionFunc('freely')
|
||||
|
||||
setTimeout(() => {
|
||||
assert(modelsMock.Note.update.called === false)
|
||||
assert(checkViewPermissionSpy.called === false)
|
||||
done()
|
||||
}, 5)
|
||||
})
|
||||
|
||||
it('should not change permission to freely when config allowAnonymousViews is true', function (done) {
|
||||
configMock.allowAnonymous = false
|
||||
configMock.allowAnonymousEdits = false
|
||||
configMock.allowAnonymousViews = true
|
||||
realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
|
||||
|
||||
permissionFunc('freely')
|
||||
|
||||
setTimeout(() => {
|
||||
assert(modelsMock.Note.update.called === false)
|
||||
assert(checkViewPermissionSpy.called === false)
|
||||
done()
|
||||
}, 5)
|
||||
})
|
||||
|
||||
it('should change permission to editable when config allowAnonymousViews is true', function (done) {
|
||||
configMock.allowAnonymous = false
|
||||
configMock.allowAnonymousEdits = false
|
||||
configMock.allowAnonymousViews = true
|
||||
realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
|
||||
|
||||
permissionFunc('editable')
|
||||
|
||||
setTimeout(() => {
|
||||
assert(checkViewPermissionSpy.callCount === 2)
|
||||
assert(otherClient.emit.called === false)
|
||||
assert(otherClient.disconnect.called === false)
|
||||
assert(clientSocket.emit.called === false)
|
||||
assert(clientSocket.disconnect.called === false)
|
||||
done()
|
||||
}, 5)
|
||||
})
|
||||
|
||||
it('should change permission to freely when config allowAnonymousEdits and allowAnonymousViews are false true', function (done) {
|
||||
configMock.allowAnonymous = false
|
||||
configMock.allowAnonymousEdits = true
|
||||
configMock.allowAnonymousViews = true
|
||||
realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
|
||||
|
||||
permissionFunc('freely')
|
||||
|
||||
setTimeout(() => {
|
||||
assert(checkViewPermissionSpy.callCount === 2)
|
||||
assert(otherClient.emit.called === false)
|
||||
assert(otherClient.disconnect.called === false)
|
||||
assert(clientSocket.emit.called === false)
|
||||
assert(clientSocket.disconnect.called === false)
|
||||
done()
|
||||
}, 5)
|
||||
})
|
||||
|
||||
it('should change permission to editable when config allowAnonymousEdits and allowAnonymousViews are false true', function (done) {
|
||||
configMock.allowAnonymous = false
|
||||
configMock.allowAnonymousEdits = true
|
||||
configMock.allowAnonymousViews = true
|
||||
realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
|
||||
|
||||
permissionFunc('editable')
|
||||
|
||||
setTimeout(() => {
|
||||
assert(checkViewPermissionSpy.callCount === 2)
|
||||
assert(otherClient.emit.called === false)
|
||||
assert(otherClient.disconnect.called === false)
|
||||
assert(clientSocket.emit.called === false)
|
||||
assert(clientSocket.disconnect.called === false)
|
||||
done()
|
||||
}, 5)
|
||||
})
|
||||
})
|
||||
})
|