Merge branch 'develop' into feature/configurable-break-style

This commit is contained in:
Yukai Huang 2019-11-01 10:27:47 +08:00
commit 75ee5ad255
No known key found for this signature in database
GPG Key ID: D4D3B2F0E99D4914
16 changed files with 172 additions and 58 deletions

View File

@ -3,7 +3,8 @@
"db": { "db": {
"dialect": "sqlite", "dialect": "sqlite",
"storage": ":memory:" "storage": ":memory:"
} },
"linkifyHeaderStyle": "gfm"
}, },
"development": { "development": {
"loglevel": "debug", "loglevel": "debug",
@ -13,7 +14,8 @@
"db": { "db": {
"dialect": "sqlite", "dialect": "sqlite",
"storage": "./db.codimd.sqlite" "storage": "./db.codimd.sqlite"
} },
"linkifyHeaderStyle": "gfm"
}, },
"production": { "production": {
"domain": "localhost", "domain": "localhost",
@ -127,6 +129,7 @@
"plantuml": "plantuml":
{ {
"server": "https://www.plantuml.com/plantuml" "server": "https://www.plantuml.com/plantuml"
} },
"linkifyHeaderStyle": "gfm"
} }
} }

View File

@ -18,6 +18,6 @@ RUN set -xe && \
FROM hackmdio/runtime:1.0.4 FROM hackmdio/runtime:1.0.4
USER hackmd USER hackmd
WORKDIR /home/hackmd/app WORKDIR /home/hackmd/app
COPY --from=BUILD /home/hackmd/app . COPY --chown=1500:1500 --from=BUILD /home/hackmd/app .
EXPOSE 3000 EXPOSE 3000
ENTRYPOINT ["/home/hackmd/app/docker-entrypoint.sh"] ENTRYPOINT ["/home/hackmd/app/docker-entrypoint.sh"]

View File

@ -104,6 +104,7 @@ module.exports = {
consumerSecret: undefined consumerSecret: undefined
}, },
github: { github: {
enterpriseURL: undefined, // if you use github.com, not need to specify
clientID: undefined, clientID: undefined,
clientSecret: undefined clientSecret: undefined
}, },
@ -163,5 +164,19 @@ module.exports = {
allowGravatar: true, allowGravatar: true,
allowPDFExport: true, allowPDFExport: true,
openID: false, openID: false,
defaultUseHardbreak: true defaultUseHardbreak: true,
// linkifyHeaderStyle - How is a header text converted into a link id.
// Header Example: "3.1. Good Morning my Friend! - Do you have 5$?"
// * 'keep-case' is the legacy CodiMD value.
// Generated id: "31-Good-Morning-my-Friend---Do-you-have-5"
// * 'lower-case' is the same like legacy (see above), but converted to lower-case.
// Generated id: "#31-good-morning-my-friend---do-you-have-5"
// * 'gfm' _GitHub-Flavored Markdown_ style as described here:
// https://gist.github.com/asabaylus/3071099#gistcomment-1593627
// It works like 'lower-case', but making sure the ID is unique.
// This is What GitHub, GitLab and (hopefully) most other tools use.
// Generated id: "31-good-morning-my-friend---do-you-have-5"
// 2nd appearance: "31-good-morning-my-friend---do-you-have-5-1"
// 3rd appearance: "31-good-morning-my-friend---do-you-have-5-2"
linkifyHeaderStyle: 'keep-case'
} }

View File

@ -66,6 +66,7 @@ module.exports = {
consumerSecret: process.env.CMD_TWITTER_CONSUMERSECRET consumerSecret: process.env.CMD_TWITTER_CONSUMERSECRET
}, },
github: { github: {
enterpriseURL: process.env.CMD_GITHUB_ENTERPRISE_URL,
clientID: process.env.CMD_GITHUB_CLIENTID, clientID: process.env.CMD_GITHUB_CLIENTID,
clientSecret: process.env.CMD_GITHUB_CLIENTSECRET clientSecret: process.env.CMD_GITHUB_CLIENTSECRET
}, },
@ -136,5 +137,6 @@ module.exports = {
allowGravatar: toBooleanConfig(process.env.CMD_ALLOW_GRAVATAR), allowGravatar: toBooleanConfig(process.env.CMD_ALLOW_GRAVATAR),
allowPDFExport: toBooleanConfig(process.env.CMD_ALLOW_PDF_EXPORT), allowPDFExport: toBooleanConfig(process.env.CMD_ALLOW_PDF_EXPORT),
openID: toBooleanConfig(process.env.CMD_OPENID), openID: toBooleanConfig(process.env.CMD_OPENID),
defaultUseHardbreak: toBooleanConfig(process.env.CMD_DEFAULT_USE_HARD_BREAK) defaultUseHardbreak: toBooleanConfig(process.env.CMD_DEFAULT_USE_HARD_BREAK),
linkifyHeaderStyle: process.env.CMD_LINKIFY_HEADER_STYLE
} }

View File

@ -200,6 +200,7 @@ config.sslCAPath.forEach(function (capath, i, array) {
array[i] = path.resolve(appRootPath, capath) array[i] = path.resolve(appRootPath, capath)
}) })
config.appRootPath = appRootPath
config.sslCertPath = path.resolve(appRootPath, config.sslCertPath) config.sslCertPath = path.resolve(appRootPath, config.sslCertPath)
config.sslKeyPath = path.resolve(appRootPath, config.sslKeyPath) config.sslKeyPath = path.resolve(appRootPath, config.sslKeyPath)
config.dhParamPath = path.resolve(appRootPath, config.dhParamPath) config.dhParamPath = path.resolve(appRootPath, config.dhParamPath)

View File

@ -103,7 +103,8 @@ module.exports = function (sequelize, DataTypes) {
else photo += '?size=bigger' else photo += '?size=bigger'
break break
case 'github': case 'github':
photo = 'https://avatars.githubusercontent.com/u/' + profile.id if (profile.photos && profile.photos[0]) photo = profile.photos[0].value.replace('?', '')
else photo = 'https://avatars.githubusercontent.com/u/' + profile.id
if (bigger) photo += '?s=400' if (bigger) photo += '?s=400'
else photo += '?s=96' else photo += '?s=96'
break break

View File

@ -316,17 +316,22 @@ function actionPDF (req, res, note) {
var content = extracted.markdown var content = extracted.markdown
var title = models.Note.decodeTitle(note.title) var title = models.Note.decodeTitle(note.title)
var highlightCssPath = path.join(config.appRootPath, '/node_modules/highlight.js/styles/github-gist.css')
if (!fs.existsSync(config.tmpPath)) { if (!fs.existsSync(config.tmpPath)) {
fs.mkdirSync(config.tmpPath) fs.mkdirSync(config.tmpPath)
} }
var path = config.tmpPath + '/' + Date.now() + '.pdf' var pdfPath = config.tmpPath + '/' + Date.now() + '.pdf'
content = content.replace(/\]\(\//g, '](' + url + '/') content = content.replace(/\]\(\//g, '](' + url + '/')
markdownpdf().from.string(content).to(path, function () { var markdownpdfOptions = {
if (!fs.existsSync(path)) { highlightCssPath: highlightCssPath
logger.error('PDF seems to not be generated as expected. File doesn\'t exist: ' + path) }
markdownpdf(markdownpdfOptions).from.string(content).to(pdfPath, function () {
if (!fs.existsSync(pdfPath)) {
logger.error('PDF seems to not be generated as expected. File doesn\'t exist: ' + pdfPath)
return errorInternalError(res) return errorInternalError(res)
} }
var stream = fs.createReadStream(path) var stream = fs.createReadStream(pdfPath)
var filename = title var filename = title
// Be careful of special characters // Be careful of special characters
filename = encodeURIComponent(filename) filename = encodeURIComponent(filename)
@ -336,7 +341,7 @@ function actionPDF (req, res, note) {
res.setHeader('Content-Type', 'application/pdf; charset=UTF-8') res.setHeader('Content-Type', 'application/pdf; charset=UTF-8')
res.setHeader('X-Robots-Tag', 'noindex, nofollow') // prevent crawling res.setHeader('X-Robots-Tag', 'noindex, nofollow') // prevent crawling
stream.pipe(res) stream.pipe(res)
fs.unlinkSync(path) fs.unlinkSync(pdfPath)
}) })
} }

View File

@ -6,13 +6,21 @@ const GithubStrategy = require('passport-github').Strategy
const config = require('../../../config') const config = require('../../../config')
const response = require('../../../response') const response = require('../../../response')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const { URL } = require('url')
const githubAuth = module.exports = Router() const githubAuth = module.exports = Router()
function githubUrl (path) {
return config.github.enterpriseURL && new URL(path, config.github.enterpriseURL).toString()
}
passport.use(new GithubStrategy({ passport.use(new GithubStrategy({
clientID: config.github.clientID, clientID: config.github.clientID,
clientSecret: config.github.clientSecret, clientSecret: config.github.clientSecret,
callbackURL: config.serverURL + '/auth/github/callback' callbackURL: config.serverURL + '/auth/github/callback',
authorizationURL: githubUrl('login/oauth/authorize'),
tokenURL: githubUrl('login/oauth/access_token'),
userProfileURL: githubUrl('api/v3/user')
}, passportGeneralCallback)) }, passportGeneralCallback))
githubAuth.get('/auth/github', function (req, res, next) { githubAuth.get('/auth/github', function (req, res, next) {

View File

@ -6,16 +6,24 @@ const GitlabStrategy = require('passport-gitlab2').Strategy
const config = require('../../../config') const config = require('../../../config')
const response = require('../../../response') const response = require('../../../response')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const HttpsProxyAgent = require('https-proxy-agent')
const gitlabAuth = module.exports = Router() const gitlabAuth = module.exports = Router()
passport.use(new GitlabStrategy({ const gitlabAuthStrategy = new GitlabStrategy({
baseURL: config.gitlab.baseURL, baseURL: config.gitlab.baseURL,
clientID: config.gitlab.clientID, clientID: config.gitlab.clientID,
clientSecret: config.gitlab.clientSecret, clientSecret: config.gitlab.clientSecret,
scope: config.gitlab.scope, scope: config.gitlab.scope,
callbackURL: config.serverURL + '/auth/gitlab/callback' callbackURL: config.serverURL + '/auth/gitlab/callback'
}, passportGeneralCallback)) }, passportGeneralCallback)
if (process.env['https_proxy']) {
const httpsProxyAgent = new HttpsProxyAgent(process.env['https_proxy'])
gitlabAuthStrategy._oauth2.setAgent(httpsProxyAgent)
}
passport.use(gitlabAuthStrategy)
gitlabAuth.get('/auth/gitlab', function (req, res, next) { gitlabAuth.get('/auth/gitlab', function (req, res, next) {
setReturnToFromReferer(req) setReturnToFromReferer(req)

View File

@ -100,7 +100,8 @@ statusRouter.get('/config', function (req, res) {
plantumlServer: config.plantuml.server, plantumlServer: config.plantuml.server,
DROPBOX_APP_KEY: config.dropbox.appKey, DROPBOX_APP_KEY: config.dropbox.appKey,
allowedUploadMimeTypes: config.allowedUploadMimeTypes, allowedUploadMimeTypes: config.allowedUploadMimeTypes,
defaultUseHardbreak: config.defaultUseHardbreak defaultUseHardbreak: config.defaultUseHardbreak,
linkifyHeaderStyle: config.linkifyHeaderStyle
} }
res.set({ res.set({
'Cache-Control': 'private', // only cache by client 'Cache-Control': 'private', // only cache by client

View File

@ -67,6 +67,7 @@
"handlebars": "~4.1.2", "handlebars": "~4.1.2",
"helmet": "~3.20.0", "helmet": "~3.20.0",
"highlight.js": "~9.15.9", "highlight.js": "~9.15.9",
"https-proxy-agent": "^3.0.1",
"i18n": "~0.8.3", "i18n": "~0.8.3",
"ionicons": "~2.0.1", "ionicons": "~2.0.1",
"isomorphic-fetch": "~2.2.1", "isomorphic-fetch": "~2.2.1",

View File

@ -168,11 +168,11 @@ export function renderTags (view) {
} }
function slugifyWithUTF8 (text) { function slugifyWithUTF8 (text) {
// remove html tags and trim spaces // remove HTML tags and trim spaces
let newText = stripTags(text.toString().trim()) let newText = stripTags(text.toString().trim())
// replace all spaces in between to dashes // replace space between words with dashes
newText = newText.replace(/\s+/g, '-') newText = newText.replace(/\s+/g, '-')
// slugify string to make it valid for attribute // slugify string to make it valid as an attribute
newText = newText.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '') newText = newText.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '')
return newText return newText
} }
@ -859,6 +859,36 @@ const anchorForId = id => {
return anchor return anchor
} }
const createHeaderId = (headerContent, headerIds = null) => {
// to escape characters not allow in css and humanize
const slug = slugifyWithUTF8(headerContent)
let id
if (window.linkifyHeaderStyle === 'keep-case') {
id = slug
} else if (window.linkifyHeaderStyle === 'lower-case') {
// to make compatible with GitHub, GitLab, Pandoc and many more
id = slug.toLowerCase()
} else if (window.linkifyHeaderStyle === 'gfm') {
// see GitHub implementation reference:
// https://gist.github.com/asabaylus/3071099#gistcomment-1593627
// it works like 'lower-case', but ...
const idBase = slug.toLowerCase()
id = idBase
if (headerIds !== null) {
// ... making sure the id is unique
let i = 1
while (headerIds.has(id)) {
id = idBase + '-' + i
i++
}
headerIds.add(id)
}
} else {
throw new Error('Unknown linkifyHeaderStyle value "' + window.linkifyHeaderStyle + '"')
}
return id
}
const linkifyAnchors = (level, containingElement) => { const linkifyAnchors = (level, containingElement) => {
const headers = containingElement.getElementsByTagName(`h${level}`) const headers = containingElement.getElementsByTagName(`h${level}`)
@ -866,9 +896,7 @@ const linkifyAnchors = (level, containingElement) => {
const header = headers[i] const header = headers[i]
if (header.getElementsByClassName('anchor').length === 0) { if (header.getElementsByClassName('anchor').length === 0) {
if (typeof header.id === 'undefined' || header.id === '') { if (typeof header.id === 'undefined' || header.id === '') {
// to escape characters not allow in css and humanize header.id = createHeaderId(getHeaderContent(header))
const id = slugifyWithUTF8(getHeaderContent(header))
header.id = id
} }
if (!(typeof header.id === 'undefined' || header.id === '')) { if (!(typeof header.id === 'undefined' || header.id === '')) {
header.insertBefore(anchorForId(header.id), header.firstChild) header.insertBefore(anchorForId(header.id), header.firstChild)
@ -894,8 +922,33 @@ function getHeaderContent (header) {
return headerHTML[0].innerHTML return headerHTML[0].innerHTML
} }
function changeHeaderId ($header, id, newId) {
$header.attr('id', newId)
const $headerLink = $header.find(`> a.anchor[href="#${id}"]`)
$headerLink.attr('href', `#${newId}`)
$headerLink.attr('title', newId)
}
export function deduplicatedHeaderId (view) { export function deduplicatedHeaderId (view) {
// headers contained in the last change
const headers = view.find(':header.raw').removeClass('raw').toArray() const headers = view.find(':header.raw').removeClass('raw').toArray()
if (headers.length === 0) {
return
}
if (window.linkifyHeaderStyle === 'gfm') {
// consistent with GitHub, GitLab, Pandoc & co.
// all headers contained in the document, in order of appearance
const allHeaders = view.find(`:header`).toArray()
// list of finaly assigned header IDs
const headerIds = new Set()
for (let j = 0; j < allHeaders.length; j++) {
const $header = $(allHeaders[j])
const id = $header.attr('id')
const newId = createHeaderId(getHeaderContent($header), headerIds)
changeHeaderId($header, id, newId)
}
} else {
// the legacy way
for (let i = 0; i < headers.length; i++) { for (let i = 0; i < headers.length; i++) {
const id = $(headers[i]).attr('id') const id = $(headers[i]).attr('id')
if (!id) continue if (!id) continue
@ -903,11 +956,9 @@ export function deduplicatedHeaderId (view) {
for (let j = 0; j < duplicatedHeaders.length; j++) { for (let j = 0; j < duplicatedHeaders.length; j++) {
if (duplicatedHeaders[j] !== headers[i]) { if (duplicatedHeaders[j] !== headers[i]) {
const newId = id + j const newId = id + j
const $duplicatedHeader = $(duplicatedHeaders[j]) const $header = $(duplicatedHeaders[j])
$duplicatedHeader.attr('id', newId) changeHeaderId($header, id, newId)
const $headerLink = $duplicatedHeader.find(`> a.anchor[href="#${id}"]`) }
$headerLink.attr('href', `#${newId}`)
$headerLink.attr('title', newId)
} }
} }
} }

View File

@ -3116,6 +3116,27 @@ function matchInContainer (text) {
} }
} }
const textCompleteKeyMap = {
Up: function () {
return false
},
Right: function () {
editor.doc.cm.execCommand('goCharRight')
},
Down: function () {
return false
},
Left: function () {
editor.doc.cm.execCommand('goCharLeft')
},
Enter: function () {
return false
},
Backspace: function () {
editor.doc.cm.execCommand('delCharBefore')
}
}
$(editor.getInputField()) $(editor.getInputField())
.textcomplete([ .textcomplete([
{ // emoji strategy { // emoji strategy
@ -3317,29 +3338,10 @@ $(editor.getInputField())
}, },
'textComplete:show': function (e) { 'textComplete:show': function (e) {
$(this).data('autocompleting', true) $(this).data('autocompleting', true)
editor.setOption('extraKeys', { editor.addKeyMap(textCompleteKeyMap)
Up: function () {
return false
},
Right: function () {
editor.doc.cm.execCommand('goCharRight')
},
Down: function () {
return false
},
Left: function () {
editor.doc.cm.execCommand('goCharLeft')
},
Enter: function () {
return false
},
Backspace: function () {
editor.doc.cm.execCommand('delCharBefore')
}
})
}, },
'textComplete:hide': function (e) { 'textComplete:hide': function (e) {
$(this).data('autocompleting', false) $(this).data('autocompleting', false)
editor.setOption('extraKeys', editorInstance.defaultExtraKeys) editor.removeKeyMap(textCompleteKeyMap)
} }
}) })

View File

@ -8,4 +8,6 @@ window.allowedUploadMimeTypes = <%- JSON.stringify(allowedUploadMimeTypes) %>
window.defaultUseHardbreak = <%- defaultUseHardbreak %> window.defaultUseHardbreak = <%- defaultUseHardbreak %>
window.linkifyHeaderStyle = '<%- linkifyHeaderStyle %>'
window.DROPBOX_APP_KEY = '<%- DROPBOX_APP_KEY %>' window.DROPBOX_APP_KEY = '<%- DROPBOX_APP_KEY %>'

View File

@ -166,19 +166,25 @@ export function initTableEditor (editor) {
'Alt-Shift-Ctrl-Down': () => { tableEditor.moveRow(1, opts) }, 'Alt-Shift-Ctrl-Down': () => { tableEditor.moveRow(1, opts) },
'Alt-Shift-Cmd-Down': () => { tableEditor.moveRow(1, opts) } 'Alt-Shift-Cmd-Down': () => { tableEditor.moveRow(1, opts) }
}) })
let lastActive
// enable keymap if the cursor is in a table // enable keymap if the cursor is in a table
function updateActiveState () { function updateActiveState () {
const tableTools = $('.toolbar .table-tools') const tableTools = $('.toolbar .table-tools')
const active = tableEditor.cursorIsInTable(opts) const active = tableEditor.cursorIsInTable(opts)
// avoid to update if state not changed
if (lastActive === active) {
return
}
if (active) { if (active) {
tableTools.show() tableTools.show()
tableTools.parent().scrollLeft(tableTools.parent()[0].scrollWidth) tableTools.parent().scrollLeft(tableTools.parent()[0].scrollWidth)
editor.setOption('extraKeys', keyMap) editor.addKeyMap(keyMap)
} else { } else {
tableTools.hide() tableTools.hide()
editor.setOption('extraKeys', null) editor.removeKeyMap(keyMap)
tableEditor.resetSmartCursor() tableEditor.resetSmartCursor()
} }
lastActive = active
} }
// event subscriptions // event subscriptions
editor.on('cursorActivity', () => { editor.on('cursorActivity', () => {

View File

@ -6449,6 +6449,14 @@ https-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
https-proxy-agent@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81"
integrity sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==
dependencies:
agent-base "^4.3.0"
debug "^3.1.0"
i18n@~0.8.3: i18n@~0.8.3:
version "0.8.3" version "0.8.3"
resolved "https://registry.yarnpkg.com/i18n/-/i18n-0.8.3.tgz#2d8cf1c24722602c2041d01ba6ae5eaa51388f0e" resolved "https://registry.yarnpkg.com/i18n/-/i18n-0.8.3.tgz#2d8cf1c24722602c2041d01ba6ae5eaa51388f0e"