Merge pull request #1515 from hackmdio/release-2.1.0

This commit is contained in:
Max Wu 2020-05-18 21:19:54 +08:00 committed by GitHub
commit 89a0de4205
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1851 additions and 834 deletions

View File

@ -1,7 +1,6 @@
language: node_js
node_js:
- "lts/carbon"
- "lts/dubnium"
- "11"
- "12"
@ -12,7 +11,6 @@ cache: npm
matrix:
fast_finish: true
include:
- node_js: lts/carbon
- node_js: lts/dubnium
allow_failures:
- node_js: "11"

8
app.js
View File

@ -17,6 +17,7 @@ var passportSocketIo = require('passport.socketio')
var helmet = require('helmet')
var i18n = require('i18n')
var flash = require('connect-flash')
var apiMetrics = require('prometheus-api-metrics')
// core
var config = require('./lib/config')
@ -56,6 +57,12 @@ function createHttpServer () {
var app = express()
var server = createHttpServer()
// API and process monitoring with Prometheus for Node.js micro-service
app.use(apiMetrics({
metricsPath: '/metrics/router',
excludeRoutes: ['/metrics/codimd']
}))
// logger
app.use(morgan('combined', {
stream: logger.stream
@ -131,6 +138,7 @@ app.use('/', express.static(path.join(__dirname, '/public'), { maxAge: config.st
app.use('/docs', express.static(path.resolve(__dirname, config.docsPath), { maxAge: config.staticCacheTime }))
app.use('/uploads', express.static(path.resolve(__dirname, config.uploadsPath), { maxAge: config.staticCacheTime }))
app.use('/default.md', express.static(path.resolve(__dirname, config.defaultNotePath), { maxAge: config.staticCacheTime }))
app.use(require('./lib/metrics').router)
// session
app.use(session({

View File

@ -1,4 +1,6 @@
FROM hackmdio/buildpack:1.0.4 as BUILD
ARG RUNTIME
FROM hackmdio/buildpack:node-10-0baafb79 as BUILD
COPY --chown=hackmd:hackmd . .
@ -12,11 +14,12 @@ RUN set -xe && \
rm -rf .git .gitignore .travis.yml .dockerignore .editorconfig .babelrc .mailmap .sequelizerc.example \
test docs contribute \
package-lock.json webpack.prod.js webpack.htmlexport.js webpack.dev.js webpack.common.js \
config.json.example README.md CONTRIBUTING.md AUTHORS
config.json.example README.md CONTRIBUTING.md AUTHORS node_modules
FROM hackmdio/runtime:1.0.6
FROM $RUNTIME
USER hackmd
WORKDIR /home/hackmd/app
COPY --chown=1500:1500 --from=BUILD /home/hackmd/app .
RUN npm install --production && npm cache clean --force && rm -rf /tmp/{core-js-banners,phantomjs}
EXPOSE 3000
ENTRYPOINT ["/home/hackmd/app/docker-entrypoint.sh"]

View File

@ -1,5 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
set -x
CURRENT_DIR=$(dirname "$BASH_SOURCE")
docker build -t hackmdio/codimd -f "$CURRENT_DIR/Dockerfile" "$CURRENT_DIR/.."
GIT_SHA1="$(git rev-parse HEAD)"
GIT_SHORT_ID="${GIT_SHA1:0:8}"
GIT_TAG=$(git describe --exact-match --tags $(git log -n1 --pretty='%h') 2>/dev/null || echo "")
DOCKER_TAG="${GIT_TAG:-$GIT_SHORT_ID}"
docker build --build-arg RUNTIME=hackmdio/runtime:node-10-d27854ef -t "hackmdio/hackmd:$DOCKER_TAG" -f "$CURRENT_DIR/Dockerfile" "$CURRENT_DIR/.."
docker build --build-arg RUNTIME=hackmdio/runtime:node-10-cjk-d27854ef -t "hackmdio/hackmd:$DOCKER_TAG-cjk" -f "$CURRENT_DIR/Dockerfile" "$CURRENT_DIR/.."

View File

@ -15,50 +15,56 @@ const emailAuth = module.exports = Router()
passport.use(new LocalStrategy({
usernameField: 'email'
}, function (email, password, done) {
}, async function (email, password, done) {
if (!validator.isEmail(email)) return done(null, false)
models.User.findOne({
where: {
email: email
}
}).then(function (user) {
try {
const user = await models.User.findOne({
where: {
email: email
}
})
if (!user) return done(null, false)
if (!user.verifyPassword(password)) return done(null, false)
if (!await user.verifyPassword(password)) return done(null, false)
return done(null, user)
}).catch(function (err) {
} catch (err) {
logger.error(err)
return done(err)
})
}
}))
if (config.allowEmailRegister) {
emailAuth.post('/register', urlencodedParser, function (req, res, next) {
emailAuth.post('/register', urlencodedParser, async function (req, res, next) {
if (!req.body.email || !req.body.password) return response.errorBadRequest(req, res)
if (!validator.isEmail(req.body.email)) return response.errorBadRequest(req, res)
models.User.findOrCreate({
where: {
email: req.body.email
},
defaults: {
password: req.body.password
}
}).spread(function (user, created) {
if (user) {
if (created) {
logger.debug('user registered: ' + user.id)
req.flash('info', "You've successfully registered, please signin.")
} else {
logger.debug('user found: ' + user.id)
req.flash('error', 'This email has been used, please try another one.')
try {
const [user, created] = await models.User.findOrCreate({
where: {
email: req.body.email
},
defaults: {
password: req.body.password
}
})
if (!user) {
req.flash('error', 'Failed to register your account, please try again.')
return res.redirect(config.serverURL + '/')
}
req.flash('error', 'Failed to register your account, please try again.')
if (created) {
logger.debug('user registered: ' + user.id)
req.flash('info', "You've successfully registered, please signin.")
} else {
logger.debug('user found: ' + user.id)
req.flash('error', 'This email has been used, please try another one.')
}
return res.redirect(config.serverURL + '/')
}).catch(function (err) {
} catch (err) {
logger.error('auth callback failed: ' + err)
return response.errorInternalError(req, res)
})
}
})
}

View File

@ -16,6 +16,7 @@ passport.use(new OAuth2CustomStrategy({
clientSecret: config.oauth2.clientSecret,
callbackURL: config.serverURL + '/auth/oauth2/callback',
userProfileURL: config.oauth2.userProfileURL,
state: config.oauth2.state,
scope: config.oauth2.scope
}, passportGeneralCallback))

View File

@ -100,6 +100,7 @@ module.exports = {
userProfileDisplayNameAttr: 'displayName',
userProfileEmailAttr: 'email',
userProfilePhotoAttr: 'photo',
state: true,
scope: 'email'
},
facebook: {

View File

@ -94,6 +94,7 @@ module.exports = {
tokenURL: process.env.CMD_OAUTH2_TOKEN_URL,
userProfileURL: process.env.CMD_OAUTH2_USER_PROFILE_URL,
scope: process.env.CMD_OAUTH2_SCOPE,
state: process.env.CMD_OAUTH2_STATE,
userProfileUsernameAttr: process.env.CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR,
userProfileDisplayNameAttr: process.env.CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR,
userProfileEmailAttr: process.env.CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR,

View File

@ -2,7 +2,7 @@
const config = require('../config')
const logger = require('../logger')
const lutim = require('lib/imageRouter/lutim')
const lutim = require('lutim')
exports.uploadImage = function (imagePath, callback) {
if (!imagePath || typeof imagePath !== 'string') {

View File

@ -6,9 +6,10 @@ const config = require('../config')
const { getImageMimeType } = require('../utils')
const logger = require('../logger')
const AWS = require('aws-sdk')
const awsConfig = new AWS.Config(config.s3)
const s3 = new AWS.S3(awsConfig)
const { S3Client } = require('@aws-sdk/client-s3-node/S3Client')
const { PutObjectCommand } = require('@aws-sdk/client-s3-node/commands/PutObjectCommand')
const s3 = new S3Client(config.s3)
exports.uploadImage = function (imagePath, callback) {
if (!imagePath || typeof imagePath !== 'string') {
@ -32,16 +33,12 @@ exports.uploadImage = function (imagePath, callback) {
Body: buffer,
ACL: 'public-read'
}
const mimeType = getImageMimeType(imagePath)
if (mimeType) { params.ContentType = mimeType }
s3.putObject(params, function (err, data) {
if (err) {
callback(new Error(err), null)
return
}
const command = new PutObjectCommand(params)
s3.send(command).then(data => {
let s3Endpoint = 's3.amazonaws.com'
if (config.s3.endpoint) {
s3Endpoint = config.s3.endpoint
@ -49,6 +46,10 @@ exports.uploadImage = function (imagePath, callback) {
s3Endpoint = `s3-${config.s3.region}.amazonaws.com`
}
callback(null, `https://${s3Endpoint}/${config.s3bucket}/${params.Key}`)
}).catch(err => {
if (err) {
callback(new Error(err), null)
}
})
})
}

15
lib/metrics.js Normal file
View File

@ -0,0 +1,15 @@
'use strict'
const { Router } = require('express')
const { wrap } = require('./utils')
// load controller
const statusController = require('./status')
const appRouter = Router()
// register route
appRouter.get('/status', wrap(statusController.getStatus))
appRouter.get('/metrics/codimd', wrap(statusController.getMetrics))
exports.router = appRouter

View File

@ -1,7 +1,7 @@
'use strict'
// external modules
var Sequelize = require('sequelize')
var scrypt = require('scrypt')
var Scrypt = require('scrypt-kdf')
// core
var logger = require('../logger')
@ -41,22 +41,34 @@ module.exports = function (sequelize, DataTypes) {
}
},
password: {
type: Sequelize.TEXT,
set: function (value) {
var hash = scrypt.kdfSync(value, scrypt.paramsSync(0.1)).toString('hex')
this.setDataValue('password', hash)
}
type: Sequelize.TEXT
}
})
User.prototype.verifyPassword = function (attempt) {
if (scrypt.verifyKdfSync(Buffer.from(this.password, 'hex'), attempt)) {
return this
} else {
return false
}
User.hashPassword = async function (plain) {
return (await Scrypt.kdf(plain, await Scrypt.pickParams(0.1))).toString('hex')
}
User.prototype.verifyPassword = async function (attempt) {
if (await Scrypt.verify(Buffer.from(this.password, 'hex'), attempt)) {
return this
}
return false
}
User.addHook('beforeCreate', async function (user) {
// only do hash when password is presented
if (user.password) {
user.password = await User.hashPassword(user.password)
}
})
User.addHook('beforeUpdate', async function (user) {
if (user.changed('password')) {
user.password = await User.hashPassword(user.password)
}
})
User.associate = function (models) {
User.hasMany(models.Note, {
foreignKey: 'ownerId',
@ -103,10 +115,11 @@ module.exports = function (sequelize, DataTypes) {
else photo += '?size=bigger'
break
case 'github':
if (profile.photos && profile.photos[0]) photo = profile.photos[0].value.replace('?', '')
else photo = 'https://avatars.githubusercontent.com/u/' + profile.id
if (bigger) photo += '?s=400'
else photo += '?s=96'
const photoURL = new URL(profile.photos && profile.photos[0]
? profile.photos[0].value
: `https://avatars.githubusercontent.com/u/${profile.id}`)
photoURL.searchParams.set('s', bigger ? 400 : 96)
photo = photoURL.toString()
break
case 'gitlab':
photo = profile.avatarUrl

View File

@ -61,6 +61,8 @@ async function showNote (req, res) {
// if allow free url enable, auto create note
if (!config.allowFreeURL || config.forbiddenNoteIDs.includes(noteId)) {
return errorNotFound(req, res)
} else if (!config.allowAnonymous && !userId) {
return errorForbidden(req, res)
}
note = await createNote(userId, noteId)
}

View File

@ -298,10 +298,12 @@ function getStatus () {
}
})
.catch(function (err) {
return logger.error('count user failed: ' + err)
logger.error('count user failed: ' + err)
return Promise.reject(new Error('count user failed: ' + err))
})
}).catch(function (err) {
return logger.error('count note failed: ' + err)
logger.error('count note failed: ' + err)
return Promise.reject(new Error('count note failed: ' + err))
})
}
@ -772,8 +774,7 @@ function queueForConnect (socket) {
const noteId = socket.noteId
logger.info('SERVER connected a client to [' + noteId + ']:')
logger.info(JSON.stringify(user))
// logger.info(notes);
getStatus(function (data) {
getStatus().then(function (data) {
logger.info(JSON.stringify(data))
})
}

View File

@ -27,7 +27,6 @@ appRouter.get('/404', errorPageController.errorNotFound)
// get 500 internal error
appRouter.get('/500', errorPageController.errorInternalError)
appRouter.get('/status', wrap(statusController.getStatus))
appRouter.get('/config', statusController.getConfig)
// register auth module

View File

@ -4,14 +4,30 @@ const realtime = require('../realtime/realtime')
const config = require('../config')
exports.getStatus = async (req, res) => {
const data = await realtime.getStatus()
res.set({
'Cache-Control': 'private', // only cache by client
'X-Robots-Tag': 'noindex, nofollow', // prevent crawling
'Content-Type': 'application/json'
})
res.send(data)
try {
const data = await realtime.getStatus()
res.send(data)
} catch (e) {
console.error(e)
res.status(500).send(e.toString())
}
}
exports.getMetrics = async (req, res) => {
const data = await realtime.getStatus()
res.set({
'Cache-Control': 'private', // only cache by client
'X-Robots-Tag': 'noindex, nofollow', // prevent crawling
'Content-Type': 'text/plain; charset=utf-8'
})
res.render('../js/lib/common/metrics.ejs', data)
}
exports.getConfig = (req, res) => {

2247
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "codimd",
"version": "2.0.1",
"version": "2.1.0",
"description": "Realtime collaborative markdown notes on all platforms.",
"keywords": [
"Collaborative",
@ -30,25 +30,19 @@
"postinstall": "bin/heroku"
},
"dependencies": {
"@hackmd/codemirror": "~5.49.5",
"@aws-sdk/client-s3-node": "0.1.0-preview.2",
"@hackmd/diff-match-patch": "~1.1.3",
"@hackmd/idle-js": "~1.0.1",
"@hackmd/imgur": "~0.5.0",
"@hackmd/js-sequence-diagrams": "~0.0.1-alpha.3",
"@hackmd/lz-string": "~1.4.4",
"@hackmd/meta-marked": "~0.4.4",
"@hackmd/pandoc.js": "^0.1.9",
"@passport-next/passport-openid": "~1.0.0",
"@susisu/mte-kernel": "^2.1.0",
"archiver": "~3.1.1",
"async": "~3.1.0",
"aws-sdk": "~2.503.0",
"azure-storage": "~2.10.3",
"babel-polyfill": "~6.26.0",
"base64url": "~3.0.1",
"body-parser": "~1.19.0",
"bootstrap": "~3.4.0",
"bootstrap-validator": "~0.11.8",
"chance": "~1.0.18",
"cheerio": "~0.22.0",
"connect-flash": "~0.1.1",
@ -59,46 +53,17 @@
"ejs": "~2.6.2",
"express": "~4.17.1",
"express-session": "~1.16.2",
"file-saver": "~2.0.2",
"flowchart.js": "~1.12.2",
"fork-awesome": "~1.1.7",
"formidable": "~1.2.1",
"gist-embed": "~2.6.0",
"graceful-fs": "~4.2.1",
"handlebars": "~4.1.2",
"helmet": "~3.20.0",
"highlight.js": "~9.15.9",
"https-proxy-agent": "^3.0.1",
"i18n": "~0.8.3",
"ionicons": "~2.0.1",
"isomorphic-fetch": "~2.2.1",
"jquery": "~3.4.1",
"jquery-mousewheel": "~3.1.13",
"jquery-ui": "~1.12.1",
"js-cookie": "~2.2.0",
"js-yaml": "~3.13.1",
"jsdom-nogyp": "~0.8.3",
"keymaster": "~1.6.2",
"list.js": "~1.5.0",
"lodash": "~4.17.15",
"lutim": "~1.0.2",
"markdown-it": "~10.0.0",
"markdown-it-abbr": "~1.0.4",
"markdown-it-container": "~2.0.0",
"markdown-it-deflist": "~2.0.3",
"markdown-it-footnote": "~3.0.2",
"markdown-it-imsize": "~2.0.1",
"markdown-it-ins": "~2.0.0",
"markdown-it-mark": "~2.0.0",
"markdown-it-mathjax": "~2.0.0",
"markdown-it-regexp": "~0.4.0",
"markdown-it-sub": "~1.0.0",
"markdown-it-sup": "~1.0.0",
"markdown-pdf": "~9.0.0",
"markdownlint": "^0.17.0",
"mathjax": "~2.7.5",
"mattermost-redux": "~5.13.0",
"mermaid": "~8.4.8",
"method-override": "~3.0.0",
"minimist": "~1.2.0",
"minio": "^7.0.12",
@ -119,50 +84,38 @@
"passport-saml": "~1.0.0",
"passport-twitter": "~1.0.4",
"passport.socketio": "~3.7.0",
"pdfobject": "~2.1.1",
"pg": "~6.1.2",
"pg-hstore": "~2.3.2",
"plantuml-encoder": "^1.2.5",
"prismjs": "~1.17.1",
"prom-client": "^12.0.0",
"prometheus-api-metrics": "^2.2.5",
"randomcolor": "~0.5.4",
"raphael": "~2.2.8",
"readline-sync": "~1.4.7",
"request": "~2.88.0",
"reveal.js": "~3.9.2",
"scrypt": "~6.0.3",
"select2": "~3.5.2-browserify",
"scrypt-kdf": "^2.0.1",
"sequelize": "5.21.3",
"sequelize-cli": "~5.5.1",
"shortid": "~2.2.14",
"socket.io": "~2.2.0",
"socket.io-client": "~2.2.0",
"spin.js": "~4.0.0",
"sqlite3": "~4.0.9",
"store": "~2.0.12",
"tedious": "~6.2.0",
"toobusy-js": "~0.5.1",
"turndown": "~5.0.3",
"uuid": "~3.3.2",
"validator": "~11.1.0",
"vega": "~5.9.1",
"vega-embed": "~6.2.2",
"vega-lite": "~4.4.0",
"velocity-animate": "~1.5.2",
"visibilityjs": "~2.0.2",
"viz.js": "~2.1.2",
"winston": "~3.2.1",
"ws": "~7.1.1",
"wurl": "~2.5.3",
"xss": "~1.0.6"
"ws": "~7.1.1"
},
"devDependencies": {
"@hackmd/codemirror": "~5.49.5",
"@hackmd/emojify.js": "^2.1.0",
"@hackmd/idle-js": "~1.0.1",
"@hackmd/js-sequence-diagrams": "~0.0.1-alpha.3",
"@susisu/mte-kernel": "^2.1.0",
"acorn": "~6.1.1",
"babel-core": "~6.26.3",
"babel-loader": "~7.1.4",
"babel-plugin-transform-runtime": "~6.23.0",
"babel-preset-env": "~1.7.0",
"babel-runtime": "~6.26.0",
"bootstrap": "~3.4.0",
"bootstrap-validator": "~0.11.8",
"copy-webpack-plugin": "~4.5.2",
"css-loader": "~1.0.0",
"dictionary-de": "^2.0.3",
@ -173,39 +126,87 @@
"exports-loader": "~0.7.0",
"expose-loader": "~0.7.5",
"file-loader": "~2.0.0",
"file-saver": "~2.0.2",
"flowchart.js": "~1.12.2",
"fork-awesome": "~1.1.7",
"gist-embed": "~2.6.0",
"graceful-fs": "~4.2.1",
"handlebars": "~4.1.2",
"highlight.js": "~9.15.9",
"html-webpack-plugin": "~4.0.0-beta.2",
"imports-loader": "~0.8.0",
"intelli-espower-loader": "~1.0.1",
"ionicons": "~2.0.1",
"jquery": "~3.4.1",
"jquery-mousewheel": "~3.1.13",
"jquery-ui": "~1.12.1",
"js-cookie": "~2.2.0",
"js-yaml": "~3.13.1",
"jsonlint": "~1.6.2",
"keymaster": "~1.6.2",
"leaflet": "~1.6.0",
"less": "~3.9.0",
"less-loader": "~4.1.0",
"list.js": "~1.5.0",
"markdown-it-abbr": "~1.0.4",
"markdown-it-container": "~2.0.0",
"markdown-it-deflist": "~2.0.3",
"markdown-it-footnote": "~3.0.2",
"markdown-it-imsize": "~2.0.1",
"markdown-it-ins": "~2.0.0",
"markdown-it-mark": "~2.0.0",
"markdown-it-mathjax": "~2.0.0",
"markdown-it-regexp": "~0.4.0",
"markdown-it-ruby": "^0.1.1",
"markdown-it-sub": "~1.0.0",
"markdown-it-sup": "~1.0.0",
"markdownlint": "^0.17.0",
"mathjax": "~2.7.5",
"mermaid": "~8.4.8",
"mini-css-extract-plugin": "~0.4.1",
"mocha": "~5.2.0",
"mock-require": "~3.0.3",
"nyc": "~14.0.0",
"optimize-css-assets-webpack-plugin": "~5.0.0",
"pdfobject": "~2.1.1",
"plantuml-encoder": "^1.2.5",
"power-assert": "~1.6.1",
"prismjs": "^1.17.1",
"raphael": "~2.2.8",
"reveal.js": "~3.9.2",
"script-loader": "~0.7.2",
"select2": "~3.5.2-browserify",
"sinon": "~7.3.2",
"socket.io-client": "~2.2.0",
"spin.js": "~4.0.0",
"standard": "~13.1.0",
"store": "~2.0.12",
"string-loader": "~0.0.1",
"style-loader": "~0.23.1",
"tedious": "~6.2.0",
"turndown": "~5.0.3",
"typo-js": "^1.0.3",
"uglifyjs-webpack-plugin": "~1.2.7",
"url-loader": "~1.0.1",
"vega": "~5.9.1",
"vega-embed": "~6.2.2",
"vega-lite": "~4.4.0",
"velocity-animate": "~1.5.2",
"visibilityjs": "~2.0.2",
"viz.js": "~2.1.2",
"webpack": "~4.39.0",
"webpack-cli": "~3.3.6",
"webpack-merge": "~4.2.1",
"webpack-parallel-uglify-plugin": "~1.1.0"
"webpack-parallel-uglify-plugin": "~1.1.0",
"wurl": "~2.5.3",
"xss": "~1.0.6"
},
"optionalDependencies": {
"bufferutil": "~4.0.0",
"utf-8-validate": "~5.0.1"
},
"engines": {
"node": ">=8.0.0 <12.0.0"
"node": ">=10.0.0 <13.0.0"
},
"maintainers": [
{

View File

@ -15,6 +15,7 @@ import { stripTags } from '../../utils/string'
import getUIElements from './lib/editor/ui-elements'
import { emojifyImageDir } from './lib/editor/constants'
import { parseFenceCodeParams, serializeParamToAttribute } from './lib/markdown/utils'
import markdownit from 'markdown-it'
import markdownitContainer from 'markdown-it-container'
@ -1028,24 +1029,30 @@ export function scrollToHash () {
location.hash = hash
}
const fenceCodeAlias = {
sequence: 'sequence-diagram',
flow: 'flow-chart',
graphviz: 'graphviz',
mermaid: 'mermaid',
abc: 'abc',
vega: 'vega',
geo: 'geo'
}
function highlightRender (code, lang) {
if (!lang || /no(-?)highlight|plain|text/.test(lang)) { return }
const params = parseFenceCodeParams(lang)
const attr = serializeParamToAttribute(params)
lang = lang.split(/\s+/g)[0]
code = escapeHTML(code)
if (lang === 'sequence') {
return `<div class="sequence-diagram raw">${code}</div>`
} else if (lang === 'flow') {
return `<div class="flow-chart raw">${code}</div>`
} else if (lang === 'graphviz') {
return `<div class="graphviz raw">${code}</div>`
} else if (lang === 'mermaid') {
return `<div class="mermaid raw">${code}</div>`
} else if (lang === 'abc') {
return `<div class="abc raw">${code}</div>`
} else if (lang === 'vega') {
return `<div class="vega raw">${code}</div>`
} else if (lang === 'geo') {
return `<div class="geo raw">${code}</div>`
const langAlias = fenceCodeAlias[lang]
if (langAlias) {
return `<div class="${langAlias} raw"${attr}>${code}</div>`
}
const result = {
value: code
}
@ -1167,7 +1174,7 @@ md.renderer.rules.fence = (tokens, idx, options, env, self) => {
}
if (options.highlight) {
highlighted = options.highlight(token.content, langName) || md.utils.escapeHtml(token.content)
highlighted = options.highlight(token.content, info) || md.utils.escapeHtml(token.content)
} else {
highlighted = md.utils.escapeHtml(token.content)
}

View File

@ -0,0 +1,11 @@
online_notes <%- onlineNotes %>
online_users <%- onlineUsers %>
distinct_online_users <%- distinctOnlineUsers %>
notes_count <%- notesCount %>
registered_users <%- registeredUsers %>
online_registered_users <%- onlineRegisteredUsers %>
distinct_online_registered_users <%- distinctOnlineRegisteredUsers %>
is_connection_busy <%- isConnectionBusy ? 1 : 0 %>
connection_socket_queue_length <%- connectionSocketQueueLength %>
is_disconnect_busy <%- isDisconnectBusy ? 1: 0 %>
disconnect_socket_queue_length <%- disconnectSocketQueueLength %>

View File

@ -0,0 +1,53 @@
export function parseFenceCodeParams (lang) {
const attrMatch = lang.match(/{(.*)}/)
const params = {}
if (attrMatch && attrMatch.length >= 2) {
const attrs = attrMatch[1]
const paraMatch = attrs.match(/([#.](\S+?)\s)|((\S+?)\s*=\s*("(.+?)"|'(.+?)'|\[[^\]]*\]|\{[}]*\}|(\S+)))/g)
paraMatch && paraMatch.forEach(param => {
param = param.trim()
if (param[0] === '#') {
params['id'] = param.slice(1)
} else if (param[0] === '.') {
if (params['class']) params['class'] = []
params['class'] = params['class'].concat(param.slice(1))
} else {
const offset = param.indexOf('=')
const id = param.substring(0, offset).trim().toLowerCase()
let val = param.substring(offset + 1).trim()
const valStart = val[0]
const valEnd = val[val.length - 1]
if (['"', "'"].indexOf(valStart) !== -1 && ['"', "'"].indexOf(valEnd) !== -1 && valStart === valEnd) {
val = val.substring(1, val.length - 1)
}
if (id === 'class') {
if (params['class']) params['class'] = []
params['class'] = params['class'].concat(val)
} else {
params[id] = val
}
}
})
}
return params
}
export function serializeParamToAttribute (params) {
if (Object.getOwnPropertyNames(params).length === 0) {
return ''
} else {
return ` data-params="${escape(JSON.stringify(params))}"`
}
}
/**
* @param {HTMLElement} elem
*/
export function deserializeParamAttributeFromElement (elem) {
const params = elem.getAttribute('data-params')
if (params) {
return JSON.parse(unescape(params))
} else {
return {}
}
}

View File

@ -73,7 +73,7 @@ md.renderer.rules.fence = (tokens, idx, options, env, self) => {
}
if (options.highlight) {
highlighted = options.highlight(token.content, langName) || md.utils.escapeHtml(token.content)
highlighted = options.highlight(token.content, info) || md.utils.escapeHtml(token.content)
} else {
highlighted = md.utils.escapeHtml(token.content)
}