mirror of
https://github.com/status-im/codimd.git
synced 2025-02-28 10:10:29 +00:00
commit
2e468db210
@ -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
|
||||
|
@ -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
1
app.js
@ -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
|
||||
|
4
app.json
4
app.json
@ -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": [
|
||||
|
@ -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 = {}
|
||||
|
||||
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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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')
|
||||
}).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
|
||||
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)
|
||||
const noteInFS = readFileSystemNote(filePath)
|
||||
if (shouldSyncNote(note, noteInFS)) {
|
||||
const noteId = await syncNote(noteInFS, note)
|
||||
return callback(null, noteId)
|
||||
}
|
||||
} else {
|
||||
return callback(null, note.id)
|
||||
}
|
||||
} else {
|
||||
var filePath = path.join(config.docsPath, noteId + '.md')
|
||||
if (Note.checkFileExist(filePath)) {
|
||||
Note.create({
|
||||
// create new note with alias, and will sync md file in beforeCreateHook
|
||||
const note = await 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, note.id)
|
||||
}
|
||||
} 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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
864
package-lock.json
generated
864
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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", "MS ゴシック", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
|
@ -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/).
|
||||
|
@ -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
|
||||
---
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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) {
|
||||
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)
|
||||
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(
|
||||
|
@ -13,3 +13,5 @@ window.linkifyHeaderStyle = '<%- linkifyHeaderStyle %>'
|
||||
window.DROPBOX_APP_KEY = '<%- DROPBOX_APP_KEY %>'
|
||||
|
||||
window.USE_CDN = <%- useCDN %>
|
||||
|
||||
window.defaultTocDepth = <%- defaultTocDepth %>
|
||||
|
@ -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 %>
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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 || ''
|
||||
}
|
||||
}
|
||||
|
@ -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>'
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -71,3 +71,11 @@
|
||||
background-position: right bottom;
|
||||
width: 100%; height: 100%;
|
||||
}
|
||||
|
||||
.CodeMirror-hints {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.CodeMirror-hint {
|
||||
color: white;
|
||||
}
|
||||
|
6
public/vendor/md-toc.js
vendored
6
public/vendor/md-toc.js
vendored
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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) { %>
|
||||
|
@ -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: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user