Merge pull request #1190 from hackmdio/refactor

replace package with npm published version
This commit is contained in:
Raccoon 2019-04-12 19:02:43 +08:00 committed by GitHub
commit 1434cdb6b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 2317 additions and 3582 deletions

View File

@ -8,12 +8,6 @@ env:
jobs:
include:
- env: task=npm-test
node_js:
- 6
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:
- 8

View File

@ -1,119 +1,117 @@
#!/usr/bin/env node
// First configure the logger so it does not spam the console
const logger = require("../lib/logger");
logger.transports.forEach((transport) => transport.level = "warning")
const logger = require('../lib/logger')
logger.transports.forEach((transport) => {
transport.level = 'warning'
})
const models = require("../lib/models/");
const readline = require("readline-sync");
const minimist = require("minimist");
const models = require('../lib/models/')
const readline = require('readline-sync')
const minimist = require('minimist')
function showUsage(tips) {
console.log(`${tips}
function showUsage (tips) {
console.log(`${tips}
Command-line utility to create users for email-signin.
Usage: bin/manage_users [--pass password] (--add | --del) user-email
Options:
--add Add user with the specified user-email
--del Delete user with specified user-email
--reset Reset user password with specified user-email
--pass Use password from cmdline rather than prompting
`);
process.exit(1);
Options:
--add\tAdd user with the specified user-email
--del\tDelete user with specified user-email
--reset\tReset user password with specified user-email
--pass\tUse password from cmdline rather than prompting
`)
process.exit(1)
}
function getPass(argv, action) {
// Find whether we use cmdline or prompt password
if(typeof argv["pass"] !== 'string') {
return readline.question(`Password for ${argv[action]}:`, {hideEchoBack: true});
}
console.log("Using password from commandline...");
return argv["pass"];
function getPass (argv, action) {
// Find whether we use cmdline or prompt password
if (typeof argv['pass'] !== 'string') {
return readline.question(`Password for ${argv[action]}:`, { hideEchoBack: true })
}
console.log('Using password from commandline...')
return argv['pass']
}
// Using an async function to be able to use await inside
async function createUser(argv) {
const existing_user = await models.User.findOne({where: {email: argv["add"]}});
// Cannot create already-existing users
if(existing_user != undefined) {
console.log(`User with e-mail ${existing_user.email} already exists! Aborting ...`);
process.exit(1);
}
async function createUser (argv) {
const existingUser = await models.User.findOne({ where: { email: argv['add'] } })
// Cannot create already-existing users
if (existingUser !== undefined) {
console.log(`User with e-mail ${existingUser.email} already exists! Aborting ...`)
process.exit(1)
}
const pass = getPass(argv, "add");
const pass = getPass(argv, 'add')
// Lets try to create, and check success
const ref = await models.User.create({email: argv["add"], password: pass});
if(ref == undefined) {
console.log(`Could not create user with email ${argv["add"]}`);
process.exit(1);
} else
console.log(`Created user with email ${argv["add"]}`);
// Lets try to create, and check success
const ref = await models.User.create({ email: argv['add'], password: pass })
if (ref === undefined) {
console.log(`Could not create user with email ${argv['add']}`)
process.exit(1)
} else { console.log(`Created user with email ${argv['add']}`) }
}
// Using an async function to be able to use await inside
async function deleteUser(argv) {
// Cannot delete non-existing users
const existing_user = await models.User.findOne({where: {email: argv["del"]}});
if(existing_user === undefined) {
console.log(`User with e-mail ${argv["del"]} does not exist, cannot delete`);
process.exit(1);
}
async function deleteUser (argv) {
// Cannot delete non-existing users
const existingUser = await models.User.findOne({ where: { email: argv['del'] } })
if (existingUser === undefined) {
console.log(`User with e-mail ${argv['del']} does not exist, cannot delete`)
process.exit(1)
}
// Sadly .destroy() does not return any success value with all
// backends. See sequelize #4124
await existing_user.destroy();
console.log(`Deleted user ${argv["del"]} ...`);
// Sadly .destroy() does not return any success value with all
// backends. See sequelize #4124
await existingUser.destroy()
console.log(`Deleted user ${argv['del']} ...`)
}
// Using an async function to be able to use await inside
async function resetUser(argv) {
const existing_user = await models.User.findOne({where: {email: argv["reset"]}});
// Cannot reset non-existing users
if(existing_user == undefined) {
console.log(`User with e-mail ${argv["reset"]} does not exist, cannot reset`);
process.exit(1);
}
async function resetUser (argv) {
const existingUser = await models.User.findOne({ where: { email: argv['reset'] } })
// Cannot reset non-existing users
if (existingUser === undefined) {
console.log(`User with e-mail ${argv['reset']} does not exist, cannot reset`)
process.exit(1)
}
const pass = getPass(argv, "reset");
const pass = getPass(argv, 'reset')
// set password and save
existing_user.password = pass;
await existing_user.save();
console.log(`User with email ${argv["reset"]} password has been reset`);
// set password and save
existingUser.password = pass
await existingUser.save()
console.log(`User with email ${argv['reset']} password has been reset`)
}
const options = {
add: createUser,
del: deleteUser,
reset: resetUser,
};
add: createUser,
del: deleteUser,
reset: resetUser
}
// Perform commandline-parsing
const argv = minimist(process.argv.slice(2));
const argv = minimist(process.argv.slice(2))
const keys = Object.keys(options);
const opts = keys.filter((key) => argv[key] !== undefined);
const action = opts[0];
const keys = Object.keys(options)
const opts = keys.filter((key) => argv[key] !== undefined)
const action = opts[0]
// Check for options missing
if (opts.length === 0) {
showUsage(`You did not specify either ${keys.map((key) => `--${key}`).join(' or ')}!`);
showUsage(`You did not specify either ${keys.map((key) => `--${key}`).join(' or ')}!`)
}
// Check if both are specified
if (opts.length > 1) {
showUsage(`You cannot ${action.join(' and ')} at the same time!`);
showUsage(`You cannot ${action.join(' and ')} at the same time!`)
}
// Check if not string
if (typeof argv[action] !== 'string') {
showUsage(`You must follow an email after --${action}`);
showUsage(`You must follow an email after --${action}`)
}
// Call respective processing functions
options[action](argv).then(function() {
process.exit(0);
});
options[action](argv).then(function () {
process.exit(0)
})

View File

@ -1,6 +1,6 @@
'use strict'
const {toBooleanConfig, toArrayConfig, toIntegerConfig} = require('./utils')
const { toBooleanConfig, toArrayConfig, toIntegerConfig } = require('./utils')
module.exports = {
sourceURL: process.env.CMD_SOURCE_URL,

View File

@ -1,6 +1,6 @@
'use strict'
const {toBooleanConfig, toArrayConfig, toIntegerConfig} = require('./utils')
const { toBooleanConfig, toArrayConfig, toIntegerConfig } = require('./utils')
module.exports = {
domain: process.env.HMD_DOMAIN,

View File

@ -4,11 +4,11 @@
const crypto = require('crypto')
const fs = require('fs')
const path = require('path')
const {merge} = require('lodash')
const { merge } = require('lodash')
const deepFreeze = require('deep-freeze')
const {Environment, Permission} = require('./enum')
const { Environment, Permission } = require('./enum')
const logger = require('../logger')
const {getGitCommit, getGitHubURL} = require('./utils')
const { getGitCommit, getGitHubURL } = require('./utils')
const appRootPath = path.resolve(__dirname, '../../')
const env = process.env.NODE_ENV || Environment.development
@ -17,7 +17,7 @@ const debugConfig = {
}
// Get version string from package.json
const {version, repository} = require(path.join(appRootPath, 'package.json'))
const { version, repository } = require(path.join(appRootPath, 'package.json'))
const commitID = getGitCommit(appRootPath)
const sourceURL = getGitHubURL(repository.url, commitID || version)
@ -159,8 +159,8 @@ if (Object.keys(process.env).toString().indexOf('HMD_') !== -1) {
if (config.sessionSecret === 'secret') {
logger.warn('Session secret not set. Using random generated one. Please set `sessionSecret` in your config.js file. All users will be logged out.')
config.sessionSecret = crypto.randomBytes(Math.ceil(config.sessionSecretLen / 2)) // generate crypto graphic random number
.toString('hex') // convert to hexadecimal format
.slice(0, config.sessionSecretLen) // return required number of characters
.toString('hex') // convert to hexadecimal format
.slice(0, config.sessionSecretLen) // return required number of characters
}
// Validate upload upload providers

View File

@ -1,6 +1,6 @@
'use strict'
const {toBooleanConfig} = require('./utils')
const { toBooleanConfig } = require('./utils')
module.exports = {
debug: toBooleanConfig(process.env.DEBUG),

View File

@ -1,7 +1,7 @@
'use strict'
// history
// external modules
var LZString = require('lz-string')
var LZString = require('@hackmd/lz-string')
// core
var config = require('./config')

View File

@ -30,14 +30,14 @@ exports.generateAvatarURL = function (name, email = '', big = true) {
if (typeof email !== 'string') {
email = '' + name + '@example.com'
}
name=encodeURIComponent(name)
name = encodeURIComponent(name)
let hash = crypto.createHash('md5')
hash.update(email.toLowerCase())
let hexDigest = hash.digest('hex')
if (email !== '' && config.allowGravatar) {
photo = 'https://www.gravatar.com/avatar/' + hexDigest;
photo = 'https://www.gravatar.com/avatar/' + hexDigest
if (big) {
photo += '?s=400'
} else {

View File

@ -1,5 +1,5 @@
'use strict'
const {createLogger, format, transports} = require('winston')
const { createLogger, format, transports } = require('winston')
const logger = createLogger({
level: 'debug',

View File

@ -18,8 +18,8 @@ module.exports = {
down: function (queryInterface, Sequelize) {
return queryInterface.removeColumn('Notes', 'lastchangeAt')
.then(function () {
return queryInterface.removeColumn('Notes', 'lastchangeuserId')
})
.then(function () {
return queryInterface.removeColumn('Notes', 'lastchangeuserId')
})
}
}

View File

@ -1,16 +1,16 @@
'use strict'
module.exports = {
up: function (queryInterface, Sequelize) {
queryInterface.changeColumn('Notes', 'content', {type: Sequelize.TEXT('long')})
queryInterface.changeColumn('Revisions', 'patch', {type: Sequelize.TEXT('long')})
queryInterface.changeColumn('Revisions', 'content', {type: Sequelize.TEXT('long')})
queryInterface.changeColumn('Revisions', 'lastContent', {type: Sequelize.TEXT('long')})
queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT('long') })
queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT('long') })
queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT('long') })
queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT('long') })
},
down: function (queryInterface, Sequelize) {
queryInterface.changeColumn('Notes', 'content', {type: Sequelize.TEXT})
queryInterface.changeColumn('Revisions', 'patch', {type: Sequelize.TEXT})
queryInterface.changeColumn('Revisions', 'content', {type: Sequelize.TEXT})
queryInterface.changeColumn('Revisions', 'lastContent', {type: Sequelize.TEXT})
queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT })
queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT })
queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT })
queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT })
}
}

View File

@ -2,12 +2,12 @@
module.exports = {
up: function (queryInterface, Sequelize) {
queryInterface.changeColumn('Notes', 'authorship', {type: Sequelize.TEXT('long')})
queryInterface.changeColumn('Revisions', 'authorship', {type: Sequelize.TEXT('long')})
queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT('long') })
queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT('long') })
},
down: function (queryInterface, Sequelize) {
queryInterface.changeColumn('Notes', 'authorship', {type: Sequelize.TEXT})
queryInterface.changeColumn('Revisions', 'authorship', {type: Sequelize.TEXT})
queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT })
queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT })
}
}

View File

@ -2,10 +2,10 @@
module.exports = {
up: function (queryInterface, Sequelize) {
queryInterface.changeColumn('Notes', 'permission', {type: Sequelize.ENUM('freely', 'editable', 'limited', 'locked', 'protected', 'private')})
queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'limited', 'locked', 'protected', 'private') })
},
down: function (queryInterface, Sequelize) {
queryInterface.changeColumn('Notes', 'permission', {type: Sequelize.ENUM('freely', 'editable', 'locked', 'private')})
queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'locked', 'private') })
}
}

View File

@ -18,25 +18,25 @@ module.exports = function (sequelize, DataTypes) {
unique: true,
fields: ['noteId', 'userId']
}
],
classMethods: {
associate: function (models) {
Author.belongsTo(models.Note, {
foreignKey: 'noteId',
as: 'note',
constraints: false,
onDelete: 'CASCADE',
hooks: true
})
Author.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user',
constraints: false,
onDelete: 'CASCADE',
hooks: true
})
}
}
]
})
Author.associate = function (models) {
Author.belongsTo(models.Note, {
foreignKey: 'noteId',
as: 'note',
constraints: false,
onDelete: 'CASCADE',
hooks: true
})
Author.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user',
constraints: false,
onDelete: 'CASCADE',
hooks: true
})
}
return Author
}

View File

@ -3,14 +3,16 @@
var fs = require('fs')
var path = require('path')
var Sequelize = require('sequelize')
const {cloneDeep} = require('lodash')
const { cloneDeep } = require('lodash')
// core
var config = require('../config')
var logger = require('../logger')
var dbconfig = cloneDeep(config.db)
dbconfig.logging = config.debug ? logger.info : false
dbconfig.logging = config.debug ? (data) => {
logger.info(data)
} : false
var sequelize = null
@ -39,13 +41,13 @@ sequelize.processData = processData
var db = {}
fs.readdirSync(__dirname)
.filter(function (file) {
return (file.indexOf('.') !== 0) && (file !== 'index.js')
})
.forEach(function (file) {
var model = sequelize.import(path.join(__dirname, file))
db[model.name] = model
})
.filter(function (file) {
return (file.indexOf('.') !== 0) && (file !== 'index.js')
})
.forEach(function (file) {
var model = sequelize.import(path.join(__dirname, file))
db[model.name] = model
})
Object.keys(db).forEach(function (modelName) {
if ('associate' in db[modelName]) {

View File

@ -2,7 +2,7 @@
// external modules
var fs = require('fs')
var path = require('path')
var LZString = require('lz-string')
var LZString = require('@hackmd/lz-string')
var base64url = require('base64url')
var md = require('markdown-it')()
var metaMarked = require('meta-marked')
@ -11,9 +11,10 @@ var shortId = require('shortid')
var Sequelize = require('sequelize')
var async = require('async')
var moment = require('moment')
var DiffMatchPatch = require('diff-match-patch')
var DiffMatchPatch = require('@hackmd/diff-match-patch')
var dmp = new DiffMatchPatch()
var S = require('string')
const { stripTags } = require('../../utils/string')
// core
var config = require('../config')
@ -86,486 +87,492 @@ module.exports = function (sequelize, DataTypes) {
}
}, {
paranoid: false,
classMethods: {
associate: function (models) {
Note.belongsTo(models.User, {
foreignKey: 'ownerId',
as: 'owner',
constraints: false,
onDelete: 'CASCADE',
hooks: true
})
Note.belongsTo(models.User, {
foreignKey: 'lastchangeuserId',
as: 'lastchangeuser',
constraints: false
})
Note.hasMany(models.Revision, {
foreignKey: 'noteId',
constraints: false
})
Note.hasMany(models.Author, {
foreignKey: 'noteId',
as: 'authors',
constraints: false
})
},
checkFileExist: function (filePath) {
try {
return fs.statSync(filePath).isFile()
} catch (err) {
return false
}
},
encodeNoteId: function (id) {
// remove dashes in UUID and encode in url-safe base64
let str = id.replace(/-/g, '')
let hexStr = Buffer.from(str, 'hex')
return base64url.encode(hexStr)
},
decodeNoteId: function (encodedId) {
// decode from url-safe base64
let id = base64url.toBuffer(encodedId).toString('hex')
// add dashes between the UUID string parts
let idParts = []
idParts.push(id.substr(0, 8))
idParts.push(id.substr(8, 4))
idParts.push(id.substr(12, 4))
idParts.push(id.substr(16, 4))
idParts.push(id.substr(20, 12))
return idParts.join('-')
},
checkNoteIdValid: function (id) {
var 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
var result = id.match(uuidRegex)
if (result && result.length === 1) { return true } else { return false }
},
parseNoteId: function (noteId, callback) {
async.series({
parseNoteIdByAlias: function (_callback) {
// try to parse note id by alias (e.g. doc)
Note.findOne({
where: {
alias: noteId
}
}).then(function (note) {
if (note) {
let 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
var fsModifiedTime = moment(fs.statSync(filePath).mtime)
var dbModifiedTime = moment(note.lastchangeAt || note.createdAt)
var body = fs.readFileSync(filePath, 'utf8')
var contentLength = body.length
var title = Note.parseNoteTitle(body)
if (fsModifiedTime.isAfter(dbModifiedTime) && note.content !== body) {
note.update({
title: title,
content: body,
lastchangeAt: fsModifiedTime
}).then(function (note) {
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
if (err) return _callback(err, null)
// update authorship on after making revision of docs
var patch = dmp.patch_fromText(revision.patch)
var operations = Note.transformPatchToOperations(patch, contentLength)
var authorship = note.authorship
for (let i = 0; i < operations.length; i++) {
authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship)
}
note.update({
authorship: authorship
}).then(function (note) {
return callback(null, note.id)
}).catch(function (err) {
return _callback(err, null)
})
})
}).catch(function (err) {
return _callback(err, null)
})
} else {
return callback(null, note.id)
}
} else {
return callback(null, note.id)
}
} else {
var filePath = path.join(config.docsPath, noteId + '.md')
if (Note.checkFileExist(filePath)) {
Note.create({
alias: noteId,
owner: null,
permission: 'locked'
}).then(function (note) {
return callback(null, note.id)
}).catch(function (err) {
return _callback(err, null)
})
} else {
return _callback(null, null)
}
}
}).catch(function (err) {
return _callback(err, null)
})
},
// parse note id by LZString is deprecated, here for compability
parseNoteIdByLZString: function (_callback) {
// Calculate minimal string length for an UUID that is encoded
// base64 encoded and optimize comparsion by using -1
// this should make a lot of LZ-String parsing errors obsolete
// as we can assume that a nodeId that is 48 chars or longer is a
// noteID.
const base64UuidLength = ((4 * 36) / 3) - 1
if (!(noteId.length > base64UuidLength)) {
return _callback(null, null)
}
// try to parse note id by LZString Base64
try {
var id = LZString.decompressFromBase64(noteId)
if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) }
} catch (err) {
if (err.message === 'Cannot read property \'charAt\' of undefined') {
logger.warning('Looks like we can not decode "' + noteId + '" with LZString. Can be ignored.')
} else {
logger.error(err)
}
return _callback(null, null)
}
},
parseNoteIdByBase64Url: function (_callback) {
// try to parse note id by base64url
try {
var id = Note.decodeNoteId(noteId)
if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) }
} catch (err) {
logger.error(err)
return _callback(null, null)
}
},
parseNoteIdByShortId: function (_callback) {
// try to parse note id by shortId
try {
if (shortId.isValid(noteId)) {
Note.findOne({
where: {
shortid: noteId
}
}).then(function (note) {
if (!note) return _callback(null, null)
return callback(null, note.id)
}).catch(function (err) {
return _callback(err, null)
})
} else {
return _callback(null, null)
}
} catch (err) {
return _callback(err, null)
}
}
}, function (err, result) {
if (err) {
logger.error(err)
return callback(err, null)
}
return callback(null, null)
})
},
parseNoteInfo: function (body) {
var parsed = Note.extractMeta(body)
var $ = cheerio.load(md.render(parsed.markdown))
return {
title: Note.extractNoteTitle(parsed.meta, $),
tags: Note.extractNoteTags(parsed.meta, $)
}
},
parseNoteTitle: function (body) {
var parsed = Note.extractMeta(body)
var $ = cheerio.load(md.render(parsed.markdown))
return Note.extractNoteTitle(parsed.meta, $)
},
extractNoteTitle: function (meta, $) {
var title = ''
if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) {
title = meta.title
} else {
var h1s = $('h1')
if (h1s.length > 0 && h1s.first().text().split('\n').length === 1) { title = S(h1s.first().text()).stripTags().s }
}
if (!title) title = 'Untitled'
return title
},
generateDescription: function (markdown) {
return markdown.substr(0, 100).replace(/(?:\r\n|\r|\n)/g, ' ')
},
decodeTitle: function (title) {
return title || 'Untitled'
},
generateWebTitle: function (title) {
title = !title || title === 'Untitled' ? 'CodiMD - Collaborative markdown notes' : title + ' - CodiMD'
return title
},
extractNoteTags: function (meta, $) {
var tags = []
var rawtags = []
if (meta.tags && (typeof meta.tags === 'string' || typeof meta.tags === 'number')) {
var metaTags = ('' + meta.tags).split(',')
for (let i = 0; i < metaTags.length; i++) {
var text = metaTags[i].trim()
if (text) rawtags.push(text)
}
} else {
var h6s = $('h6')
h6s.each(function (key, value) {
if (/^tags/gmi.test($(value).text())) {
var codes = $(value).find('code')
for (let i = 0; i < codes.length; i++) {
var text = S($(codes[i]).text().trim()).stripTags().s
if (text) rawtags.push(text)
}
}
})
}
for (let i = 0; i < rawtags.length; i++) {
var found = false
for (let j = 0; j < tags.length; j++) {
if (tags[j] === rawtags[i]) {
found = true
break
}
}
if (!found) { tags.push(rawtags[i]) }
}
return tags
},
extractMeta: function (content) {
var obj = null
try {
obj = metaMarked(content)
if (!obj.markdown) obj.markdown = ''
if (!obj.meta) obj.meta = {}
} catch (err) {
obj = {
markdown: content,
meta: {}
}
}
return obj
},
parseMeta: function (meta) {
var _meta = {}
if (meta) {
if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { _meta.title = meta.title }
if (meta.description && (typeof meta.description === 'string' || typeof meta.description === 'number')) { _meta.description = meta.description }
if (meta.robots && (typeof meta.robots === 'string' || typeof meta.robots === 'number')) { _meta.robots = meta.robots }
if (meta.GA && (typeof meta.GA === 'string' || typeof meta.GA === 'number')) { _meta.GA = meta.GA }
if (meta.disqus && (typeof meta.disqus === 'string' || typeof meta.disqus === 'number')) { _meta.disqus = meta.disqus }
if (meta.slideOptions && (typeof meta.slideOptions === 'object')) { _meta.slideOptions = meta.slideOptions }
}
return _meta
},
updateAuthorshipByOperation: function (operation, userId, authorships) {
var index = 0
var timestamp = Date.now()
for (let i = 0; i < operation.length; i++) {
var op = operation[i]
if (ot.TextOperation.isRetain(op)) {
index += op
} else if (ot.TextOperation.isInsert(op)) {
let opStart = index
let opEnd = index + op.length
var 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]
if (!inserted) {
let 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
authorship[2] = opStart
authorship[4] = timestamp
authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp])
authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp])
j += 2
inserted = true
} else if (authorship[1] >= opStart) {
authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp])
j += 1
inserted = true
} else if (authorship[2] <= opStart) {
authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp])
j += 1
inserted = true
}
}
}
if (authorship[1] >= opStart) {
authorship[1] += op.length
authorship[2] += op.length
}
}
}
index += op.length
} else if (ot.TextOperation.isDelete(op)) {
let opStart = index
let 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]
if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) {
authorships.splice(j, 1)
j -= 1
} else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) {
authorship[2] += op
authorship[4] = timestamp
} else if (authorship[2] >= opStart && authorship[2] <= opEnd) {
authorship[2] = opStart
authorship[4] = timestamp
} else if (authorship[1] >= opStart && authorship[1] <= opEnd) {
authorship[1] = opEnd
authorship[4] = timestamp
}
if (authorship[1] >= opEnd) {
authorship[1] += op
authorship[2] += op
}
}
}
index += op
}
}
// merge
for (let j = 0; j < authorships.length; j++) {
let authorship = authorships[j]
for (let k = j + 1; k < authorships.length; k++) {
let 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])
authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp])
authorships.splice(k, 1)
j -= 1
break
}
}
}
// clear
for (let j = 0; j < authorships.length; j++) {
let authorship = authorships[j]
if (!authorship[0]) {
authorships.splice(j, 1)
j -= 1
}
}
return authorships
},
transformPatchToOperations: function (patch, contentLength) {
var operations = []
if (patch.length > 0) {
// calculate original content length
for (let j = patch.length - 1; j >= 0; j--) {
var p = patch[j]
for (let i = 0; i < p.diffs.length; i++) {
var diff = p.diffs[i]
switch (diff[0]) {
case 1: // insert
contentLength -= diff[1].length
break
case -1: // delete
contentLength += diff[1].length
break
}
}
}
// generate operations
var bias = 0
var lengthBias = 0
for (let j = 0; j < patch.length; j++) {
var operation = []
let 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]
switch (diff[0]) {
case 0: // retain
if (i === 0) {
// first
operation.push(currIndex + diff[1].length)
} else if (i !== p.diffs.length - 1) {
// mid
operation.push(diff[1].length)
} else {
// last
operation.push(currLength + lengthBias - currIndex)
}
currIndex += diff[1].length
break
case 1: // insert
operation.push(diff[1])
lengthBias += diff[1].length
currIndex += diff[1].length
break
case -1: // delete
operation.push(-diff[1].length)
bias += diff[1].length
currIndex += diff[1].length
break
}
}
operations.push(operation)
}
}
return operations
}
},
hooks: {
beforeCreate: function (note, options, callback) {
// if no content specified then use default note
if (!note.content) {
var body = null
let filePath = null
if (!note.alias) {
filePath = config.defaultNotePath
} else {
filePath = path.join(config.docsPath, note.alias + '.md')
}
if (Note.checkFileExist(filePath)) {
var fsCreatedTime = moment(fs.statSync(filePath).ctime)
body = fs.readFileSync(filePath, 'utf8')
note.title = Note.parseNoteTitle(body)
note.content = body
if (filePath !== config.defaultNotePath) {
note.createdAt = fsCreatedTime
beforeCreate: function (note, options) {
return new Promise(function (resolve, reject) {
// if no content specified then use default note
if (!note.content) {
var body = null
let filePath = null
if (!note.alias) {
filePath = config.defaultNotePath
} else {
filePath = path.join(config.docsPath, note.alias + '.md')
}
if (Note.checkFileExist(filePath)) {
var fsCreatedTime = moment(fs.statSync(filePath).ctime)
body = fs.readFileSync(filePath, 'utf8')
note.title = Note.parseNoteTitle(body)
note.content = body
if (filePath !== config.defaultNotePath) {
note.createdAt = fsCreatedTime
}
}
}
}
// if no permission specified and have owner then give default permission in config, else default permission is freely
if (!note.permission) {
if (note.ownerId) {
note.permission = config.defaultPermission
} else {
note.permission = 'freely'
// if no permission specified and have owner then give default permission in config, else default permission is freely
if (!note.permission) {
if (note.ownerId) {
note.permission = config.defaultPermission
} else {
note.permission = 'freely'
}
}
}
return callback(null, note)
return resolve(note)
})
},
afterCreate: function (note, options, callback) {
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
callback(err, note)
return new Promise(function (resolve, reject) {
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
if (err) {
return reject(err)
}
return resolve(note)
})
})
}
}
})
Note.associate = function (models) {
Note.belongsTo(models.User, {
foreignKey: 'ownerId',
as: 'owner',
constraints: false,
onDelete: 'CASCADE',
hooks: true
})
Note.belongsTo(models.User, {
foreignKey: 'lastchangeuserId',
as: 'lastchangeuser',
constraints: false
})
Note.hasMany(models.Revision, {
foreignKey: 'noteId',
constraints: false
})
Note.hasMany(models.Author, {
foreignKey: 'noteId',
as: 'authors',
constraints: false
})
}
Note.checkFileExist = function (filePath) {
try {
return fs.statSync(filePath).isFile()
} catch (err) {
return false
}
}
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')
return base64url.encode(hexStr)
}
Note.decodeNoteId = function (encodedId) {
// decode from url-safe base64
let id = base64url.toBuffer(encodedId).toString('hex')
// add dashes between the UUID string parts
let idParts = []
idParts.push(id.substr(0, 8))
idParts.push(id.substr(8, 4))
idParts.push(id.substr(12, 4))
idParts.push(id.substr(16, 4))
idParts.push(id.substr(20, 12))
return idParts.join('-')
}
Note.checkNoteIdValid = function (id) {
var 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
var result = id.match(uuidRegex)
if (result && result.length === 1) { return true } else { return false }
}
Note.parseNoteId = function (noteId, callback) {
async.series({
parseNoteIdByAlias: function (_callback) {
// try to parse note id by alias (e.g. doc)
Note.findOne({
where: {
alias: noteId
}
}).then(function (note) {
if (note) {
let 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
var fsModifiedTime = moment(fs.statSync(filePath).mtime)
var dbModifiedTime = moment(note.lastchangeAt || note.createdAt)
var body = fs.readFileSync(filePath, 'utf8')
var contentLength = body.length
var title = Note.parseNoteTitle(body)
if (fsModifiedTime.isAfter(dbModifiedTime) && note.content !== body) {
note.update({
title: title,
content: body,
lastchangeAt: fsModifiedTime
}).then(function (note) {
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
if (err) return _callback(err, null)
// update authorship on after making revision of docs
var patch = dmp.patch_fromText(revision.patch)
var operations = Note.transformPatchToOperations(patch, contentLength)
var authorship = note.authorship
for (let i = 0; i < operations.length; i++) {
authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship)
}
note.update({
authorship: authorship
}).then(function (note) {
return callback(null, note.id)
}).catch(function (err) {
return _callback(err, null)
})
})
}).catch(function (err) {
return _callback(err, null)
})
} else {
return callback(null, note.id)
}
} else {
return callback(null, note.id)
}
} else {
var filePath = path.join(config.docsPath, noteId + '.md')
if (Note.checkFileExist(filePath)) {
Note.create({
alias: noteId,
owner: null,
permission: 'locked'
}).then(function (note) {
return callback(null, note.id)
}).catch(function (err) {
return _callback(err, null)
})
} else {
return _callback(null, null)
}
}
}).catch(function (err) {
return _callback(err, null)
})
},
// parse note id by LZString is deprecated, here for compability
parseNoteIdByLZString: function (_callback) {
// Calculate minimal string length for an UUID that is encoded
// base64 encoded and optimize comparsion by using -1
// this should make a lot of LZ-String parsing errors obsolete
// as we can assume that a nodeId that is 48 chars or longer is a
// noteID.
const base64UuidLength = ((4 * 36) / 3) - 1
if (!(noteId.length > base64UuidLength)) {
return _callback(null, null)
}
// try to parse note id by LZString Base64
try {
var id = LZString.decompressFromBase64(noteId)
if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) }
} catch (err) {
if (err.message === 'Cannot read property \'charAt\' of undefined') {
logger.warning('Looks like we can not decode "' + noteId + '" with LZString. Can be ignored.')
} else {
logger.error(err)
}
return _callback(null, null)
}
},
parseNoteIdByBase64Url: function (_callback) {
// try to parse note id by base64url
try {
var id = Note.decodeNoteId(noteId)
if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) }
} catch (err) {
logger.error(err)
return _callback(null, null)
}
},
parseNoteIdByShortId: function (_callback) {
// try to parse note id by shortId
try {
if (shortId.isValid(noteId)) {
Note.findOne({
where: {
shortid: noteId
}
}).then(function (note) {
if (!note) return _callback(null, null)
return callback(null, note.id)
}).catch(function (err) {
return _callback(err, null)
})
} else {
return _callback(null, null)
}
} catch (err) {
return _callback(err, null)
}
}
}, function (err, result) {
if (err) {
logger.error(err)
return callback(err, null)
}
return callback(null, null)
})
}
Note.parseNoteInfo = function (body) {
var parsed = Note.extractMeta(body)
var $ = cheerio.load(md.render(parsed.markdown))
return {
title: Note.extractNoteTitle(parsed.meta, $),
tags: Note.extractNoteTags(parsed.meta, $)
}
}
Note.parseNoteTitle = function (body) {
var parsed = Note.extractMeta(body)
var $ = cheerio.load(md.render(parsed.markdown))
return Note.extractNoteTitle(parsed.meta, $)
}
Note.extractNoteTitle = function (meta, $) {
var title = ''
if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) {
title = meta.title
} else {
var h1s = $('h1')
if (h1s.length > 0 && h1s.first().text().split('\n').length === 1) { title = stripTags(h1s.first().text()) }
}
if (!title) title = 'Untitled'
return title
}
Note.generateDescription = function (markdown) {
return markdown.substr(0, 100).replace(/(?:\r\n|\r|\n)/g, ' ')
}
Note.decodeTitle = function (title) {
return title || 'Untitled'
}
Note.generateWebTitle = function (title) {
title = !title || title === 'Untitled' ? 'CodiMD - Collaborative markdown notes' : title + ' - CodiMD'
return title
}
Note.extractNoteTags = function (meta, $) {
var tags = []
var rawtags = []
if (meta.tags && (typeof meta.tags === 'string' || typeof meta.tags === 'number')) {
var metaTags = ('' + meta.tags).split(',')
for (let i = 0; i < metaTags.length; i++) {
var text = metaTags[i].trim()
if (text) rawtags.push(text)
}
} else {
var h6s = $('h6')
h6s.each(function (key, value) {
if (/^tags/gmi.test($(value).text())) {
var codes = $(value).find('code')
for (let i = 0; i < codes.length; i++) {
var text = stripTags($(codes[i]).text().trim())
if (text) rawtags.push(text)
}
}
})
}
for (let i = 0; i < rawtags.length; i++) {
var found = false
for (let j = 0; j < tags.length; j++) {
if (tags[j] === rawtags[i]) {
found = true
break
}
}
if (!found) { tags.push(rawtags[i]) }
}
return tags
}
Note.extractMeta = function (content) {
var obj = null
try {
obj = metaMarked(content)
if (!obj.markdown) obj.markdown = ''
if (!obj.meta) obj.meta = {}
} catch (err) {
obj = {
markdown: content,
meta: {}
}
}
return obj
}
Note.parseMeta = function (meta) {
var _meta = {}
if (meta) {
if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { _meta.title = meta.title }
if (meta.description && (typeof meta.description === 'string' || typeof meta.description === 'number')) { _meta.description = meta.description }
if (meta.robots && (typeof meta.robots === 'string' || typeof meta.robots === 'number')) { _meta.robots = meta.robots }
if (meta.GA && (typeof meta.GA === 'string' || typeof meta.GA === 'number')) { _meta.GA = meta.GA }
if (meta.disqus && (typeof meta.disqus === 'string' || typeof meta.disqus === 'number')) { _meta.disqus = meta.disqus }
if (meta.slideOptions && (typeof meta.slideOptions === 'object')) { _meta.slideOptions = meta.slideOptions }
}
return _meta
}
Note.updateAuthorshipByOperation = function (operation, userId, authorships) {
var index = 0
var timestamp = Date.now()
for (let i = 0; i < operation.length; i++) {
var op = operation[i]
if (ot.TextOperation.isRetain(op)) {
index += op
} else if (ot.TextOperation.isInsert(op)) {
let opStart = index
let opEnd = index + op.length
var 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]
if (!inserted) {
let 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
authorship[2] = opStart
authorship[4] = timestamp
authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp])
authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp])
j += 2
inserted = true
} else if (authorship[1] >= opStart) {
authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp])
j += 1
inserted = true
} else if (authorship[2] <= opStart) {
authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp])
j += 1
inserted = true
}
}
}
if (authorship[1] >= opStart) {
authorship[1] += op.length
authorship[2] += op.length
}
}
}
index += op.length
} else if (ot.TextOperation.isDelete(op)) {
let opStart = index
let 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]
if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) {
authorships.splice(j, 1)
j -= 1
} else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) {
authorship[2] += op
authorship[4] = timestamp
} else if (authorship[2] >= opStart && authorship[2] <= opEnd) {
authorship[2] = opStart
authorship[4] = timestamp
} else if (authorship[1] >= opStart && authorship[1] <= opEnd) {
authorship[1] = opEnd
authorship[4] = timestamp
}
if (authorship[1] >= opEnd) {
authorship[1] += op
authorship[2] += op
}
}
}
index += op
}
}
// merge
for (let j = 0; j < authorships.length; j++) {
let authorship = authorships[j]
for (let k = j + 1; k < authorships.length; k++) {
let 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])
authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp])
authorships.splice(k, 1)
j -= 1
break
}
}
}
// clear
for (let j = 0; j < authorships.length; j++) {
let authorship = authorships[j]
if (!authorship[0]) {
authorships.splice(j, 1)
j -= 1
}
}
return authorships
}
Note.transformPatchToOperations = function (patch, contentLength) {
var operations = []
if (patch.length > 0) {
// calculate original content length
for (let j = patch.length - 1; j >= 0; j--) {
var p = patch[j]
for (let i = 0; i < p.diffs.length; i++) {
var diff = p.diffs[i]
switch (diff[0]) {
case 1: // insert
contentLength -= diff[1].length
break
case -1: // delete
contentLength += diff[1].length
break
}
}
}
// generate operations
var bias = 0
var lengthBias = 0
for (let j = 0; j < patch.length; j++) {
var operation = []
let 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]
switch (diff[0]) {
case 0: // retain
if (i === 0) {
// first
operation.push(currIndex + diff[1].length)
} else if (i !== p.diffs.length - 1) {
// mid
operation.push(diff[1].length)
} else {
// last
operation.push(currLength + lengthBias - currIndex)
}
currIndex += diff[1].length
break
case 1: // insert
operation.push(diff[1])
lengthBias += diff[1].length
currIndex += diff[1].length
break
case -1: // delete
operation.push(-diff[1].length)
bias += diff[1].length
currIndex += diff[1].length
break
}
}
operations.push(operation)
}
}
return operations
}
return Note
}

View File

@ -7,6 +7,8 @@ var childProcess = require('child_process')
var shortId = require('shortid')
var path = require('path')
var Op = Sequelize.Op
// core
var config = require('../config')
var logger = require('../logger')
@ -97,214 +99,212 @@ module.exports = function (sequelize, DataTypes) {
this.setDataValue('authorship', value ? JSON.stringify(value) : value)
}
}
}, {
classMethods: {
associate: function (models) {
Revision.belongsTo(models.Note, {
foreignKey: 'noteId',
as: 'note',
constraints: false,
onDelete: 'CASCADE',
hooks: true
})
})
Revision.associate = function (models) {
Revision.belongsTo(models.Note, {
foreignKey: 'noteId',
as: 'note',
constraints: false,
onDelete: 'CASCADE',
hooks: true
})
}
Revision.getNoteRevisions = function (note, callback) {
Revision.findAll({
where: {
noteId: note.id
},
getNoteRevisions: function (note, callback) {
Revision.findAll({
where: {
noteId: note.id
},
order: [['createdAt', 'DESC']]
}).then(function (revisions) {
var data = []
for (var i = 0, l = revisions.length; i < l; i++) {
var revision = revisions[i]
data.push({
time: moment(revision.createdAt).valueOf(),
length: revision.length
})
order: [['createdAt', 'DESC']]
}).then(function (revisions) {
var data = []
for (var i = 0, l = revisions.length; i < l; i++) {
var revision = revisions[i]
data.push({
time: moment(revision.createdAt).valueOf(),
length: revision.length
})
}
callback(null, data)
}).catch(function (err) {
callback(err, null)
})
}
Revision.getPatchedNoteRevisionByTime = function (note, time, callback) {
// find all revisions to prepare for all possible calculation
Revision.findAll({
where: {
noteId: note.id
},
order: [['createdAt', 'DESC']]
}).then(function (revisions) {
if (revisions.length <= 0) return callback(null, null)
// measure target revision position
Revision.count({
where: {
noteId: note.id,
createdAt: {
[Op.gte]: time
}
callback(null, data)
}).catch(function (err) {
callback(err, null)
})
},
getPatchedNoteRevisionByTime: function (note, time, callback) {
// find all revisions to prepare for all possible calculation
Revision.findAll({
where: {
noteId: note.id
},
order: [['createdAt', 'DESC']]
}).then(function (revisions) {
if (revisions.length <= 0) return callback(null, null)
// measure target revision position
Revision.count({
where: {
noteId: note.id,
createdAt: {
$gte: time
},
order: [['createdAt', 'DESC']]
}).then(function (count) {
if (count <= 0) return callback(null, null)
sendDmpWorker({
msg: 'get revision',
revisions: revisions,
count: count
}, callback)
}).catch(function (err) {
return callback(err, null)
})
}).catch(function (err) {
return callback(err, null)
})
}
Revision.checkAllNotesRevision = function (callback) {
Revision.saveAllNotesRevision(function (err, notes) {
if (err) return callback(err, null)
if (!notes || notes.length <= 0) {
return callback(null, notes)
} else {
Revision.checkAllNotesRevision(callback)
}
})
}
Revision.saveAllNotesRevision = function (callback) {
sequelize.models.Note.findAll({
// query all notes that need to save for revision
where: {
[Op.and]: [
{
lastchangeAt: {
[Op.or]: {
[Op.eq]: null,
[Op.and]: {
[Op.ne]: null,
[Op.gt]: sequelize.col('createdAt')
}
}
},
order: [['createdAt', 'DESC']]
}).then(function (count) {
if (count <= 0) return callback(null, null)
sendDmpWorker({
msg: 'get revision',
revisions: revisions,
count: count
}, callback)
}).catch(function (err) {
return callback(err, null)
})
}).catch(function (err) {
return callback(err, null)
})
},
checkAllNotesRevision: function (callback) {
Revision.saveAllNotesRevision(function (err, notes) {
if (err) return callback(err, null)
if (!notes || notes.length <= 0) {
return callback(null, notes)
}
},
{
savedAt: {
[Op.or]: {
[Op.eq]: null,
[Op.lt]: sequelize.col('lastchangeAt')
}
}
}
]
}
}).then(function (notes) {
if (notes.length <= 0) return callback(null, notes)
var savedNotes = []
async.each(notes, function (note, _callback) {
// revision saving policy: note not been modified for 5 mins or not save for 10 mins
if (note.lastchangeAt && note.savedAt) {
var lastchangeAt = moment(note.lastchangeAt)
var savedAt = moment(note.savedAt)
if (moment().isAfter(lastchangeAt.add(5, 'minutes'))) {
savedNotes.push(note)
Revision.saveNoteRevision(note, _callback)
} else if (lastchangeAt.isAfter(savedAt.add(10, 'minutes'))) {
savedNotes.push(note)
Revision.saveNoteRevision(note, _callback)
} else {
Revision.checkAllNotesRevision(callback)
return _callback(null, null)
}
})
} else {
savedNotes.push(note)
Revision.saveNoteRevision(note, _callback)
}
}, function (err) {
if (err) {
return callback(err, null)
}
// return null when no notes need saving at this moment but have delayed tasks to be done
var result = ((savedNotes.length === 0) && (notes.length > savedNotes.length)) ? null : savedNotes
return callback(null, result)
})
}).catch(function (err) {
return callback(err, null)
})
}
Revision.saveNoteRevision = function (note, callback) {
Revision.findAll({
where: {
noteId: note.id
},
saveAllNotesRevision: function (callback) {
sequelize.models.Note.findAll({
// query all notes that need to save for revision
where: {
$and: [
{
lastchangeAt: {
$or: {
$eq: null,
$and: {
$ne: null,
$gt: sequelize.col('createdAt')
}
}
}
},
{
savedAt: {
$or: {
$eq: null,
$lt: sequelize.col('lastchangeAt')
}
}
}
]
}
}).then(function (notes) {
if (notes.length <= 0) return callback(null, notes)
var savedNotes = []
async.each(notes, function (note, _callback) {
// revision saving policy: note not been modified for 5 mins or not save for 10 mins
if (note.lastchangeAt && note.savedAt) {
var lastchangeAt = moment(note.lastchangeAt)
var savedAt = moment(note.savedAt)
if (moment().isAfter(lastchangeAt.add(5, 'minutes'))) {
savedNotes.push(note)
Revision.saveNoteRevision(note, _callback)
} else if (lastchangeAt.isAfter(savedAt.add(10, 'minutes'))) {
savedNotes.push(note)
Revision.saveNoteRevision(note, _callback)
} else {
return _callback(null, null)
}
} else {
savedNotes.push(note)
Revision.saveNoteRevision(note, _callback)
}
}, function (err) {
if (err) {
return callback(err, null)
}
// return null when no notes need saving at this moment but have delayed tasks to be done
var result = ((savedNotes.length === 0) && (notes.length > savedNotes.length)) ? null : savedNotes
return callback(null, result)
})
order: [['createdAt', 'DESC']]
}).then(function (revisions) {
if (revisions.length <= 0) {
// if no revision available
Revision.create({
noteId: note.id,
lastContent: note.content ? note.content : '',
length: note.content ? note.content.length : 0,
authorship: note.authorship
}).then(function (revision) {
Revision.finishSaveNoteRevision(note, revision, callback)
}).catch(function (err) {
return callback(err, null)
})
},
saveNoteRevision: function (note, callback) {
Revision.findAll({
where: {
noteId: note.id
},
order: [['createdAt', 'DESC']]
}).then(function (revisions) {
if (revisions.length <= 0) {
// if no revision available
Revision.create({
noteId: note.id,
lastContent: note.content ? note.content : '',
length: note.content ? note.content.length : 0,
authorship: note.authorship
} else {
var latestRevision = revisions[0]
var lastContent = latestRevision.content || latestRevision.lastContent
var content = note.content
sendDmpWorker({
msg: 'create patch',
lastDoc: lastContent,
currDoc: content
}, function (err, patch) {
if (err) logger.error('save note revision error', err)
if (!patch) {
// if patch is empty (means no difference) then just update the latest revision updated time
latestRevision.changed('updatedAt', true)
latestRevision.update({
updatedAt: Date.now()
}).then(function (revision) {
Revision.finishSaveNoteRevision(note, revision, callback)
}).catch(function (err) {
return callback(err, null)
})
} else {
var latestRevision = revisions[0]
var lastContent = latestRevision.content || latestRevision.lastContent
var content = note.content
sendDmpWorker({
msg: 'create patch',
lastDoc: lastContent,
currDoc: content
}, function (err, patch) {
if (err) logger.error('save note revision error', err)
if (!patch) {
// if patch is empty (means no difference) then just update the latest revision updated time
latestRevision.changed('updatedAt', true)
latestRevision.update({
updatedAt: Date.now()
}).then(function (revision) {
Revision.finishSaveNoteRevision(note, revision, callback)
}).catch(function (err) {
return callback(err, null)
})
} else {
Revision.create({
noteId: note.id,
patch: patch,
content: note.content,
length: note.content.length,
authorship: note.authorship
}).then(function (revision) {
// clear last revision content to reduce db size
latestRevision.update({
content: null
}).then(function () {
Revision.finishSaveNoteRevision(note, revision, callback)
}).catch(function (err) {
return callback(err, null)
})
}).catch(function (err) {
return callback(err, null)
})
}
Revision.create({
noteId: note.id,
patch: patch,
content: note.content,
length: note.content.length,
authorship: note.authorship
}).then(function (revision) {
// clear last revision content to reduce db size
latestRevision.update({
content: null
}).then(function () {
Revision.finishSaveNoteRevision(note, revision, callback)
}).catch(function (err) {
return callback(err, null)
})
}).catch(function (err) {
return callback(err, null)
})
}
}).catch(function (err) {
return callback(err, null)
})
},
finishSaveNoteRevision: function (note, revision, callback) {
note.update({
savedAt: revision.updatedAt
}).then(function () {
return callback(null, revision)
}).catch(function (err) {
return callback(err, null)
})
}
}
})
}).catch(function (err) {
return callback(err, null)
})
}
Revision.finishSaveNoteRevision = function (note, revision, callback) {
note.update({
savedAt: revision.updatedAt
}).then(function () {
return callback(null, revision)
}).catch(function (err) {
return callback(err, null)
})
}
return Revision
}

View File

@ -5,7 +5,7 @@ var scrypt = require('scrypt')
// core
var logger = require('../logger')
var {generateAvatarURL} = require('../letter-avatars')
var { generateAvatarURL } = require('../letter-avatars')
module.exports = function (sequelize, DataTypes) {
var User = sequelize.define('User', {
@ -47,111 +47,108 @@ module.exports = function (sequelize, DataTypes) {
this.setDataValue('password', hash)
}
}
}, {
instanceMethods: {
verifyPassword: function (attempt) {
if (scrypt.verifyKdfSync(Buffer.from(this.password, 'hex'), attempt)) {
return this
} else {
return false
}
}
},
classMethods: {
associate: function (models) {
User.hasMany(models.Note, {
foreignKey: 'ownerId',
constraints: false
})
User.hasMany(models.Note, {
foreignKey: 'lastchangeuserId',
constraints: false
})
},
getProfile: function (user) {
if (!user) {
return null
}
return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null)
},
parseProfile: function (profile) {
try {
profile = JSON.parse(profile)
} catch (err) {
logger.error(err)
profile = null
}
if (profile) {
profile = {
name: profile.displayName || profile.username,
photo: User.parsePhotoByProfile(profile),
biggerphoto: User.parsePhotoByProfile(profile, true)
}
}
return profile
},
parsePhotoByProfile: function (profile, bigger) {
var photo = null
switch (profile.provider) {
case 'facebook':
photo = 'https://graph.facebook.com/' + profile.id + '/picture'
if (bigger) photo += '?width=400'
else photo += '?width=96'
break
case 'twitter':
photo = 'https://twitter.com/' + profile.username + '/profile_image'
if (bigger) photo += '?size=original'
else photo += '?size=bigger'
break
case 'github':
photo = 'https://avatars.githubusercontent.com/u/' + profile.id
if (bigger) photo += '?s=400'
else photo += '?s=96'
break
case 'gitlab':
photo = profile.avatarUrl
if (photo) {
if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400')
else photo = photo.replace(/(\?s=)\d*$/i, '$196')
} else {
photo = generateAvatarURL(profile.username)
}
break
case 'mattermost':
photo = profile.avatarUrl
if (photo) {
if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400')
else photo = photo.replace(/(\?s=)\d*$/i, '$196')
} else {
photo = generateAvatarURL(profile.username)
}
break
case 'dropbox':
photo = generateAvatarURL('', profile.emails[0].value, bigger)
break
case 'google':
photo = profile.photos[0].value
if (bigger) photo = photo.replace(/(\?sz=)\d*$/i, '$1400')
else photo = photo.replace(/(\?sz=)\d*$/i, '$196')
break
case 'ldap':
photo = generateAvatarURL(profile.username, profile.emails[0], bigger)
break
case 'saml':
photo = generateAvatarURL(profile.username, profile.emails[0], bigger)
break
}
return photo
},
parseProfileByEmail: function (email) {
return {
name: email.substring(0, email.lastIndexOf('@')),
photo: generateAvatarURL('', email, false),
biggerphoto: generateAvatarURL('', email, true)
}
})
User.prototype.verifyPassword = function (attempt) {
if (scrypt.verifyKdfSync(Buffer.from(this.password, 'hex'), attempt)) {
return this
} else {
return false
}
}
User.associate = function (models) {
User.hasMany(models.Note, {
foreignKey: 'ownerId',
constraints: false
})
User.hasMany(models.Note, {
foreignKey: 'lastchangeuserId',
constraints: false
})
}
User.getProfile = function (user) {
if (!user) {
return null
}
return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null)
}
User.parseProfile = function (profile) {
try {
profile = JSON.parse(profile)
} catch (err) {
logger.error(err)
profile = null
}
if (profile) {
profile = {
name: profile.displayName || profile.username,
photo: User.parsePhotoByProfile(profile),
biggerphoto: User.parsePhotoByProfile(profile, true)
}
}
})
return profile
}
User.parsePhotoByProfile = function (profile, bigger) {
var photo = null
switch (profile.provider) {
case 'facebook':
photo = 'https://graph.facebook.com/' + profile.id + '/picture'
if (bigger) photo += '?width=400'
else photo += '?width=96'
break
case 'twitter':
photo = 'https://twitter.com/' + profile.username + '/profile_image'
if (bigger) photo += '?size=original'
else photo += '?size=bigger'
break
case 'github':
photo = 'https://avatars.githubusercontent.com/u/' + profile.id
if (bigger) photo += '?s=400'
else photo += '?s=96'
break
case 'gitlab':
photo = profile.avatarUrl
if (photo) {
if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400')
else photo = photo.replace(/(\?s=)\d*$/i, '$196')
} else {
photo = generateAvatarURL(profile.username)
}
break
case 'mattermost':
photo = profile.avatarUrl
if (photo) {
if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400')
else photo = photo.replace(/(\?s=)\d*$/i, '$196')
} else {
photo = generateAvatarURL(profile.username)
}
break
case 'dropbox':
photo = generateAvatarURL('', profile.emails[0].value, bigger)
break
case 'google':
photo = profile.photos[0].value
if (bigger) photo = photo.replace(/(\?sz=)\d*$/i, '$1400')
else photo = photo.replace(/(\?sz=)\d*$/i, '$196')
break
case 'ldap':
photo = generateAvatarURL(profile.username, profile.emails[0], bigger)
break
case 'saml':
photo = generateAvatarURL(profile.username, profile.emails[0], bigger)
break
}
return photo
}
User.parseProfileByEmail = function (email) {
return {
name: email.substring(0, email.lastIndexOf('@')),
photo: generateAvatarURL('', email, false),
biggerphoto: generateAvatarURL('', email, true)
}
}
return User
}

View File

@ -18,7 +18,7 @@ var utils = require('./utils')
// public
var response = {
errorForbidden: function (res) {
const {req} = res
const { req } = res
if (req.user) {
responseError(res, '403', 'Forbidden', 'oh no.')
} else {
@ -549,16 +549,16 @@ function gitlabActionProjects (req, res, note) {
ret.accesstoken = user.accessToken
ret.profileid = user.profileid
request(
config.gitlab.baseURL + '/api/' + config.gitlab.version + '/projects?membership=yes&per_page=100&access_token=' + user.accessToken,
function (error, httpResponse, body) {
if (!error && httpResponse.statusCode === 200) {
ret.projects = JSON.parse(body)
return res.send(ret)
} else {
return res.send(ret)
}
}
)
config.gitlab.baseURL + '/api/' + config.gitlab.version + '/projects?membership=yes&per_page=100&access_token=' + user.accessToken,
function (error, httpResponse, body) {
if (!error && httpResponse.statusCode === 200) {
ret.projects = JSON.parse(body)
return res.send(ret)
} else {
return res.send(ret)
}
}
)
}).catch(function (err) {
logger.error('gitlab action projects failed: ' + err)
return response.errorInternalError(res)

View File

@ -4,7 +4,7 @@ const Router = require('express').Router
const passport = require('passport')
const DropboxStrategy = require('passport-dropbox-oauth2').Strategy
const config = require('../../../config')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
let dropboxAuth = module.exports = Router()

View File

@ -7,8 +7,8 @@ const LocalStrategy = require('passport-local').Strategy
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const {setReturnToFromReferer} = require('../utils')
const {urlencodedParser} = require('../../utils')
const { setReturnToFromReferer } = require('../utils')
const { urlencodedParser } = require('../../utils')
const response = require('../../../response')
let emailAuth = module.exports = Router()

View File

@ -5,7 +5,7 @@ const passport = require('passport')
const FacebookStrategy = require('passport-facebook').Strategy
const config = require('../../../config')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
let facebookAuth = module.exports = Router()

View File

@ -5,7 +5,7 @@ const passport = require('passport')
const GithubStrategy = require('passport-github').Strategy
const config = require('../../../config')
const response = require('../../../response')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
let githubAuth = module.exports = Router()

View File

@ -5,7 +5,7 @@ const passport = require('passport')
const GitlabStrategy = require('passport-gitlab2').Strategy
const config = require('../../../config')
const response = require('../../../response')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
let gitlabAuth = module.exports = Router()

View File

@ -4,7 +4,7 @@ const Router = require('express').Router
const passport = require('passport')
var GoogleStrategy = require('passport-google-oauth20').Strategy
const config = require('../../../config')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
let googleAuth = module.exports = Router()
@ -12,14 +12,14 @@ passport.use(new GoogleStrategy({
clientID: config.google.clientID,
clientSecret: config.google.clientSecret,
callbackURL: config.serverURL + '/auth/google/callback',
userProfileURL: "https://www.googleapis.com/oauth2/v3/userinfo"
userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo'
}, passportGeneralCallback))
googleAuth.get('/auth/google', function (req, res, next) {
setReturnToFromReferer(req)
passport.authenticate('google', { scope: ['profile'] })(req, res, next)
})
// google auth callback
// google auth callback
googleAuth.get('/auth/google/callback',
passport.authenticate('google', {
successReturnToOrRedirect: config.serverURL + '/',

View File

@ -6,8 +6,8 @@ const LDAPStrategy = require('passport-ldapauth')
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const {setReturnToFromReferer} = require('../utils')
const {urlencodedParser} = require('../../utils')
const { setReturnToFromReferer } = require('../utils')
const { urlencodedParser } = require('../../utils')
const response = require('../../../response')
let ldapAuth = module.exports = Router()

View File

@ -5,7 +5,7 @@ const passport = require('passport')
const Mattermost = require('mattermost')
const OAuthStrategy = require('passport-oauth2').Strategy
const config = require('../../../config')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const mattermost = new Mattermost.Client()
@ -24,12 +24,12 @@ mattermostStrategy.userProfile = (accessToken, done) => {
mattermost.token = accessToken
mattermost.useHeaderToken()
mattermost.getMe(
(data) => {
done(null, data)
},
(err) => {
done(err)
}
(data) => {
done(null, data)
},
(err) => {
done(err)
}
)
}

View File

@ -4,7 +4,7 @@ const Router = require('express').Router
const passport = require('passport')
const { Strategy, InternalOAuthError } = require('passport-oauth2')
const config = require('../../../config')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
let oauth2Auth = module.exports = Router()

View File

@ -6,8 +6,8 @@ const OpenIDStrategy = require('@passport-next/passport-openid').Strategy
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const {urlencodedParser} = require('../../utils')
const {setReturnToFromReferer} = require('../utils')
const { urlencodedParser } = require('../../utils')
const { setReturnToFromReferer } = require('../utils')
let openIDAuth = module.exports = Router()

View File

@ -6,7 +6,7 @@ const SamlStrategy = require('passport-saml').Strategy
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const {urlencodedParser} = require('../../utils')
const { urlencodedParser } = require('../../utils')
const fs = require('fs')
const intersection = function (array1, array2) { return array1.filter((n) => array2.includes(n)) }

View File

@ -5,7 +5,7 @@ const passport = require('passport')
const TwitterStrategy = require('passport-twitter').Strategy
const config = require('../../../config')
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
let twitterAuth = module.exports = Router()

View File

@ -2,7 +2,7 @@
const Router = require('express').Router
const {urlencodedParser} = require('./utils')
const { urlencodedParser } = require('./utils')
const history = require('../history')
const historyRouter = module.exports = Router()

View File

@ -2,7 +2,7 @@
const config = require('../../config')
const logger = require('../../logger')
const imgur = require('imgur')
const imgur = require('@hackmd/imgur')
exports.uploadImage = function (imagePath, callback) {
if (!imagePath || typeof imagePath !== 'string') {
@ -17,12 +17,12 @@ exports.uploadImage = function (imagePath, callback) {
imgur.setClientId(config.imgur.clientID)
imgur.uploadFile(imagePath)
.then(function (json) {
if (config.debug) {
logger.info('SERVER uploadimage success: ' + JSON.stringify(json))
}
callback(null, json.data.link.replace(/^http:\/\//i, 'https://'))
}).catch(function (err) {
callback(new Error(err), null)
})
.then(function (json) {
if (config.debug) {
logger.info('SERVER uploadimage success: ' + JSON.stringify(json))
}
callback(null, json.data.link.replace(/^http:\/\//i, 'https://'))
}).catch(function (err) {
callback(new Error(err), null)
})
}

View File

@ -3,7 +3,7 @@ const fs = require('fs')
const path = require('path')
const config = require('../../config')
const {getImageMimeType} = require('../../utils')
const { getImageMimeType } = require('../../utils')
const logger = require('../../logger')
const Minio = require('minio')

View File

@ -3,7 +3,7 @@ const fs = require('fs')
const path = require('path')
const config = require('../../config')
const {getImageMimeType} = require('../../utils')
const { getImageMimeType } = require('../../utils')
const logger = require('../../logger')
const AWS = require('aws-sdk')

View File

@ -4,7 +4,7 @@ const Router = require('express').Router
const response = require('../response')
const {markdownParser} = require('./utils')
const { markdownParser } = require('./utils')
const noteRouter = module.exports = Router()

View File

@ -8,7 +8,7 @@ const config = require('../config')
const models = require('../models')
const logger = require('../logger')
const {urlencodedParser} = require('./utils')
const { urlencodedParser } = require('./utils')
const statusRouter = module.exports = Router()

View File

@ -8,7 +8,7 @@ const response = require('../response')
const config = require('../config')
const models = require('../models')
const logger = require('../logger')
const {generateAvatar} = require('../letter-avatars')
const { generateAvatar } = require('../letter-avatars')
const UserRouter = module.exports = Router()

View File

@ -1,6 +1,6 @@
'use strict'
// external modules
var DiffMatchPatch = require('diff-match-patch')
var DiffMatchPatch = require('@hackmd/diff-match-patch')
var dmp = new DiffMatchPatch()
// core

View File

@ -1,5 +1,5 @@
{
"name": "CodiMD",
"name": "codimd",
"version": "1.3.1",
"description": "Realtime collaborative markdown notes on all platforms.",
"main": "app.js",
@ -16,9 +16,13 @@
"doctoc": "doctoc --title='# Table of Contents' README.md"
},
"dependencies": {
"@hackmd/codemirror": "^5.41.2",
"@hackmd/diff-match-patch": "^1.1.1",
"@hackmd/idle-js": "^1.0.1",
"@hackmd/imgur": "^0.4.1",
"@hackmd/js-sequence-diagrams": "^0.0.1-alpha.2",
"@hackmd/lz-string": "1.4.4",
"@passport-next/passport-openid": "^1.0.0",
"Idle.Js": "git+https://github.com/shawnmclean/Idle.js",
"archiver": "^2.1.1",
"async": "^2.1.4",
"aws-sdk": "^2.345.0",
@ -29,14 +33,12 @@
"bootstrap-validator": "^0.11.8",
"chance": "^1.0.4",
"cheerio": "^0.22.0",
"codemirror": "git+https://github.com/hackmdio/CodeMirror.git",
"compression": "^1.6.2",
"connect-flash": "^0.1.1",
"connect-session-sequelize": "^4.1.0",
"connect-session-sequelize": "^6.0.0",
"cookie": "0.3.1",
"cookie-parser": "1.4.3",
"deep-freeze": "^0.0.1",
"diff-match-patch": "git+https://github.com/hackmdio/diff-match-patch.git",
"ejs": "^2.5.5",
"emojify.js": "~1.1.0",
"express": ">=4.14",
@ -51,19 +53,16 @@
"helmet": "^3.13.0",
"highlight.js": "~9.12.0",
"i18n": "^0.8.3",
"imgur": "git+https://github.com/hackmdio/node-imgur.git",
"ionicons": "~2.0.1",
"jquery": "^3.1.1",
"jquery-mousewheel": "^3.1.13",
"jquery-ui": "^1.12.1",
"js-cookie": "^2.1.3",
"js-url": "^2.3.0",
"js-yaml": "^3.7.0",
"jsdom-nogyp": "^0.8.3",
"keymaster": "^1.6.2",
"list.js": "^1.5.0",
"lodash": "^4.17.11",
"lz-string": "git+https://github.com/hackmdio/lz-string.git",
"markdown-it": "^8.2.2",
"markdown-it-abbr": "^1.0.4",
"markdown-it-container": "^2.0.0",
@ -105,21 +104,19 @@
"pg-hstore": "^2.3.2",
"prismjs": "^1.6.0",
"randomcolor": "^0.5.3",
"raphael": "git+https://github.com/dmitrybaranovskiy/raphael",
"raphael": "^2.2.8",
"readline-sync": "^1.4.7",
"request": "^2.88.0",
"reveal.js": "~3.7.0",
"scrypt": "^6.0.3",
"select2": "^3.5.2-browserify",
"sequelize": "^3.28.0",
"sequelize-cli": "^2.5.1",
"sequelize": "5.3.2",
"shortid": "2.2.8",
"socket.io": "~2.1.1",
"socket.io-client": "~2.1.1",
"spin.js": "^2.3.2",
"sqlite3": "^4.0.1",
"store": "^2.0.12",
"string": "^3.3.3",
"tedious": "^1.14.0",
"toobusy-js": "^0.5.1",
"turndown": "^5.0.1",
@ -130,6 +127,7 @@
"viz.js": "^1.7.0",
"winston": "^3.1.0",
"ws": "^6.0.0",
"wurl": "^2.5.3",
"xss": "^1.0.3"
},
"resolutions": {
@ -138,7 +136,7 @@
"**/request": "^2.88.0"
},
"engines": {
"node": ">=6.x"
"node": ">=8.0.0"
},
"bugs": "https://github.com/hackmdio/codimd/issues",
"keywords": [
@ -185,13 +183,14 @@
"html-webpack-plugin": "4.0.0-beta.2",
"imports-loader": "^0.8.0",
"jsonlint": "^1.6.2",
"less": "^2.7.1",
"less": "^3.9.0",
"less-loader": "^4.1.0",
"mini-css-extract-plugin": "^0.4.1",
"mocha": "^5.2.0",
"mock-require": "^3.0.3",
"optimize-css-assets-webpack-plugin": "^5.0.0",
"script-loader": "^0.7.2",
"sequelize-cli": "^5.4.0",
"string-loader": "^0.0.1",
"style-loader": "^0.21.0",
"uglifyjs-webpack-plugin": "^1.2.7",

View File

@ -12,7 +12,6 @@ module.exports = {
"ui": false,
"Spinner": false,
"modeType": false,
"Idle": false,
"serverurl": false,
"key": false,
"gapi": false,

View File

@ -1,37 +1,37 @@
/* eslint-env browser, jquery */
/* global moment, serverurl */
import {
checkIfAuth,
clearLoginState,
getLoginState,
resetCheckAuth,
setloginStateChangeEvent
} from './lib/common/login'
import {
clearDuplicatedHistory,
deleteServerHistory,
getHistory,
getStorageHistory,
parseHistory,
parseServerToHistory,
parseStorageToHistory,
postHistoryToServer,
removeHistory,
saveHistory,
saveStorageHistoryToServer
} from './history'
import { saveAs } from 'file-saver'
import List from 'list.js'
import unescapeHTML from 'lodash/unescape'
require('./locale')
require('../css/cover.css')
require('../css/site.css')
import {
checkIfAuth,
clearLoginState,
getLoginState,
resetCheckAuth,
setloginStateChangeEvent
} from './lib/common/login'
import {
clearDuplicatedHistory,
deleteServerHistory,
getHistory,
getStorageHistory,
parseHistory,
parseServerToHistory,
parseStorageToHistory,
postHistoryToServer,
removeHistory,
saveHistory,
saveStorageHistoryToServer
} from './history'
import { saveAs } from 'file-saver'
import List from 'list.js'
import S from 'string'
const options = {
valueNames: ['id', 'text', 'timestamp', 'fromNow', 'time', 'tags', 'pinned'],
item: `<li class="col-xs-12 col-sm-6 col-md-6 col-lg-4">
@ -67,27 +67,27 @@ pageInit()
function pageInit () {
checkIfAuth(
data => {
$('.ui-signin').hide()
$('.ui-or').hide()
$('.ui-welcome').show()
if (data.photo) $('.ui-avatar').prop('src', data.photo).show()
else $('.ui-avatar').prop('src', '').hide()
$('.ui-name').html(data.name)
$('.ui-signout').show()
$('.ui-history').click()
parseServerToHistory(historyList, parseHistoryCallback)
},
() => {
$('.ui-signin').show()
$('.ui-or').show()
$('.ui-welcome').hide()
$('.ui-avatar').prop('src', '').hide()
$('.ui-name').html('')
$('.ui-signout').hide()
parseStorageToHistory(historyList, parseHistoryCallback)
}
)
data => {
$('.ui-signin').hide()
$('.ui-or').hide()
$('.ui-welcome').show()
if (data.photo) $('.ui-avatar').prop('src', data.photo).show()
else $('.ui-avatar').prop('src', '').hide()
$('.ui-name').html(data.name)
$('.ui-signout').show()
$('.ui-history').click()
parseServerToHistory(historyList, parseHistoryCallback)
},
() => {
$('.ui-signin').show()
$('.ui-or').show()
$('.ui-welcome').hide()
$('.ui-avatar').prop('src', '').hide()
$('.ui-name').html('')
$('.ui-signout').hide()
parseStorageToHistory(historyList, parseHistoryCallback)
}
)
}
$('.masthead-nav li').click(function () {
@ -132,7 +132,7 @@ function checkHistoryList () {
function parseHistoryCallback (list, notehistory) {
checkHistoryList()
// sort by pinned then timestamp
// sort by pinned then timestamp
list.sort('', {
sortFunction (a, b) {
const notea = a.values()
@ -152,13 +152,13 @@ function parseHistoryCallback (list, notehistory) {
}
}
})
// parse filter tags
// parse filter tags
const filtertags = []
for (let i = 0, l = list.items.length; i < l; i++) {
const tags = list.items[i]._values.tags
if (tags && tags.length > 0) {
for (let j = 0; j < tags.length; j++) {
// push info filtertags if not found
// push info filtertags if not found
let found = false
if (filtertags.includes(tags[j])) { found = true }
if (!found) { filtertags.push(tags[j]) }
@ -178,20 +178,20 @@ historyList.on('updated', e => {
const a = itemEl.find('a')
const pin = itemEl.find('.ui-history-pin')
const tagsEl = itemEl.find('.tags')
// parse link to element a
// parse link to element a
a.attr('href', `${serverurl}/${values.id}`)
// parse pinned
// parse pinned
if (values.pinned) {
pin.addClass('active')
} else {
pin.removeClass('active')
}
// parse tags
// parse tags
const tags = values.tags
if (tags && tags.length > 0 && tagsEl.children().length <= 0) {
const labels = []
for (let j = 0; j < tags.length; j++) {
// push into the item label
// push into the item label
labels.push(`<span class='label label-default'>${tags[j]}</span>`)
}
tagsEl.html(labels.join(' '))
@ -328,7 +328,7 @@ $('.ui-open-history').bind('change', e => {
const reader = new FileReader()
reader.onload = () => {
const notehistory = JSON.parse(reader.result)
// console.log(notehistory);
// console.log(notehistory);
if (!reader.result) return
getHistory(data => {
let mergedata = data.concat(notehistory)
@ -397,7 +397,7 @@ function buildTagsFilter (tags) {
for (let i = 0; i < tags.length; i++) {
tags[i] = {
id: i,
text: S(tags[i]).unescapeHTML().s
text: unescapeHTML(tags[i])
}
}
filtertags = tags

View File

@ -1,6 +1,24 @@
/* eslint-env browser, jquery */
/* global moment, serverurl */
import Prism from 'prismjs'
import hljs from 'highlight.js'
import PDFObject from 'pdfobject'
import { saveAs } from 'file-saver'
import escapeHTML from 'lodash/escape'
import unescapeHTML from 'lodash/unescape'
import { stripTags } from '../../utils/string'
import getUIElements from './lib/editor/ui-elements'
import markdownit from 'markdown-it'
import markdownitContainer from 'markdown-it-container'
/* Defined regex markdown it plugins */
import Plugin from 'markdown-it-regexp'
require('prismjs/themes/prism.css')
require('prismjs/components/prism-wiki')
require('prismjs/components/prism-haskell')
@ -10,17 +28,9 @@ require('prismjs/components/prism-jsx')
require('prismjs/components/prism-makefile')
require('prismjs/components/prism-gherkin')
import Prism from 'prismjs'
import hljs from 'highlight.js'
import PDFObject from 'pdfobject'
import S from 'string'
import { saveAs } from 'file-saver'
require('./lib/common/login')
require('../vendor/md-toc')
var Viz = require('viz.js')
import getUIElements from './lib/editor/ui-elements'
const ui = getUIElements()
// auto update last change
@ -157,7 +167,7 @@ export function renderTags (view) {
function slugifyWithUTF8 (text) {
// remove html tags and trim spaces
let newText = S(text).trim().stripTags().s
let newText = stripTags(text.toString().trim())
// replace all spaces in between to dashes
newText = newText.replace(/\s+/g, '-')
// slugify string to make it valid for attribute
@ -190,7 +200,7 @@ export function parseMeta (md, edit, view, toc, tocAffix) {
dir = meta.dir
breaks = meta.breaks
}
// text language
// text language
if (lang && typeof lang === 'string') {
view.attr('lang', lang)
toc.attr('lang', lang)
@ -202,7 +212,7 @@ export function parseMeta (md, edit, view, toc, tocAffix) {
tocAffix.removeAttr('lang')
if (edit) { edit.removeAttr('lang', lang) }
}
// text direction
// text direction
if (dir && typeof dir === 'string') {
view.attr('dir', dir)
toc.attr('dir', dir)
@ -212,7 +222,7 @@ export function parseMeta (md, edit, view, toc, tocAffix) {
toc.removeAttr('dir')
tocAffix.removeAttr('dir')
}
// breaks
// breaks
if (typeof breaks === 'boolean' && !breaks) {
md.options.breaks = false
} else {
@ -245,7 +255,7 @@ if (typeof window.mermaid !== 'undefined' && window.mermaid) window.mermaid.star
// dynamic event or object binding here
export function finishView (view) {
// todo list
// todo list
const lis = view.find('li.raw').removeClass('raw').sortByDepth().toArray()
for (let li of lis) {
@ -259,9 +269,9 @@ export function finishView (view) {
li.innerHTML = html
let disabled = 'disabled'
if (typeof editor !== 'undefined' && window.havePermission()) { disabled = '' }
if (/^\s*\[[x ]\]\s*/.test(html)) {
li.innerHTML = html.replace(/^\s*\[ \]\s*/, `<input type="checkbox" class="task-list-item-checkbox "${disabled}><label></label>`)
.replace(/^\s*\[x\]\s*/, `<input type="checkbox" class="task-list-item-checkbox" checked ${disabled}><label></label>`)
if (/^\s*\[[x ]]\s*/.test(html)) {
li.innerHTML = html.replace(/^\s*\[ ]\s*/, `<input type="checkbox" class="task-list-item-checkbox "${disabled}><label></label>`)
.replace(/^\s*\[x]\s*/, `<input type="checkbox" class="task-list-item-checkbox" checked ${disabled}><label></label>`)
if (li.tagName.toLowerCase() !== 'li') {
li.parentElement.setAttribute('class', 'task-list-item')
} else {
@ -269,42 +279,42 @@ export function finishView (view) {
}
}
if (typeof editor !== 'undefined' && window.havePermission()) { $(li).find('input').change(toggleTodoEvent) }
// color tag in list will convert it to tag icon with color
// color tag in list will convert it to tag icon with color
const tagColor = $(li).closest('ul').find('.color')
tagColor.each((key, value) => {
$(value).addClass('fa fa-tag').css('color', $(value).attr('data-color'))
})
}
// youtube
// youtube
view.find('div.youtube.raw').removeClass('raw')
.click(function () {
imgPlayiframe(this, '//www.youtube.com/embed/')
})
.click(function () {
imgPlayiframe(this, '//www.youtube.com/embed/')
})
// vimeo
view.find('div.vimeo.raw').removeClass('raw')
.click(function () {
imgPlayiframe(this, '//player.vimeo.com/video/')
})
.each((key, value) => {
$.ajax({
type: 'GET',
url: `//vimeo.com/api/v2/video/${$(value).attr('data-videoid')}.json`,
jsonp: 'callback',
dataType: 'jsonp',
success (data) {
const thumbnailSrc = data[0].thumbnail_large
const image = `<img src="${thumbnailSrc}" />`
$(value).prepend(image)
if (window.viewAjaxCallback) window.viewAjaxCallback()
}
})
})
.click(function () {
imgPlayiframe(this, '//player.vimeo.com/video/')
})
.each((key, value) => {
$.ajax({
type: 'GET',
url: `//vimeo.com/api/v2/video/${$(value).attr('data-videoid')}.json`,
jsonp: 'callback',
dataType: 'jsonp',
success (data) {
const thumbnailSrc = data[0].thumbnail_large
const image = `<img src="${thumbnailSrc}" />`
$(value).prepend(image)
if (window.viewAjaxCallback) window.viewAjaxCallback()
}
})
})
// gist
view.find('code[data-gist-id]').each((key, value) => {
if ($(value).children().length === 0) { $(value).gist(window.viewAjaxCallback) }
})
// sequence diagram
// sequence diagram
const sequences = view.find('div.sequence-diagram.raw').removeClass('raw')
sequences.each((key, value) => {
try {
@ -327,7 +337,7 @@ export function finishView (view) {
console.warn(err)
}
})
// flowchart
// flowchart
const flow = view.find('div.flow-chart.raw').removeClass('raw')
flow.each((key, value) => {
try {
@ -351,7 +361,7 @@ export function finishView (view) {
console.warn(err)
}
})
// graphviz
// graphviz
var graphvizs = view.find('div.graphviz.raw').removeClass('raw')
graphvizs.each(function (key, value) {
try {
@ -370,7 +380,7 @@ export function finishView (view) {
console.warn(err)
}
})
// mermaid
// mermaid
const mermaids = view.find('div.mermaid.raw').removeClass('raw')
mermaids.each((key, value) => {
try {
@ -412,16 +422,16 @@ export function finishView (view) {
console.warn(err)
}
})
// image href new window(emoji not included)
// image href new window(emoji not included)
const images = view.find('img.raw[src]').removeClass('raw')
images.each((key, value) => {
// if it's already wrapped by link, then ignore
// if it's already wrapped by link, then ignore
const $value = $(value)
$value[0].onload = e => {
if (window.viewAjaxCallback) window.viewAjaxCallback()
}
})
// blockquote
// blockquote
const blockquote = view.find('blockquote.raw').removeClass('raw')
const blockquoteP = blockquote.find('p')
blockquoteP.each((key, value) => {
@ -429,96 +439,96 @@ export function finishView (view) {
html = replaceExtraTags(html)
$(value).html(html)
})
// color tag in blockquote will change its left border color
// color tag in blockquote will change its left border color
const blockquoteColor = blockquote.find('.color')
blockquoteColor.each((key, value) => {
$(value).closest('blockquote').css('border-left-color', $(value).attr('data-color'))
})
// slideshare
// slideshare
view.find('div.slideshare.raw').removeClass('raw')
.each((key, value) => {
$.ajax({
type: 'GET',
url: `//www.slideshare.net/api/oembed/2?url=http://www.slideshare.net/${$(value).attr('data-slideshareid')}&format=json`,
jsonp: 'callback',
dataType: 'jsonp',
success (data) {
const $html = $(data.html)
const iframe = $html.closest('iframe')
const caption = $html.closest('div')
const inner = $('<div class="inner"></div>').append(iframe)
const height = iframe.attr('height')
const width = iframe.attr('width')
const ratio = (height / width) * 100
inner.css('padding-bottom', `${ratio}%`)
$(value).html(inner).append(caption)
if (window.viewAjaxCallback) window.viewAjaxCallback()
}
})
})
.each((key, value) => {
$.ajax({
type: 'GET',
url: `//www.slideshare.net/api/oembed/2?url=http://www.slideshare.net/${$(value).attr('data-slideshareid')}&format=json`,
jsonp: 'callback',
dataType: 'jsonp',
success (data) {
const $html = $(data.html)
const iframe = $html.closest('iframe')
const caption = $html.closest('div')
const inner = $('<div class="inner"></div>').append(iframe)
const height = iframe.attr('height')
const width = iframe.attr('width')
const ratio = (height / width) * 100
inner.css('padding-bottom', `${ratio}%`)
$(value).html(inner).append(caption)
if (window.viewAjaxCallback) window.viewAjaxCallback()
}
})
})
// speakerdeck
view.find('div.speakerdeck.raw').removeClass('raw')
.each((key, value) => {
const url = `https://speakerdeck.com/${$(value).attr('data-speakerdeckid')}`
const inner = $('<a>Speakerdeck</a>')
inner.attr('href', url)
inner.attr('rel', 'noopener noreferrer')
inner.attr('target', '_blank')
$(value).append(inner)
})
.each((key, value) => {
const url = `https://speakerdeck.com/${$(value).attr('data-speakerdeckid')}`
const inner = $('<a>Speakerdeck</a>')
inner.attr('href', url)
inner.attr('rel', 'noopener noreferrer')
inner.attr('target', '_blank')
$(value).append(inner)
})
// pdf
view.find('div.pdf.raw').removeClass('raw')
.each(function (key, value) {
const url = $(value).attr('data-pdfurl')
const inner = $('<div></div>')
$(this).append(inner)
PDFObject.embed(url, inner, {
height: '400px'
})
})
.each(function (key, value) {
const url = $(value).attr('data-pdfurl')
const inner = $('<div></div>')
$(this).append(inner)
PDFObject.embed(url, inner, {
height: '400px'
})
})
// syntax highlighting
view.find('code.raw').removeClass('raw')
.each((key, value) => {
const langDiv = $(value)
if (langDiv.length > 0) {
const reallang = langDiv[0].className.replace(/hljs|wrap/g, '').trim()
const codeDiv = langDiv.find('.code')
let code = ''
if (codeDiv.length > 0) code = codeDiv.html()
else code = langDiv.html()
var result
if (!reallang) {
result = {
value: code
}
} else if (reallang === 'haskell' || reallang === 'go' || reallang === 'typescript' || reallang === 'jsx' || reallang === 'gherkin') {
code = S(code).unescapeHTML().s
result = {
value: Prism.highlight(code, Prism.languages[reallang])
}
} else if (reallang === 'tiddlywiki' || reallang === 'mediawiki') {
code = S(code).unescapeHTML().s
result = {
value: Prism.highlight(code, Prism.languages.wiki)
}
} else if (reallang === 'cmake') {
code = S(code).unescapeHTML().s
result = {
value: Prism.highlight(code, Prism.languages.makefile)
}
} else {
code = S(code).unescapeHTML().s
const languages = hljs.listLanguages()
if (!languages.includes(reallang)) {
result = hljs.highlightAuto(code)
} else {
result = hljs.highlight(reallang, code)
}
}
if (codeDiv.length > 0) codeDiv.html(result.value)
else langDiv.html(result.value)
.each((key, value) => {
const langDiv = $(value)
if (langDiv.length > 0) {
const reallang = langDiv[0].className.replace(/hljs|wrap/g, '').trim()
const codeDiv = langDiv.find('.code')
let code = ''
if (codeDiv.length > 0) code = codeDiv.html()
else code = langDiv.html()
var result
if (!reallang) {
result = {
value: code
}
})
} else if (reallang === 'haskell' || reallang === 'go' || reallang === 'typescript' || reallang === 'jsx' || reallang === 'gherkin') {
code = unescapeHTML(code)
result = {
value: Prism.highlight(code, Prism.languages[reallang])
}
} else if (reallang === 'tiddlywiki' || reallang === 'mediawiki') {
code = unescapeHTML(code)
result = {
value: Prism.highlight(code, Prism.languages.wiki)
}
} else if (reallang === 'cmake') {
code = unescapeHTML(code)
result = {
value: Prism.highlight(code, Prism.languages.makefile)
}
} else {
code = unescapeHTML(code)
const languages = hljs.listLanguages()
if (!languages.includes(reallang)) {
result = hljs.highlightAuto(code)
} else {
result = hljs.highlight(reallang, code)
}
}
if (codeDiv.length > 0) codeDiv.html(result.value)
else langDiv.html(result.value)
}
})
// mathjax
const mathjaxdivs = view.find('span.mathjax.raw').removeClass('raw').toArray()
try {
@ -532,7 +542,7 @@ export function finishView (view) {
} catch (err) {
console.warn(err)
}
// render title
// render title
document.title = renderTitle(view)
}
@ -592,23 +602,23 @@ window.removeDOMEvents = removeDOMEvents
function generateCleanHTML (view) {
const src = view.clone()
const eles = src.find('*')
// remove syncscroll parts
// remove syncscroll parts
eles.removeClass('part')
src.find('*[class=""]').removeAttr('class')
eles.removeAttr('data-startline data-endline')
src.find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll')
// remove gist content
// remove gist content
src.find('code[data-gist-id]').children().remove()
// disable todo list
// disable todo list
src.find('input.task-list-item-checkbox').attr('disabled', '')
// replace emoji image path
// replace emoji image path
src.find('img.emoji').each((key, value) => {
let name = $(value).attr('alt')
name = name.substr(1)
name = name.slice(0, name.length - 1)
$(value).attr('src', `https://cdnjs.cloudflare.com/ajax/libs/emojify.js/1.1.0/images/basic/${name}.png`)
})
// replace video to iframe
// replace video to iframe
src.find('div[data-videoid]').each((key, value) => {
const id = $(value).attr('data-videoid')
const style = $(value).attr('style')
@ -644,12 +654,12 @@ export function exportToHTML (view) {
const title = renderTitle(ui.area.markdown)
const filename = `${renderFilename(ui.area.markdown)}.html`
const src = generateCleanHTML(view)
// generate toc
// generate toc
const toc = $('#ui-toc').clone()
toc.find('*').removeClass('active').find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll')
const tocAffix = $('#ui-toc-affix').clone()
tocAffix.find('*').removeClass('active').find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll')
// generate html via template
// generate html via template
$.get(`${serverurl}/build/html.min.css`, css => {
$.get(`${serverurl}/views/html.hbs`, data => {
const template = window.Handlebars.compile(data)
@ -664,7 +674,7 @@ export function exportToHTML (view) {
dir: (md && md.meta && md.meta.dir) ? `dir="${md.meta.dir}"` : null
}
const html = template(context)
// console.log(html);
// console.log(html);
const blob = new Blob([html], {
type: 'text/html;charset=utf-8'
})
@ -779,20 +789,20 @@ export function smoothHashScroll () {
const hash = element.hash
if (hash) {
$element.on('click', function (e) {
// store hash
// store hash
const hash = decodeURIComponent(this.hash)
// escape special characters in jquery selector
// escape special characters in jquery selector
const $hash = $(hash.replace(/(:|\.|\[|\]|,)/g, '\\$1'))
// return if no element been selected
// return if no element been selected
if ($hash.length <= 0) return
// prevent default anchor click behavior
// prevent default anchor click behavior
e.preventDefault()
// animate
// animate
$('body, html').stop(true, true).animate({
scrollTop: $hash.offset().top
}, 100, 'linear', () => {
// when done, add hash to url
// (default click behaviour)
// when done, add hash to url
// (default click behaviour)
window.location.hash = hash
})
})
@ -902,7 +912,7 @@ export function scrollToHash () {
function highlightRender (code, lang) {
if (!lang || /no(-?)highlight|plain|text/.test(lang)) { return }
code = S(code).escapeHTML().s
code = escapeHTML(code)
if (lang === 'sequence') {
return `<div class="sequence-diagram raw">${code}</div>`
} else if (lang === 'flow') {
@ -934,9 +944,6 @@ function highlightRender (code, lang) {
return result.value
}
import markdownit from 'markdown-it'
import markdownitContainer from 'markdown-it-container'
export let md = markdownit('default', {
html: true,
breaks: true,
@ -1034,109 +1041,106 @@ md.renderer.rules.fence = (tokens, idx, options, env, self) => {
return `<pre><code${self.renderAttrs(token)}>${highlighted}</code></pre>\n`
}
/* Defined regex markdown it plugins */
import Plugin from 'markdown-it-regexp'
// youtube
const youtubePlugin = new Plugin(
// regexp to match
/{%youtube\s*([\d\D]*?)\s*%}/,
// regexp to match
/{%youtube\s*([\d\D]*?)\s*%}/,
(match, utils) => {
const videoid = match[1]
if (!videoid) return
const div = $('<div class="youtube raw"></div>')
div.attr('data-videoid', videoid)
const thumbnailSrc = `//img.youtube.com/vi/${videoid}/hqdefault.jpg`
const image = `<img src="${thumbnailSrc}" />`
div.append(image)
const icon = '<i class="icon fa fa-youtube-play fa-5x"></i>'
div.append(icon)
return div[0].outerHTML
}
(match, utils) => {
const videoid = match[1]
if (!videoid) return
const div = $('<div class="youtube raw"></div>')
div.attr('data-videoid', videoid)
const thumbnailSrc = `//img.youtube.com/vi/${videoid}/hqdefault.jpg`
const image = `<img src="${thumbnailSrc}" />`
div.append(image)
const icon = '<i class="icon fa fa-youtube-play fa-5x"></i>'
div.append(icon)
return div[0].outerHTML
}
)
// vimeo
const vimeoPlugin = new Plugin(
// regexp to match
/{%vimeo\s*([\d\D]*?)\s*%}/,
// regexp to match
/{%vimeo\s*([\d\D]*?)\s*%}/,
(match, utils) => {
const videoid = match[1]
if (!videoid) return
const div = $('<div class="vimeo raw"></div>')
div.attr('data-videoid', videoid)
const icon = '<i class="icon fa fa-vimeo-square fa-5x"></i>'
div.append(icon)
return div[0].outerHTML
}
(match, utils) => {
const videoid = match[1]
if (!videoid) return
const div = $('<div class="vimeo raw"></div>')
div.attr('data-videoid', videoid)
const icon = '<i class="icon fa fa-vimeo-square fa-5x"></i>'
div.append(icon)
return div[0].outerHTML
}
)
// gist
const gistPlugin = new Plugin(
// regexp to match
/{%gist\s*([\d\D]*?)\s*%}/,
// regexp to match
/{%gist\s*([\d\D]*?)\s*%}/,
(match, utils) => {
const gistid = match[1]
const code = `<code data-gist-id="${gistid}"></code>`
return code
}
(match, utils) => {
const gistid = match[1]
const code = `<code data-gist-id="${gistid}"></code>`
return code
}
)
// TOC
const tocPlugin = new Plugin(
// regexp to match
/^\[TOC\]$/i,
// regexp to match
/^\[TOC\]$/i,
(match, utils) => '<div class="toc"></div>'
(match, utils) => '<div class="toc"></div>'
)
// slideshare
const slidesharePlugin = new Plugin(
// regexp to match
/{%slideshare\s*([\d\D]*?)\s*%}/,
// regexp to match
/{%slideshare\s*([\d\D]*?)\s*%}/,
(match, utils) => {
const slideshareid = match[1]
const div = $('<div class="slideshare raw"></div>')
div.attr('data-slideshareid', slideshareid)
return div[0].outerHTML
}
(match, utils) => {
const slideshareid = match[1]
const div = $('<div class="slideshare raw"></div>')
div.attr('data-slideshareid', slideshareid)
return div[0].outerHTML
}
)
// speakerdeck
const speakerdeckPlugin = new Plugin(
// regexp to match
/{%speakerdeck\s*([\d\D]*?)\s*%}/,
// regexp to match
/{%speakerdeck\s*([\d\D]*?)\s*%}/,
(match, utils) => {
const speakerdeckid = match[1]
const div = $('<div class="speakerdeck raw"></div>')
div.attr('data-speakerdeckid', speakerdeckid)
return div[0].outerHTML
}
(match, utils) => {
const speakerdeckid = match[1]
const div = $('<div class="speakerdeck raw"></div>')
div.attr('data-speakerdeckid', speakerdeckid)
return div[0].outerHTML
}
)
// pdf
const pdfPlugin = new Plugin(
// regexp to match
/{%pdf\s*([\d\D]*?)\s*%}/,
// regexp to match
/{%pdf\s*([\d\D]*?)\s*%}/,
(match, utils) => {
const pdfurl = match[1]
if (!isValidURL(pdfurl)) return match[0]
const div = $('<div class="pdf raw"></div>')
div.attr('data-pdfurl', pdfurl)
return div[0].outerHTML
}
(match, utils) => {
const pdfurl = match[1]
if (!isValidURL(pdfurl)) return match[0]
const div = $('<div class="pdf raw"></div>')
div.attr('data-pdfurl', pdfurl)
return div[0].outerHTML
}
)
const emojijsPlugin = new Plugin(
// regexp to match emoji shortcodes :something:
// We generate an universal regex that guaranteed only contains the
// emojies we have available. This should prevent all false-positives
new RegExp(':(' + window.emojify.emojiNames.map((item) => { return RegExp.escape(item) }).join('|') + '):', 'i'),
// regexp to match emoji shortcodes :something:
// We generate an universal regex that guaranteed only contains the
// emojies we have available. This should prevent all false-positives
new RegExp(':(' + window.emojify.emojiNames.map((item) => { return RegExp.escape(item) }).join('|') + '):', 'i'),
(match, utils) => {
const emoji = match[1].toLowerCase()
const div = $(`<img class="emoji" alt=":${emoji}:" src="${serverurl}/build/emojify.js/dist/images/basic/${emoji}.png"></img>`)
return div[0].outerHTML
}
(match, utils) => {
const emoji = match[1].toLowerCase()
const div = $(`<img class="emoji" alt=":${emoji}:" src="${serverurl}/build/emojify.js/dist/images/basic/${emoji}.png"></img>`)
return div[0].outerHTML
}
)
// yaml meta, from https://github.com/eugeneware/remarkable-meta

View File

@ -2,65 +2,64 @@
/* global serverurl, moment */
import store from 'store'
import S from 'string'
import LZString from 'lz-string'
import LZString from '@hackmd/lz-string'
import escapeHTML from 'lodash/escape'
import wurl from 'wurl'
import {
checkNoteIdValid,
encodeNoteId
} from './utils'
import {
checkIfAuth
} from './lib/common/login'
import { checkIfAuth } from './lib/common/login'
import {
urlpath
} from './lib/config'
import { urlpath } from './lib/config'
window.migrateHistoryFromTempCallback = null
migrateHistoryFromTemp()
function migrateHistoryFromTemp () {
if (window.url('#tempid')) {
if (wurl('#tempid')) {
$.get(`${serverurl}/temp`, {
tempid: window.url('#tempid')
tempid: wurl('#tempid')
})
.done(data => {
if (data && data.temp) {
getStorageHistory(olddata => {
if (!olddata || olddata.length === 0) {
saveHistoryToStorage(JSON.parse(data.temp))
}
})
}
})
.always(() => {
let hash = location.hash.split('#')[1]
hash = hash.split('&')
for (let i = 0; i < hash.length; i++) {
if (hash[i].indexOf('tempid') === 0) {
hash.splice(i, 1)
i--
.done(data => {
if (data && data.temp) {
getStorageHistory(olddata => {
if (!olddata || olddata.length === 0) {
saveHistoryToStorage(JSON.parse(data.temp))
}
})
}
}
hash = hash.join('&')
location.hash = hash
if (window.migrateHistoryFromTempCallback) { window.migrateHistoryFromTempCallback() }
})
})
.always(() => {
let hash = location.hash.split('#')[1]
hash = hash.split('&')
for (let i = 0; i < hash.length; i++) {
if (hash[i].indexOf('tempid') === 0) {
hash.splice(i, 1)
i--
}
}
hash = hash.join('&')
location.hash = hash
if (window.migrateHistoryFromTempCallback) { window.migrateHistoryFromTempCallback() }
})
}
}
export function saveHistory (notehistory) {
checkIfAuth(
() => {
saveHistoryToServer(notehistory)
},
() => {
saveHistoryToStorage(notehistory)
}
)
() => {
saveHistoryToServer(notehistory)
},
() => {
saveHistoryToStorage(notehistory)
}
)
}
function saveHistoryToStorage (notehistory) {
@ -79,9 +78,9 @@ export function saveStorageHistoryToServer (callback) {
$.post(`${serverurl}/history`, {
history: data
})
.done(data => {
callback(data)
})
.done(data => {
callback(data)
})
}
}
@ -108,7 +107,7 @@ export function clearDuplicatedHistory (notehistory) {
}
function addHistory (id, text, time, tags, pinned, notehistory) {
// only add when note id exists
// only add when note id exists
if (id) {
notehistory.push({
id,
@ -134,14 +133,14 @@ export function removeHistory (id, notehistory) {
// used for inner
export function writeHistory (title, tags) {
checkIfAuth(
() => {
// no need to do this anymore, this will count from server-side
// writeHistoryToServer(title, tags);
},
() => {
writeHistoryToStorage(title, tags)
}
)
() => {
// no need to do this anymore, this will count from server-side
// writeHistoryToServer(title, tags);
},
() => {
writeHistoryToStorage(title, tags)
}
)
}
function writeHistoryToStorage (title, tags) {
@ -162,7 +161,7 @@ if (!Array.isArray) {
}
function renderHistory (title, tags) {
// console.debug(tags);
// console.debug(tags);
const id = urlpath ? location.pathname.slice(urlpath.length + 1, location.pathname.length).split('/')[1] : location.pathname.split('/')[1]
return {
id,
@ -174,7 +173,7 @@ function renderHistory (title, tags) {
function generateHistory (title, tags, notehistory) {
const info = renderHistory(title, tags)
// keep any pinned data
// keep any pinned data
let pinned = false
for (let i = 0; i < notehistory.length; i++) {
if (notehistory[i].id === info.id && notehistory[i].pinned) {
@ -191,25 +190,25 @@ function generateHistory (title, tags, notehistory) {
// used for outer
export function getHistory (callback) {
checkIfAuth(
() => {
getServerHistory(callback)
},
() => {
getStorageHistory(callback)
}
)
() => {
getServerHistory(callback)
},
() => {
getStorageHistory(callback)
}
)
}
function getServerHistory (callback) {
$.get(`${serverurl}/history`)
.done(data => {
if (data.history) {
callback(data.history)
}
})
.fail((xhr, status, error) => {
console.error(xhr.responseText)
})
.done(data => {
if (data.history) {
callback(data.history)
}
})
.fail((xhr, status, error) => {
console.error(xhr.responseText)
})
}
export function getStorageHistory (callback) {
@ -224,25 +223,25 @@ export function getStorageHistory (callback) {
export function parseHistory (list, callback) {
checkIfAuth(
() => {
parseServerToHistory(list, callback)
},
() => {
parseStorageToHistory(list, callback)
}
)
() => {
parseServerToHistory(list, callback)
},
() => {
parseStorageToHistory(list, callback)
}
)
}
export function parseServerToHistory (list, callback) {
$.get(`${serverurl}/history`)
.done(data => {
if (data.history) {
parseToHistory(list, data.history, callback)
}
})
.fail((xhr, status, error) => {
console.error(xhr.responseText)
})
.done(data => {
if (data.history) {
parseToHistory(list, data.history, callback)
}
})
.fail((xhr, status, error) => {
console.error(xhr.responseText)
})
}
export function parseStorageToHistory (list, callback) {
@ -268,15 +267,15 @@ function parseToHistory (list, notehistory, callback) {
} catch (err) {
console.error(err)
}
// parse time to timestamp and fromNow
// parse time to timestamp and fromNow
const timestamp = (typeof notehistory[i].time === 'number' ? moment(notehistory[i].time) : moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a'))
notehistory[i].timestamp = timestamp.valueOf()
notehistory[i].fromNow = timestamp.fromNow()
notehistory[i].time = timestamp.format('llll')
// prevent XSS
notehistory[i].text = S(notehistory[i].text).escapeHTML().s
notehistory[i].tags = (notehistory[i].tags && notehistory[i].tags.length > 0) ? S(notehistory[i].tags).escapeHTML().s.split(',') : []
// add to list
// prevent XSS
notehistory[i].text = escapeHTML(notehistory[i].text)
notehistory[i].tags = (notehistory[i].tags && notehistory[i].tags.length > 0) ? escapeHTML(notehistory[i].tags).split(',') : []
// add to list
if (notehistory[i].id && list.get('id', notehistory[i].id).length === 0) { list.add(notehistory[i]) }
}
}

File diff suppressed because it is too large Load Diff

View File

@ -60,22 +60,22 @@ export function checkIfAuth (yesCallback, noCallback) {
if (checkLoginStateChanged()) checkAuth = false
if (!checkAuth || typeof cookieLoginState === 'undefined') {
$.get(`${serverurl}/me`)
.done(data => {
if (data && data.status === 'ok') {
profile = data
yesCallback(profile)
setLoginState(true, data.id)
} else {
noCallback()
setLoginState(false)
}
})
.fail(() => {
noCallback()
})
.always(() => {
checkAuth = true
})
.done(data => {
if (data && data.status === 'ok') {
profile = data
yesCallback(profile)
setLoginState(true, data.id)
} else {
noCallback()
setLoginState(false)
}
})
.fail(() => {
noCallback()
})
.always(() => {
checkAuth = true
})
} else if (cookieLoginState) {
yesCallback(profile)
} else {

View File

@ -51,7 +51,7 @@ export function insertText (cm, text, cursorEnd = 0) {
let cursor = cm.getCursor()
cm.replaceSelection(text, cursor, cursor)
cm.focus()
cm.setCursor({line: cursor.line, ch: cursor.ch + cursorEnd})
cm.setCursor({ line: cursor.line, ch: cursor.ch + cursorEnd })
}
export function insertLink (cm, isImage) {
@ -80,7 +80,7 @@ export function insertLink (cm, isImage) {
cm.setSelections(ranges)
} else {
cm.replaceRange(symbol + linkEnd, cursor, cursor)
cm.setCursor({line: cursor.line, ch: cursor.ch + symbol.length + linkEnd.length})
cm.setCursor({ line: cursor.line, ch: cursor.ch + symbol.length + linkEnd.length })
}
}
cm.focus()
@ -88,8 +88,8 @@ 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})
let startOfLine = { line: cursor.line, ch: 0 }
let startOfLineText = cm.getRange(startOfLine, { line: cursor.line, ch: 1 })
// See if it is already a header
if (startOfLineText === '#') {
cm.replaceRange('#', startOfLine, startOfLine)
@ -108,14 +108,14 @@ export function insertOnStartOfLines (cm, symbol) {
if (!range.empty()) {
const from = range.from()
const to = range.to()
let selection = cm.getRange({line: from.line, ch: 0}, to)
let selection = cm.getRange({ line: from.line, ch: 0 }, to)
selection = selection.replace(/\n/g, '\n' + symbol)
selection = symbol + selection
cm.replaceRange(selection, from, to)
} else {
cm.replaceRange(symbol, {line: cursor.line, ch: 0}, {line: cursor.line, ch: 0})
cm.replaceRange(symbol, { line: cursor.line, ch: 0 }, { line: cursor.line, ch: 0 })
}
}
cm.setCursor({line: cursor.line, ch: cursor.ch + symbol.length})
cm.setCursor({ line: cursor.line, ch: cursor.ch + symbol.length })
cm.focus()
}

View File

@ -188,7 +188,7 @@ function buildMapInner (callback) {
}
nonEmptyList.push(0)
// make the first line go top
// make the first line go top
_scrollMap[0] = viewTop
const parts = markdownArea.find('.part').toArray()
@ -336,7 +336,7 @@ export function syncScrollToView (event, preventAnimate) {
const scrollInfo = editor.getScrollInfo()
const textHeight = editor.defaultTextHeight()
lineNo = Math.floor(scrollInfo.top / textHeight)
// if reach the last line, will start lerp to the bottom
// 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) {
topDiffPercent = diffToBottom / textHeight

View File

@ -1,29 +1,29 @@
/* eslint-env browser, jquery */
/* global refreshView */
import {
autoLinkify,
deduplicatedHeaderId,
removeDOMEvents,
finishView,
generateToc,
md,
parseMeta,
postProcess,
renderTOC,
scrollToHash,
smoothHashScroll,
updateLastChange
} from './extra'
import { preventXSS } from './render'
require('../css/extra.css')
require('../css/slide-preview.css')
require('../css/site.css')
require('highlight.js/styles/github-gist.css')
import {
autoLinkify,
deduplicatedHeaderId,
removeDOMEvents,
finishView,
generateToc,
md,
parseMeta,
postProcess,
renderTOC,
scrollToHash,
smoothHashScroll,
updateLastChange
} from './extra'
import { preventXSS } from './render'
const markdown = $('#doc.markdown-body')
const text = markdown.text()
const lastMeta = md.meta
@ -38,7 +38,7 @@ if (md.meta.type && md.meta.type === 'slide') {
const slides = window.RevealMarkdown.slidify(text, slideOptions)
markdown.html(slides)
window.RevealMarkdown.initialize()
// prevent XSS
// prevent XSS
markdown.html(preventXSS(markdown.html()))
markdown.addClass('slides')
} else {
@ -46,12 +46,12 @@ if (md.meta.type && md.meta.type === 'slide') {
refreshView()
markdown.removeClass('slides')
}
// only render again when meta changed
// only render again when meta changed
if (JSON.stringify(md.meta) !== JSON.stringify(lastMeta)) {
parseMeta(md, null, markdown, $('#ui-toc'), $('#ui-toc-affix'))
rendered = md.render(text)
}
// prevent XSS
// prevent XSS
rendered = preventXSS(rendered)
const result = postProcess(rendered)
markdown.html(result.html())
@ -98,14 +98,14 @@ function generateScrollspy () {
}
function windowResize () {
// toc right
// toc right
const paddingRight = parseFloat(markdown.css('padding-right'))
const right = ($(window).width() - (markdown.offset().left + markdown.outerWidth() - paddingRight))
toc.css('right', `${right}px`)
// affix toc left
// affix toc left
let newbool
const rightMargin = (markdown.parent().outerWidth() - markdown.outerWidth()) / 2
// for ipad or wider device
// for ipad or wider device
if (rightMargin >= 133) {
newbool = true
const affixLeftMargin = (tocAffix.outerWidth() - tocAffix.width()) / 2
@ -126,7 +126,7 @@ $(document).ready(() => {
windowResize()
generateScrollspy()
setTimeout(scrollToHash, 0)
// tooltip
// tooltip
$('[data-toggle="tooltip"]').tooltip()
})

View File

@ -44,7 +44,7 @@ var filterXSSOptions = {
onIgnoreTag: function (tag, html, options) {
// allow comment tag
if (tag === '!--') {
// do not filter its attributes
// do not filter its attributes
return html.replace(/<(?!!--)/g, '&lt;').replace(/-->/g, '__HTML_COMMENT_END__').replace(/>/g, '&gt;').replace(/__HTML_COMMENT_END__/g, '-->')
}
},

View File

@ -1,12 +1,12 @@
/* eslint-env browser, jquery */
/* global serverurl, Reveal, RevealMarkdown */
require('../css/extra.css')
require('../css/site.css')
import { preventXSS } from './render'
import { md, updateLastChange, removeDOMEvents, finishView } from './extra'
require('../css/extra.css')
require('../css/site.css')
const body = preventXSS($('.slides').text())
window.createtime = window.lastchangeui.time.attr('data-createtime')
@ -17,7 +17,7 @@ $('.ui-edit').attr('href', `${url}/edit`)
$('.ui-print').attr('href', `${url}?print-pdf`)
$(document).ready(() => {
// tooltip
// tooltip
$('[data-toggle="tooltip"]').tooltip()
})
@ -127,7 +127,7 @@ function renderSlide (event) {
Reveal.addEventListener('ready', event => {
renderSlide(event)
const markdown = $(event.currentSlide)
// force browser redraw
// force browser redraw
setTimeout(() => {
markdown.hide().show(0)
}, 0)

7
utils/string.js Normal file
View File

@ -0,0 +1,7 @@
'use strict'
function stripTags (s) {
return s.replace(RegExp(`</?[^<>]*>`, 'gi'), '')
}
exports.stripTags = stripTags

View File

@ -195,15 +195,11 @@ module.exports = {
'bootstrap-validator',
'expose-loader?select2!select2',
'expose-loader?moment!moment',
'script-loader!js-url',
path.join(__dirname, 'public/js/cover.js')
],
index: [
'babel-polyfill',
'script-loader!jquery-ui-resizable',
'script-loader!js-url',
'script-loader!Idle.Js',
'expose-loader?LZString!lz-string',
'script-loader!codemirror',
'script-loader!inlineAttachment',
'script-loader!jqueryTextcomplete',
@ -218,16 +214,16 @@ module.exports = {
'index-styles': [
path.join(__dirname, 'public/vendor/jquery-ui/jquery-ui.min.css'),
path.join(__dirname, 'public/vendor/codemirror-spell-checker/spell-checker.min.css'),
path.join(__dirname, 'node_modules/codemirror/lib/codemirror.css'),
path.join(__dirname, 'node_modules/codemirror/addon/fold/foldgutter.css'),
path.join(__dirname, 'node_modules/codemirror/addon/display/fullscreen.css'),
path.join(__dirname, 'node_modules/codemirror/addon/dialog/dialog.css'),
path.join(__dirname, 'node_modules/codemirror/addon/scroll/simplescrollbars.css'),
path.join(__dirname, 'node_modules/codemirror/addon/search/matchesonscrollbar.css'),
path.join(__dirname, 'node_modules/codemirror/theme/monokai.css'),
path.join(__dirname, 'node_modules/codemirror/theme/one-dark.css'),
path.join(__dirname, 'node_modules/codemirror/mode/tiddlywiki/tiddlywiki.css'),
path.join(__dirname, 'node_modules/codemirror/mode/mediawiki/mediawiki.css'),
path.join(__dirname, 'node_modules/@hackmd/codemirror/lib/codemirror.css'),
path.join(__dirname, 'node_modules/@hackmd/codemirror/addon/fold/foldgutter.css'),
path.join(__dirname, 'node_modules/@hackmd/codemirror/addon/display/fullscreen.css'),
path.join(__dirname, 'node_modules/@hackmd/codemirror/addon/dialog/dialog.css'),
path.join(__dirname, 'node_modules/@hackmd/codemirror/addon/scroll/simplescrollbars.css'),
path.join(__dirname, 'node_modules/@hackmd/codemirror/addon/search/matchesonscrollbar.css'),
path.join(__dirname, 'node_modules/@hackmd/codemirror/theme/monokai.css'),
path.join(__dirname, 'node_modules/@hackmd/codemirror/theme/one-dark.css'),
path.join(__dirname, 'node_modules/@hackmd/codemirror/mode/tiddlywiki/tiddlywiki.css'),
path.join(__dirname, 'node_modules/@hackmd/codemirror/mode/mediawiki/mediawiki.css'),
path.join(__dirname, 'public/css/github-extract.css'),
path.join(__dirname, 'public/vendor/showup/showup.css'),
path.join(__dirname, 'public/css/mermaid.css'),
@ -248,13 +244,10 @@ module.exports = {
'expose-loader?jsyaml!js-yaml',
'script-loader!mermaid',
'expose-loader?moment!moment',
'script-loader!js-url',
'script-loader!handlebars',
'expose-loader?hljs!highlight.js',
'expose-loader?emojify!emojify.js',
'script-loader!Idle.Js',
'script-loader!gist-embed',
'expose-loader?LZString!lz-string',
'script-loader!codemirror',
'script-loader!inlineAttachment',
'script-loader!jqueryTextcomplete',
@ -355,7 +348,7 @@ module.exports = {
modules: ['node_modules'],
extensions: ['.js'],
alias: {
codemirror: path.join(__dirname, 'node_modules/codemirror/codemirror.min.js'),
codemirror: path.join(__dirname, 'node_modules/@hackmd/codemirror/codemirror.min.js'),
inlineAttachment: path.join(__dirname, 'public/vendor/inlineAttachment/inline-attachment.js'),
jqueryTextcomplete: path.join(__dirname, 'public/vendor/jquery-textcomplete/jquery.textcomplete.js'),
codemirrorSpellChecker: path.join(__dirname, 'public/vendor/codemirror-spell-checker/spell-checker.min.js'),

1980
yarn.lock

File diff suppressed because it is too large Load Diff