Merge pull request #1677 from hackmdio/release/2.4.0

Release 2.4.0
This commit is contained in:
Yukai Huang 2021-05-11 19:24:04 +08:00 committed by GitHub
commit 2e468db210
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 575 additions and 863 deletions

View File

@ -71,3 +71,5 @@ If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`. You can also use git [aliases](https://git-scm.com/book/tr/v2/Git-Basics-Git-Aliases)
like `git config --global alias.ci 'commit -s'`. Now you can commit with
`git ci` and the commit will be signed.
[dcofile]: https://github.com/hackmdio/codimd/blob/develop/contribute/developer-certificate-of-origin

View File

@ -4,6 +4,7 @@ CodiMD
[![build status][travis-image]][travis-url]
[![version][github-version-badge]][github-release-page]
[![Gitter][gitter-image]][gitter-url]
[![Matrix][matrix-image]][matrix-url]
[![POEditor][poeditor-image]][poeditor-url]
CodiMD lets you collaborate in real-time with markdown.
@ -99,3 +100,5 @@ To stay up to date with your installation it's recommended to subscribe the [rel
[github-release-feed]: https://github.com/hackmdio/codimd/releases.atom
[poeditor-image]: https://img.shields.io/badge/POEditor-translate-blue.svg
[poeditor-url]: https://poeditor.com/join/project/q0nuPWyztp
[matrix-image]: https://img.shields.io/matrix/hackmdio_hackmd:gitter.im?color=blue&logo=matrix
[matrix-url]: https://matrix.to/#/#hackmdio_hackmd:gitter.im

1
app.js
View File

@ -194,6 +194,7 @@ app.set('view engine', 'ejs')
app.locals.useCDN = config.useCDN
app.locals.serverURL = config.serverURL
app.locals.sourceURL = config.sourceURL
app.locals.privacyPolicyURL = config.privacyPolicyURL
app.locals.allowAnonymous = config.allowAnonymous
app.locals.allowAnonymousEdits = config.allowAnonymousEdits
app.locals.permission = config.permission

View File

@ -143,6 +143,10 @@
"CMD_ALLOW_PDF_EXPORT": {
"description": "Enable or disable PDF exports",
"required": false
},
"PGSSLMODE": {
"description": "Enforce PG SSL mode",
"value": "require"
}
},
"addons": [

View File

@ -5,9 +5,22 @@ const config = require('../config')
const logger = require('../logger')
exports.setReturnToFromReferer = function setReturnToFromReferer (req) {
var referer = req.get('referer')
if (!req.session) req.session = {}
req.session.returnTo = referer
var referer = req.get('referer')
var refererSearchParams = new URLSearchParams(new URL(referer).search)
var nextURL = refererSearchParams.get('next')
if (nextURL) {
var isRelativeNextURL = nextURL.indexOf('://') === -1 && !nextURL.startsWith('//')
if (isRelativeNextURL) {
req.session.returnTo = (new URL(nextURL, config.serverURL)).toString()
} else {
req.session.returnTo = config.serverURL
}
} else {
req.session.returnTo = referer
}
}
exports.passportGeneralCallback = function callback (accessToken, refreshToken, profile, done) {

View File

@ -37,6 +37,7 @@ module.exports = {
defaultPermission: 'editable',
dbURL: '',
db: {},
privacyPolicyURL: '',
// ssl path
sslKeyPath: '',
sslCertPath: '',
@ -188,5 +189,6 @@ module.exports = {
// 2nd appearance: "31-good-morning-my-friend---do-you-have-5-1"
// 3rd appearance: "31-good-morning-my-friend---do-you-have-5-2"
linkifyHeaderStyle: 'keep-case',
autoVersionCheck: true
autoVersionCheck: true,
defaultTocDepth: 3
}

View File

@ -35,6 +35,7 @@ module.exports = {
sessionSecret: process.env.CMD_SESSION_SECRET,
sessionLife: toIntegerConfig(process.env.CMD_SESSION_LIFE),
responseMaxLag: toIntegerConfig(process.env.CMD_RESPONSE_MAX_LAG),
privacyPolicyURL: process.env.CMD_PRIVACY_POLICY_URL,
imageUploadType: process.env.CMD_IMAGE_UPLOAD_TYPE,
imgur: {
clientID: process.env.CMD_IMGUR_CLIENTID
@ -147,5 +148,6 @@ module.exports = {
openID: toBooleanConfig(process.env.CMD_OPENID),
defaultUseHardbreak: toBooleanConfig(process.env.CMD_DEFAULT_USE_HARD_BREAK),
linkifyHeaderStyle: process.env.CMD_LINKIFY_HEADER_STYLE,
autoVersionCheck: toBooleanConfig(process.env.CMD_AUTO_VERSION_CHECK)
autoVersionCheck: toBooleanConfig(process.env.CMD_AUTO_VERSION_CHECK),
defaultTocDepth: toIntegerConfig(process.env.CMD_DEFAULT_TOC_DEPTH)
}

View File

@ -92,20 +92,22 @@ module.exports = function (sequelize, DataTypes) {
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')
let filePath = config.defaultNotePath
if (note.alias) {
const notePathInDocPath = path.join(config.docsPath, path.basename(note.alias) + '.md')
if (Note.checkFileExist(notePathInDocPath)) {
filePath = notePathInDocPath
}
}
if (Note.checkFileExist(filePath)) {
var fsCreatedTime = moment(fs.statSync(filePath).ctime)
body = fs.readFileSync(filePath, 'utf8')
note.title = Note.parseNoteTitle(body)
note.content = body
const noteInFS = readFileSystemNote(filePath)
note.title = noteInFS.title
note.content = noteInFS.content
if (filePath !== config.defaultNotePath) {
note.createdAt = fsCreatedTime
note.createdAt = noteInFS.lastchangeAt
note.lastchangeAt = noteInFS.lastchangeAt
}
}
}
@ -196,6 +198,29 @@ module.exports = function (sequelize, DataTypes) {
})
})
}
async function syncNote (noteInFS, note) {
const contentLength = noteInFS.content.length
let note2 = await note.update({
title: noteInFS.title,
content: noteInFS.content,
lastchangeAt: noteInFS.lastchangeAt
})
const revision = await sequelize.models.Revision.saveNoteRevisionAsync(note2)
// update authorship on after making revision of docs
const patch = dmp.patch_fromText(revision.patch)
const operations = Note.transformPatchToOperations(patch, contentLength)
let authorship = note2.authorship
for (let i = 0; i < operations.length; i++) {
authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship)
}
note2 = await note.update({
authorship: authorship
})
return note2.id
}
Note.parseNoteId = function (noteId, callback) {
async.series({
parseNoteIdByAlias: function (_callback) {
@ -204,65 +229,35 @@ module.exports = function (sequelize, DataTypes) {
where: {
alias: noteId
}
}).then(function (note) {
if (note) {
const filePath = path.join(config.docsPath, noteId + '.md')
if (Note.checkFileExist(filePath)) {
// if doc in filesystem have newer modified time than last change time
// then will update the doc in db
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)
})
}).then(async function (note) {
const filePath = path.join(config.docsPath, path.basename(noteId) + '.md')
if (Note.checkFileExist(filePath)) {
try {
if (note) {
// if doc in filesystem have newer modified time than last change time
// then will update the doc in db
const noteInFS = readFileSystemNote(filePath)
if (shouldSyncNote(note, noteInFS)) {
const noteId = await syncNote(noteInFS, note)
return callback(null, noteId)
}
} else {
// create new note with alias, and will sync md file in beforeCreateHook
const note = await Note.create({
alias: noteId,
owner: null,
permission: 'locked'
})
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 (err) {
return _callback(err, null)
}
}
if (!note) {
return _callback(null, null)
}
return callback(null, note.id)
}).catch(function (err) {
return _callback(err, null)
})
@ -589,5 +584,21 @@ module.exports = function (sequelize, DataTypes) {
return operations
}
function readFileSystemNote (filePath) {
const fsModifiedTime = moment(fs.statSync(filePath).mtime)
const content = fs.readFileSync(filePath, 'utf8')
return {
lastchangeAt: fsModifiedTime,
title: Note.parseNoteTitle(content),
content: content
}
}
function shouldSyncNote (note, noteInFS) {
const dbModifiedTime = moment(note.lastchangeAt || note.createdAt)
return noteInFS.lastchangeAt.isAfter(dbModifiedTime) && note.content !== noteInFS.content
}
return Note
}

View File

@ -6,6 +6,7 @@ var moment = require('moment')
var childProcess = require('child_process')
var shortId = require('shortid')
var path = require('path')
var util = require('util')
var Op = Sequelize.Op
@ -296,6 +297,7 @@ module.exports = function (sequelize, DataTypes) {
return callback(err, null)
})
}
Revision.saveNoteRevisionAsync = util.promisify(Revision.saveNoteRevision)
Revision.finishSaveNoteRevision = function (note, revision, callback) {
note.update({
savedAt: revision.updatedAt

View File

@ -268,7 +268,7 @@ const deleteNote = async (req, res) => {
}
const updateNote = async (req, res) => {
if (req.isAuthenticated()) {
if (req.isAuthenticated() || config.allowAnonymousEdits) {
const noteId = await Note.parseNoteIdAsync(req.params.noteId)
try {
const note = await Note.findOne({
@ -294,7 +294,7 @@ const updateNote = async (req, res) => {
lastchangeAt: now,
authorship: [
[
req.user.id,
req.isAuthenticated() ? req.user.id : null,
0,
content.length,
now,
@ -308,7 +308,9 @@ const updateNote = async (req, res) => {
return errorInternalError(req, res)
}
updateHistory(req.user.id, note.id, content)
if (req.isAuthenticated()) {
updateHistory(req.user.id, noteId, content)
}
Revision.saveNoteRevision(note, (err, revision) => {
if (err) {
@ -321,7 +323,7 @@ const updateNote = async (req, res) => {
})
})
} catch (err) {
logger.error(err)
logger.error(err.stack)
logger.error('Update note failed: Internal Error.')
return errorInternalError(req, res)
}

View File

@ -32,8 +32,10 @@ function errorForbidden (req, res) {
if (req.user) {
responseError(res, '403', 'Forbidden', 'oh no.')
} else {
var nextURL = new URL('', config.serverURL)
nextURL.search = new URLSearchParams({ next: req.originalUrl })
req.flash('error', 'You are not allowed to access this page. Maybe try logging in?')
res.redirect(config.serverURL + '/')
res.redirect(nextURL.toString())
}
}

View File

@ -41,7 +41,8 @@ exports.getConfig = (req, res) => {
allowedUploadMimeTypes: config.allowedUploadMimeTypes,
defaultUseHardbreak: config.defaultUseHardbreak,
linkifyHeaderStyle: config.linkifyHeaderStyle,
useCDN: config.useCDN
useCDN: config.useCDN,
defaultTocDepth: config.defaultTocDepth
}
res.set({
'Cache-Control': 'private', // only cache by client

866
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,7 @@
"coverage:ci": "nyc mocha --no-color -R dot --require intelli-espower-loader --exit --recursive ./test",
"test": "npm run-script lint && npm run-script jsonlint && npm run-script coverage",
"test:ci": "npm run-script lint && npm run-script jsonlint && npm run-script coverage:ci",
"postinstall": "bin/heroku"
"heroku-postbuild": "npm run build && ./bin/heroku"
},
"dependencies": {
"@aws-sdk/client-s3-node": "0.1.0-preview.2",
@ -105,7 +105,7 @@
"ws": "~7.1.1"
},
"devDependencies": {
"@hackmd/codemirror": "~5.49.8",
"@hackmd/codemirror": "~5.57.7",
"@hackmd/emojify.js": "^2.1.0",
"@hackmd/idle-js": "~1.0.1",
"@hackmd/js-sequence-diagrams": "~0.0.1-alpha.3",
@ -163,7 +163,8 @@
"markdown-it-ruby": "^0.1.1",
"markdown-it-sub": "~1.0.0",
"markdown-it-sup": "~1.0.0",
"markdownlint": "^0.17.0",
"markdownlint": "^0.22.0",
"markdownlint-rule-helpers": "^0.13.0",
"markmap-lib": "^0.4.2",
"mathjax": "~2.7.5",
"mermaid": "~8.6.4",
@ -173,7 +174,7 @@
"nyc": "~14.0.0",
"optimize-css-assets-webpack-plugin": "~5.0.0",
"papaparse": "^5.2.0",
"pdfobject": "~2.1.1",
"pdfobject": "~2.2.4",
"plantuml-encoder": "^1.2.5",
"power-assert": "~1.6.1",
"prismjs": "^1.17.1",

View File

@ -263,6 +263,32 @@
padding-right: 40px;
}
.ui-toc-dropdown .nav .nav>li>ul>li>ul>li>a {
padding-top: 1px;
padding-bottom: 1px;
padding-left: 50px;
font-size: 12px;
font-weight: 400;
}
.ui-toc-dropdown[dir='rtl'] .nav .nav>li>ul>li>ul>li>a {
padding-right: 50px;
}
.ui-toc-dropdown .nav .nav>li>ul>li>ul>li>ul>li>a {
padding-top: 1px;
padding-bottom: 1px;
padding-left: 60px;
font-size: 12px;
font-weight: 400;
}
.ui-toc-dropdown[dir='rtl'] .nav .nav>li>ul>li>ul>li>ul>li>a {
padding-right: 60px;
}
.ui-toc-dropdown .nav .nav>li>a:focus,.ui-toc-dropdown .nav .nav>li>a:hover {
padding-left: 29px;
}
@ -279,6 +305,22 @@
padding-right: 39px;
}
.ui-toc-dropdown .nav .nav>li>ul>li>ul>li>a:focus,.ui-toc-dropdown .nav .nav>li>ul>li>ul>li>a:hover {
padding-left: 49px;
}
.ui-toc-dropdown[dir='rtl'] .nav .nav>li>ul>li>ul>li>a:focus,.ui-toc-dropdown[dir='rtl'] .nav .nav>li>ul>li>ul>li>a:hover {
padding-right: 49px;
}
.ui-toc-dropdown .nav .nav>li>ul>li>ul>li>ul>li>a:focus,.ui-toc-dropdown .nav .nav>li>ul>li>ul>li>ul>li>a:hover {
padding-left: 59px;
}
.ui-toc-dropdown[dir='rtl'] .nav .nav>li>ul>li>ul>li>ul>li>a:focus,.ui-toc-dropdown[dir='rtl'] .nav .nav>li>ul>li>ul>li>ul>li>a:hover {
padding-right: 59px;
}
.ui-toc-dropdown .nav .nav>.active:focus>a,.ui-toc-dropdown .nav .nav>.active:hover>a,.ui-toc-dropdown .nav .nav>.active>a {
padding-left: 28px;
font-weight: 500;
@ -297,6 +339,24 @@
padding-right: 38px;
}
.ui-toc-dropdown .nav .nav>.active>.nav>.active>.nav>.active:focus>a,.ui-toc-dropdown .nav .nav>.active>.nav>.active>.nav>.active:hover>a,.ui-toc-dropdown .nav .nav>.active>.nav>.active>.nav>.active>a {
padding-left: 48px;
font-weight: 500;
}
.ui-toc-dropdown[dir='rtl'] .nav .nav>.active>.nav>.active>.nav>.active:focus>a,.ui-toc-dropdown[dir='rtl'] .nav .nav>.active>.nav>.active>.nav>.active:hover>a,.ui-toc-dropdown[dir='rtl'] .nav .nav>.active>.active>.nav>.nav>.active>a {
padding-right: 48px;
}
.ui-toc-dropdown .nav .nav>.active>.nav>.active>.nav>.active>.nav>.active:focus>a,.ui-toc-dropdown .nav .nav>.active>.nav>.active>.nav>.active>.nav>.active:hover>a,.ui-toc-dropdown .nav .nav>.active>.nav>.active>.nav>.active>.nav>.active>a {
padding-left: 58px;
font-weight: 500;
}
.ui-toc-dropdown[dir='rtl'] .nav .nav>.active>.nav>.active>.nav>.active>.nav>.active:focus>a,.ui-toc-dropdown[dir='rtl'] .nav .nav>.active>.nav>.active>.nav>.active>.nav>.active:hover>a,.ui-toc-dropdown[dir='rtl'] .nav .nav>.active>.active>.nav>.nav>.active>.nav>.active>a {
padding-right: 58px;
}
/* support japanese font */
.markdown-body[lang^="ja"] {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial, "Hiragino Kaku Gothic Pro", "ヒラギノ角ゴ Pro W3", Osaka, Meiryo, "メイリオ", "MS Gothic", " ゴシック", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";

View File

@ -82,7 +82,8 @@ View
## Table of Contents:
You can look at the bottom right section of the view area, there is a _ToC_ button <i class="fa fa-bars"></i>.
Pressing that button will show you a current _Table of Contents_, and will highlight which section you're at.
ToCs support up to **three header levels**.
ToCs support up to **five header levels**, the **default** is **set to three**. The maxLevel can be set for each note by using
[YAML Metadata](./yaml-metadata)
## Permalink
Every header will automatically add a permalink on the right side.
@ -133,12 +134,19 @@ You can provide advanced note information to set the browser behavior (visit abo
- GA: set to use Google Analytics
- disqus: set to use Disqus
- slideOptions: setup slide mode options
- toc: set options of the Table of Contents.
## ToC:
Use the syntax `[TOC]` to embed table of content into your note.
Use the syntax `[TOC]` to embed table of content into your note. By default, three header levels are displayed. This can also be specified by using [YAML Metadata](./yaml-metadata).
[TOC]
You can also specify the number of header levels by specifying the `maxLevel` like this: `[TOC maxLevel=1]`
[TOC maxLevel=1]
## Emoji
You can type any emoji like this :smile: :smiley: :cry: :wink:
> See full emoji list [here](http://www.emoji-cheat-sheet.com/).

View File

@ -1,6 +1,44 @@
Release Notes
===
<i class="fa fa-tag"></i> 2.4.0 Papilio maraho <i class="fa fa-clock-o"></i> 2021-05-11
---
<div style="text-align: center; margin-bottom: 1em;">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/32/Papilio_maraho_male_ventral_view_20160423.jpg/569px-Papilio_maraho_male_ventral_view_20160423.jpg" width="600">
<small style="display: block;">Papilio maraho</small>
</div>
> Papilio maraho is a species of butterfly in the family Papilionidae. It is endemic to Taiwan.
> \- Wikipedia [Papilio maraho](https://en.wikipedia.org/wiki/Papilio_maraho)
[Check out the complete release note][v2_4_0]. Thank you CodiMD community and all our contributors. ❤️
[v2_4_0]: https://hackmd.io/@codimd/release-notes/%2F%40codimd%2Fv2_4_0
## Enhancements
- Support autofix linter errors
- Support anonymous updates via API
- Support mediawiki export format in pandoc export
- Add some help strings to Prometheus metrics
- Allow more syntax highlight modes in editor
- Support TOC level customization
- Follow Google guidelines to use Google OAuth
## Fixes
- Vimeo won't show up due to the jsonp callback data unable be parsed with jQuery
- Fix slide mode stored XSS
- Enforce PG ssl require mode on heroku
- Webpack exclude path should support windows path
- Free url can read any md in file system
- Use encoded noteId when calling updateHistory
## Docs
- Add matrix badge and links to README [#1629](https://github.com/hackmdio/codimd/pull/1629) [@a-andreyev](https://github.com/a-andreyev)
<i class="fa fa-tag"></i> 2.3.1 Isoetes taiwanensis <i class="fa fa-clock-o"></i> 2021-01-04
---

View File

@ -139,6 +139,22 @@ This option allows you to enable Disqus with your shortname.
disqus: codimd
```
toc
---
This option allows you to set options regarding the table of contents (toc). Currently, its only option is to set the maxDepth.
**Notice: always use two spaces as indention in YAML metadata!**
> **maxDepth:**
> default: not set (whioch will show everything until level 3 (h1 -- h3))
> max: 5 (as defined by md-toc.js)
**Example**
type
---
This option allows you to switch the document view to the slide preview, to simplify live editing of presentations.

View File

@ -260,6 +260,23 @@ if (typeof window.mermaid !== 'undefined' && window.mermaid) {
}
}
function jsonp (url, callback) {
const callbackName = 'jsonp_callback_' + Math.round(1000000000 * Math.random())
window[callbackName] = function (data) {
delete window[callbackName]
document.body.removeChild(script)
callback(data)
}
const script = document.createElement('script')
script.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'callback=' + callbackName
document.body.appendChild(script)
script.onerror = function (e) {
console.error(e)
script.remove()
}
}
// dynamic event or object binding here
export function finishView (view) {
// todo list
@ -304,17 +321,11 @@ export function finishView (view) {
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()
}
jsonp(`//vimeo.com/api/v2/video/${$(value).attr('data-videoid')}.json`, function (data) {
const thumbnailSrc = data[0].thumbnail_large
const image = `<img src="${thumbnailSrc}" />`
$(value).prepend(image)
if (window.viewAjaxCallback) window.viewAjaxCallback()
})
})
// gist
@ -597,9 +608,11 @@ export function finishView (view) {
const url = $(value).attr('data-pdfurl')
const inner = $('<div></div>')
$(this).append(inner)
PDFObject.embed(url, inner, {
height: '400px'
})
setTimeout(() => {
PDFObject.embed(url, inner, {
height: '400px'
})
}, 1)
})
// syntax highlighting
view.find('code.raw').removeClass('raw')
@ -864,8 +877,12 @@ export function generateToc (id) {
const target = $(`#${id}`)
target.html('')
/* eslint-disable no-unused-vars */
var tocOptions = md.meta.toc || {}
var maxLevel = (typeof tocOptions.maxLevel === 'number' && tocOptions.maxLevel > 0) ? tocOptions.maxLevel : window.defaultTocDepth
var toc = new window.Toc('doc', {
level: 3,
level: maxLevel,
top: -1,
class: 'toc',
ulClass: 'nav',
@ -1063,11 +1080,20 @@ export function renderTOC (view) {
const target = $(`#${id}`)
target.html('')
/* eslint-disable no-unused-vars */
const specificDepth = parseInt(toc.data('toc-depth'))
var tocOptions = md.meta.toc || {}
var yamlMaxDepth = (typeof tocOptions.maxLevel === 'number' && tocOptions.maxLevel > 0) ? tocOptions.maxLevel : window.defaultTocDepth
var maxLevel = specificDepth || yamlMaxDepth
const TOC = new window.Toc('doc', {
level: 3,
level: maxLevel,
top: -1,
class: 'toc',
targetId: id,
data: { tocDepth: specificDepth },
process: getHeaderContent
})
/* eslint-enable no-unused-vars */
@ -1322,9 +1348,12 @@ const gistPlugin = new Plugin(
// TOC
const tocPlugin = new Plugin(
// regexp to match
/^\[TOC\]$/i,
/^\[TOC(|\s*maxLevel=\d+?)\]$/i,
(match, utils) => '<div class="toc"></div>'
(match, utils) => {
const tocDepth = match[1].split(/[?&=]+/)[1]
return `<div class="toc" data-toc-depth="${tocDepth}"></div>`
}
)
// slideshare
const slidesharePlugin = new Plugin(

View File

@ -13,3 +13,5 @@ window.linkifyHeaderStyle = '<%- linkifyHeaderStyle %>'
window.DROPBOX_APP_KEY = '<%- DROPBOX_APP_KEY %>'
window.USE_CDN = <%- useCDN %>
window.defaultTocDepth = <%- defaultTocDepth %>

View File

@ -1,9 +1,23 @@
# HELP online_notes Number of currently used notes
# TYPE online_notes gauge
online_notes <%- onlineNotes %>
# HELP online_users Number of online users
# TYPE online_users gauge
online_users <%- onlineUsers %>
# HELP distinct_online_users Number of distinct online users
# TYPE distinct_online_users gauge
distinct_online_users <%- distinctOnlineUsers %>
# HELP notes_count Total count of notes
# TYPE notes_count gauge
notes_count <%- notesCount %>
# HELP registered_users Number of registered users
# TYPE registered_users gauge
registered_users <%- registeredUsers %>
# HELP online_registered_users Number of online registered users
# TYPE online_registered_users gauge
online_registered_users <%- onlineRegisteredUsers %>
# HELP distinct_online_registered_users Number of distinct online registered users
# TYPE distinct_online_registered_users gauge
distinct_online_registered_users <%- distinctOnlineRegisteredUsers %>
is_connection_busy <%- isConnectionBusy ? 1 : 0 %>
connection_socket_queue_length <%- connectionSocketQueueLength %>

View File

@ -6,7 +6,7 @@ import * as utils from './utils'
import config from './config'
import statusBarTemplate from './statusbar.html'
import toolBarTemplate from './toolbar.html'
import './markdown-lint'
import { linterOptions } from './markdown-lint'
import CodeMirrorSpellChecker, { supportLanguages, supportLanguageCodes } from './spellcheck'
import { initTableEditor } from './table-editor'
import { availableThemes } from './constants'
@ -140,6 +140,42 @@ export default class Editor {
}
}
CodeMirror.defineMode('c', function (config, modeConfig) {
return CodeMirror.overlayMode(CodeMirror.getMode(config, 'text/x-csrc'), ignoreOverlay)
})
CodeMirror.defineMode('cpp', function (config, modeConfig) {
return CodeMirror.overlayMode(CodeMirror.getMode(config, 'text/x-c++src'), ignoreOverlay)
})
CodeMirror.defineMode('java', function (config, modeConfig) {
return CodeMirror.overlayMode(CodeMirror.getMode(config, 'text/x-java'), ignoreOverlay)
})
CodeMirror.defineMode('csharp', function (config, modeConfig) {
return CodeMirror.overlayMode(CodeMirror.getMode(config, 'text/x-csharp'), ignoreOverlay)
})
CodeMirror.defineMode('objectivec', function (config, modeConfig) {
return CodeMirror.overlayMode(CodeMirror.getMode(config, 'text/x-objectivec'), ignoreOverlay)
})
CodeMirror.defineMode('scala', function (config, modeConfig) {
return CodeMirror.overlayMode(CodeMirror.getMode(config, 'text/x-scala'), ignoreOverlay)
})
CodeMirror.defineMode('kotlin', function (config, modeConfig) {
return CodeMirror.overlayMode(CodeMirror.getMode(config, 'text/x-kotlin'), ignoreOverlay)
})
CodeMirror.defineMode('json', function (config, modeConfig) {
return CodeMirror.overlayMode(CodeMirror.getMode(config, 'application/json'), ignoreOverlay)
})
CodeMirror.defineMode('jsonld', function (config, modeConfig) {
return CodeMirror.overlayMode(CodeMirror.getMode(config, 'application/ld+json'), ignoreOverlay)
})
CodeMirror.defineMode('bash', function (config, modeConfig) {
return CodeMirror.overlayMode(CodeMirror.getMode(config, 'text/x-sh'), ignoreOverlay)
})
CodeMirror.defineMode('ocaml', function (config, modeConfig) {
return CodeMirror.overlayMode(CodeMirror.getMode(config, 'text/x-ocaml'), ignoreOverlay)
})
CodeMirror.defineMode('csvpreview', function (config, modeConfig) {
return CodeMirror.overlayMode(CodeMirror.getMode(config, 'csv'), ignoreOverlay)
})
CodeMirror.defineMode('vega', function (config, modeConfig) {
return CodeMirror.overlayMode(CodeMirror.getMode(config, 'application/ld+json'), ignoreOverlay)
})
@ -674,7 +710,7 @@ export default class Editor {
this.editor.setOption('gutters', gutters.filter(g => g !== lintGutter))
Cookies.remove('linter')
}
this.editor.setOption('lint', enable)
this.editor.setOption('lint', enable ? linterOptions : false)
}
setLinter () {
@ -685,7 +721,7 @@ export default class Editor {
}
linterToggle.click(() => {
const lintEnable = this.editor.getOption('lint')
const lintEnable = !!this.editor.getOption('lint')
this.toggleLinter.bind(this)(!lintEnable)
updateLinterStatus(!lintEnable)
})

View File

@ -3,6 +3,9 @@
// load CM lint plugin explicitly
import '@hackmd/codemirror/addon/lint/lint'
import '@hackmd/codemirror/addon/hint/show-hint.css'
import helpers from 'markdownlint-rule-helpers'
window.markdownit = require('markdown-it')
// eslint-disable-next-line
require('script-loader!markdownlint');
@ -26,10 +29,11 @@ require('script-loader!markdownlint');
}
return {
messageHTML: `${ruleNames.join('/')}: ${ruleDescription}`,
messageHTML: `${ruleNames.join('/')}: ${ruleDescription} <small>markdownlint(${ruleNames[0]})</small>`,
severity: 'error',
from: CodeMirror.Pos(lineNumber, start),
to: CodeMirror.Pos(lineNumber, end)
to: CodeMirror.Pos(lineNumber, end),
__error: error
}
})
}
@ -37,11 +41,84 @@ require('script-loader!markdownlint');
CodeMirror.registerHelper('lint', 'markdown', validator)
})
export const linterOptions = {
fixedTooltip: true,
contextmenu: annotations => {
const singleFixMenus = annotations
.map(annotation => {
const error = annotation.__error
const ruleNameAlias = error.ruleNames.join('/')
if (annotation.__error.fixInfo) {
return {
content: `Click to fix this violoation of ${ruleNameAlias}`,
onClick () {
const doc = window.editor.doc
const fixInfo = normalizeFixInfo(error.fixInfo, error.lineNumber)
const line = fixInfo.lineNumber - 1
const lineContent = doc.getLine(line) || ''
const fixedText = helpers.applyFix(lineContent, fixInfo, '\n')
let from = { line, ch: 0 }
let to = { line, ch: lineContent ? lineContent.length : 0 }
if (typeof fixedText === 'string') {
doc.replaceRange(fixedText, from, to)
} else {
if (fixInfo.lineNumber === 1) {
if (doc.lineCount() > 1) {
const nextLineStart = doc.indexFromPos({
line: to.line + 1,
ch: 0
})
to = doc.posFromIndex(nextLineStart)
}
} else {
const previousLineEnd = doc.indexFromPos(from) - 1
from = doc.posFromIndex(previousLineEnd)
}
doc.replaceRange('', from, to)
}
}
}
} else {
return {
content: `Click for more information about ${ruleNameAlias}`,
onClick () {
window.open(error.ruleInformation, '_blank')
}
}
}
})
return singleFixMenus
}
}
function lint (content) {
const { content: errors } = window.markdownlint.sync({
strings: {
content
}
},
resultVersion: 3
})
return errors
}
// Taken from https://github.com/DavidAnson/markdownlint/blob/2a9274ece586514ba3e2819cec3eb74312dc1b84/helpers/helpers.js#L611
/**
* Normalizes the fields of a RuleOnErrorFixInfo instance.
*
* @param {Object} fixInfo RuleOnErrorFixInfo instance.
* @param {number} [lineNumber] Line number.
* @returns {Object} Normalized RuleOnErrorFixInfo instance.
*/
function normalizeFixInfo (fixInfo, lineNumber) {
return {
lineNumber: fixInfo.lineNumber || lineNumber,
editColumn: fixInfo.editColumn || 1,
deleteCount: fixInfo.deleteCount || 0,
insertText: fixInfo.insertText || ''
}
}

View File

@ -103,7 +103,7 @@ import { md } from './extra'
// prevent script end tags in the content from interfering
// with parsing
content = content.replace(/<\/script>/g, SCRIPT_END_PLACEHOLDER)
content = content.replace(/<\/script>/gi, SCRIPT_END_PLACEHOLDER)
return '<script type="text/template">' + content + '</script>'
}

View File

@ -80,6 +80,8 @@ const defaultOptions = {
}
var options = meta.slideOptions || {}
// delete dependencies to avoid import user defined external resources
delete options.dependencies
if (Object.hasOwnProperty.call(options, 'spotlight')) {
defaultOptions.dependencies.push({

View File

@ -71,3 +71,11 @@
background-position: right bottom;
width: 100%; height: 100%;
}
.CodeMirror-hints {
background: #333;
}
.CodeMirror-hint {
color: white;
}

View File

@ -2,6 +2,8 @@
/**
* md-toc.js v1.0.2
* https://github.com/yijian166/md-toc.js
*
* Adapted to accept data attributes
*/
(function (window) {
@ -15,6 +17,7 @@
this.tocTop = parseInt(options.top) || 0
this.elChilds = this.el.children
this.process = options['process']
this.data = options.data || {}
if (!this.elChilds.length) return
this._init()
}
@ -123,6 +126,9 @@
this.toc = document.createElement('div')
this.toc.innerHTML = this.tocContent
this.toc.setAttribute('class', this.tocClass)
if (this.data.tocDepth) {
this.toc.dataset.tocDepth = this.data.tocDepth
}
if (!this.options.targetId) {
this.el.appendChild(this.toc)
} else {

View File

@ -77,7 +77,7 @@
</a>
</div>
<div class="col-md-4 inner">
<a href="<%- serverURL %>/features#Slide-Modee">
<a href="<%- serverURL %>/features#Slide-Mode">
<i class="fa fa-tv fa-3x"></i>
<h4><%= __('Support slide mode') %></h4>
</a>
@ -156,6 +156,7 @@
<h6 class="social-foot">
<%- __('Follow us on %s and %s.', '<a href="https://github.com/hackmdio/CodiMD" target="_blank" rel="noopener"><i class="fa fa-github"></i> GitHub</a>, <a href="https://gitter.im/hackmdio/hackmd" target="_blank" rel="noopener"><i class="fa fa-comments"></i> Gitter</a>', '<a href="https://poeditor.com/join/project/q0nuPWyztp" target="_blank" rel="noopener"><i class="fa fa-globe"></i> POEditor</a>') %>
</h6>
<% if(privacyPolicyURL && privacyPolicyURL.length > 0) { %><p><a href="<%- privacyPolicyURL %>"><%= __('Privacy Policy') %></a></p><% } %>
</div>
</div>
</div>

View File

@ -21,6 +21,7 @@
<option value="rtf">Rich Text Format (.rtf)</option>
<option value="textile">Textile</option>
<option value="docx">Word (.docx)</option>
<option value="mediawiki">Mediawiki</option>
</select>
</div>
<div class="modal-footer">

View File

@ -44,8 +44,8 @@
</a>
<% } %>
<% if (authProviders.google) { %>
<a href="<%- serverURL %>/auth/google" class="btn btn-lg btn-block btn-login-method btn-google">
<i class="fa fa-google"></i> <%= __('Sign in via %s', 'Google') %>
<a href="<%- serverURL %>/auth/google" class="btn btn-lg btn-block" style="color: #fff; background-color: #4285F4; vertical-align: middle; font-size: 14px; font-family: Roboto;">
<svg aria-hidden="true" width="34" height="34" viewBox="-8 -8 34 34" style="vertical-align: middle; margin-right: 16; background-color=#fff;"><path d="M16.51 8H8.98v3h4.3c-.18 1-.74 1.48-1.6 2.04v2.01h2.6a7.8 7.8 0 002.38-5.88c0-.57-.05-.66-.15-1.18z" fill="#4285F4"></path><path d="M8.98 17c2.16 0 3.97-.72 5.3-1.94l-2.6-2a4.8 4.8 0 01-7.18-2.54H1.83v2.07A8 8 0 008.98 17z" fill="#34A853"></path><path d="M4.5 10.52a4.8 4.8 0 010-3.04V5.41H1.83a8 8 0 000 7.18l2.67-2.07z" fill="#FBBC05"></path><path d="M8.98 4.18c1.17 0 2.23.4 3.06 1.2l2.3-2.3A8 8 0 001.83 5.4L4.5 7.49a4.77 4.77 0 014.48-3.3z" fill="#EA4335"></path></svg> <%= __('Sign in via %s', 'Google') %>
</a>
<% } %>
<% if (authProviders.saml) { %>

View File

@ -443,7 +443,10 @@ module.exports = {
}, {
test: /\.js$/,
use: [{ loader: 'babel-loader' }],
exclude: [/node_modules/, /public\/vendor/]
exclude: [
path.resolve(__dirname, 'node_modules'),
path.resolve(__dirname, 'public/vendor')
]
}, {
test: /\.css$/,
use: [
@ -522,7 +525,8 @@ module.exports = {
}]
},
node: {
fs: 'empty'
fs: 'empty',
os: 'empty'
},
stats: {