summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.md8
-rw-r--r--app.js2
-rw-r--r--config.json.example2
-rw-r--r--lib/config/default.js4
-rw-r--r--lib/config/defaultSSL.js2
-rw-r--r--lib/config/dockerSecret.js3
-rw-r--r--lib/config/environment.js4
-rw-r--r--lib/config/index.js11
-rw-r--r--lib/migrations/20171009121200-longtext-for-mysql.js16
-rw-r--r--lib/models/note.js2
-rw-r--r--lib/models/revision.js10
-rw-r--r--lib/realtime.js8
-rw-r--r--lib/response.js2
-rw-r--r--locales/de.json6
-rw-r--r--locales/en.json4
-rw-r--r--locales/zh-CN.json3
-rw-r--r--locales/zh-TW.json3
-rw-r--r--package.json2
-rw-r--r--public/css/extra.css31
-rw-r--r--public/css/github-extract.css41
-rw-r--r--public/css/index.css74
-rw-r--r--public/css/markdown.css38
-rw-r--r--public/docs/features.md17
-rw-r--r--public/docs/release-notes.md55
-rw-r--r--public/js/extra.js9
-rw-r--r--public/js/index.js15
-rw-r--r--public/js/lib/appState.js3
-rw-r--r--public/js/lib/editor/ui-elements.js1
-rw-r--r--public/views/hackmd/body.ejs2
-rw-r--r--public/views/hackmd/header.ejs5
-rw-r--r--public/views/index/body.ejs3
-rw-r--r--public/views/shared/help-modal.ejs6
-rw-r--r--yarn.lock72
34 files changed, 408 insertions, 57 deletions
diff --git a/.gitignore b/.gitignore
index ab83c145..755e3f94 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
node_modules
+package-lock.json
composer.phar
composer.lock
.env.*.php
diff --git a/README.md b/README.md
index 798e17e7..1e7d79ab 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-HackMD
+HackMD Community Edition
===
[![Standard - JavaScript Style Guide][standardjs-image]][standardjs-url]
@@ -6,7 +6,7 @@ HackMD
[![Join the chat at https://gitter.im/hackmdio/hackmd][gitter-image]][gitter-url]
[![build status][travis-image]][travis-url]
[![version][github-version-badge]][github-release-page]
-
+[![Help Contribute to Open Source][codetriage-image]][codetriage-url]
HackMD lets you create realtime collaborative markdown notes on all platforms.
Inspired by Hackpad, with more focus on speed and flexibility.
@@ -143,6 +143,7 @@ There are some configs you need to change in the files below
| HMD_URL_ADDPORT | `true` or `false` | set to add port on callback url (port 80 or 443 won't applied) (only applied when domain is set) |
| HMD_USECDN | `true` or `false` | set to use CDN resources or not (default is `true`) |
| HMD_ALLOW_ANONYMOUS | `true` or `false` | set to allow anonymous usage (default is `true`) |
+| HMD_ALLOW_ANONYMOUS_EDITS | `true` or `false` | if `allowanonymous` is `true`: allow users to select `freely` permission, allowing guests to edit existing notes (default is `false`) |
| HMD_ALLOW_FREEURL | `true` or `false` | set to allow new note by accessing not exist note url |
| HMD_DEFAULT_PERMISSION | `freely`, `editable`, `limited`, `locked` or `private` | set notes default permission (only applied on signed users) |
| HMD_DB_URL | `mysql://localhost:3306/database` | set the db url |
@@ -212,6 +213,7 @@ There are some configs you need to change in the files below
| urladdport | `true` or `false` | set to add port on callback url (port 80 or 443 won't applied) (only applied when domain is set) |
| usecdn | `true` or `false` | set to use CDN resources or not (default is `true`) |
| allowanonymous | `true` or `false` | set to allow anonymous usage (default is `true`) |
+| allowanonymousedits | `true` or `false` | if `allowanonymous` is `true`: allow users to select `freely` permission, allowing guests to edit existing notes (default is `false`) |
| allowfreeurl | `true` or `false` | set to allow new note by accessing not exist note url |
| defaultpermission | `freely`, `editable`, `limited`, `locked`, `protected` or `private` | set notes default permission (only applied on signed users) |
| dburl | `mysql://localhost:3306/database` | set the db url, if set this variable then below db config won't be applied |
@@ -299,3 +301,5 @@ See more at [http://operational-transformation.github.io/](http://operational-tr
[github-release-page]: https://github.com/hackmdio/hackmd/releases
[standardjs-image]: https://cdn.rawgit.com/feross/standard/master/badge.svg
[standardjs-url]: https://github.com/feross/standard
+[codetriage-image]: https://www.codetriage.com/hackmdio/hackmd/badges/users.svg
+[codetriage-url]: https://www.codetriage.com/hackmdio/hackmd
diff --git a/app.js b/app.js
index c3f1fe8e..37e772bc 100644
--- a/app.js
+++ b/app.js
@@ -34,7 +34,7 @@ var data = {
version: config.version,
GOOGLE_API_KEY: config.google.clientSecret,
GOOGLE_CLIENT_ID: config.google.clientID,
- DROPBOX_APP_KEY: config.dropbox.clientSecret
+ DROPBOX_APP_KEY: config.dropbox.appKey
}
ejs.renderFile(constpath, data, {}, function (err, str) {
diff --git a/config.json.example b/config.json.example
index b243bf8d..c2c270c3 100644
--- a/config.json.example
+++ b/config.json.example
@@ -71,7 +71,7 @@
"searchBase": "change this",
"searchFilter": "change this",
"searchAttributes": ["change this"],
- "usernameField": "change this e.g. uid"
+ "usernameField": "change this e.g. uid",
"tlsOptions": {
"changeme": "See https://nodejs.org/api/tls.html#tls_tls_connect_options_callback"
}
diff --git a/lib/config/default.js b/lib/config/default.js
index 8d36db02..000c154a 100644
--- a/lib/config/default.js
+++ b/lib/config/default.js
@@ -16,6 +16,7 @@ module.exports = {
protocolusessl: false,
usecdn: true,
allowanonymous: true,
+ allowanonymousedits: false,
allowfreeurl: false,
defaultpermission: 'editable',
dburl: '',
@@ -81,7 +82,8 @@ module.exports = {
},
dropbox: {
clientID: undefined,
- clientSecret: undefined
+ clientSecret: undefined,
+ appKey: undefined
},
google: {
clientID: undefined,
diff --git a/lib/config/defaultSSL.js b/lib/config/defaultSSL.js
index 1f1d5590..362c62a1 100644
--- a/lib/config/defaultSSL.js
+++ b/lib/config/defaultSSL.js
@@ -12,6 +12,6 @@ function getFile (path) {
module.exports = {
sslkeypath: getFile('/run/secrets/key.pem'),
sslcertpath: getFile('/run/secrets/cert.pem'),
- sslcapath: getFile('/run/secrets/ca.pem'),
+ sslcapath: getFile('/run/secrets/ca.pem') !== undefined ? [getFile('/run/secrets/ca.pem')] : [],
dhparampath: getFile('/run/secrets/dhparam.pem')
}
diff --git a/lib/config/dockerSecret.js b/lib/config/dockerSecret.js
index ac54fd19..b9116cd3 100644
--- a/lib/config/dockerSecret.js
+++ b/lib/config/dockerSecret.js
@@ -44,7 +44,8 @@ if (fs.existsSync(basePath)) {
},
dropbox: {
clientID: getSecret('dropbox_clientID'),
- clientSecret: getSecret('dropbox_clientSecret')
+ clientSecret: getSecret('dropbox_clientSecret'),
+ appKey: getSecret('dropbox_appKey')
},
google: {
clientID: getSecret('google_clientID'),
diff --git a/lib/config/environment.js b/lib/config/environment.js
index 27e63591..eedd4913 100644
--- a/lib/config/environment.js
+++ b/lib/config/environment.js
@@ -18,6 +18,7 @@ module.exports = {
alloworigin: toArrayConfig(process.env.HMD_ALLOW_ORIGIN),
usecdn: toBooleanConfig(process.env.HMD_USECDN),
allowanonymous: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS),
+ allowanonymousedits: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS_EDITS),
allowfreeurl: toBooleanConfig(process.env.HMD_ALLOW_FREEURL),
defaultpermission: process.env.HMD_DEFAULT_PERMISSION,
dburl: process.env.HMD_DB_URL,
@@ -56,7 +57,8 @@ module.exports = {
},
dropbox: {
clientID: process.env.HMD_DROPBOX_CLIENTID,
- clientSecret: process.env.HMD_DROPBOX_CLIENTSECRET
+ clientSecret: process.env.HMD_DROPBOX_CLIENTSECRET,
+ appKey: process.env.HMD_DROPBOX_APPKEY
},
google: {
clientID: process.env.HMD_GOOGLE_CLIENTID,
diff --git a/lib/config/index.js b/lib/config/index.js
index 3ac3de53..3d22c3c3 100644
--- a/lib/config/index.js
+++ b/lib/config/index.js
@@ -13,8 +13,10 @@ const debugConfig = {
debug: (env === Environment.development)
}
+const {version} = require(path.join(appRootPath, 'package.json'))
+
const packageConfig = {
- version: '0.5.1',
+ version: version,
minimumCompatibleVersion: '0.5.0'
}
@@ -47,7 +49,7 @@ if (config.ldap.tlsca) {
// Permission
config.permission = Permission
-if (!config.allowanonymous) {
+if (!config.allowanonymous && !config.allowanonymousedits) {
delete config.permission.freely
}
if (!(config.defaultpermission in config.permission)) {
@@ -96,7 +98,10 @@ config.isSAMLEnable = config.saml.idpSsoUrl
config.isPDFExportEnable = config.allowpdfexport
// generate correct path
-config.sslcapath = path.join(appRootPath, config.sslcapath)
+config.sslcapath.forEach(function (capath, i, array) {
+ array[i] = path.resolve(appRootPath, capath)
+})
+
config.sslcertpath = path.join(appRootPath, config.sslcertpath)
config.sslkeypath = path.join(appRootPath, config.sslkeypath)
config.dhparampath = path.join(appRootPath, config.dhparampath)
diff --git a/lib/migrations/20171009121200-longtext-for-mysql.js b/lib/migrations/20171009121200-longtext-for-mysql.js
new file mode 100644
index 00000000..61b409ca
--- /dev/null
+++ b/lib/migrations/20171009121200-longtext-for-mysql.js
@@ -0,0 +1,16 @@
+'use strict'
+module.exports = {
+ up: function (queryInterface, Sequelize) {
+ queryInterface.changeColumn('Notes', 'content', {type: Sequelize.TEXT('long')})
+ queryInterface.changeColumn('Revisions', 'patch', {type: Sequelize.TEXT('long')})
+ queryInterface.changeColumn('Revisions', 'content', {type: Sequelize.TEXT('long')})
+ queryInterface.changeColumn('Revisions', 'latContent', {type: Sequelize.TEXT('long')})
+ },
+
+ down: function (queryInterface, Sequelize) {
+ queryInterface.changeColumn('Notes', 'content', {type: Sequelize.TEXT})
+ queryInterface.changeColumn('Revisions', 'patch', {type: Sequelize.TEXT})
+ queryInterface.changeColumn('Revisions', 'content', {type: Sequelize.TEXT})
+ queryInterface.changeColumn('Revisions', 'latContent', {type: Sequelize.TEXT})
+ }
+}
diff --git a/lib/models/note.js b/lib/models/note.js
index c0ef1374..33dde80d 100644
--- a/lib/models/note.js
+++ b/lib/models/note.js
@@ -60,7 +60,7 @@ module.exports = function (sequelize, DataTypes) {
}
},
content: {
- type: DataTypes.TEXT,
+ type: DataTypes.TEXT('long'),
get: function () {
return sequelize.processData(this.getDataValue('content'), '')
},
diff --git a/lib/models/revision.js b/lib/models/revision.js
index 225a95d4..170931b8 100644
--- a/lib/models/revision.js
+++ b/lib/models/revision.js
@@ -58,7 +58,7 @@ module.exports = function (sequelize, DataTypes) {
defaultValue: Sequelize.UUIDV4
},
patch: {
- type: DataTypes.TEXT,
+ type: DataTypes.TEXT('long'),
get: function () {
return sequelize.processData(this.getDataValue('patch'), '')
},
@@ -67,7 +67,7 @@ module.exports = function (sequelize, DataTypes) {
}
},
lastContent: {
- type: DataTypes.TEXT,
+ type: DataTypes.TEXT('long'),
get: function () {
return sequelize.processData(this.getDataValue('lastContent'), '')
},
@@ -76,7 +76,7 @@ module.exports = function (sequelize, DataTypes) {
}
},
content: {
- type: DataTypes.TEXT,
+ type: DataTypes.TEXT('long'),
get: function () {
return sequelize.processData(this.getDataValue('content'), '')
},
@@ -237,8 +237,8 @@ module.exports = function (sequelize, DataTypes) {
// if no revision available
Revision.create({
noteId: note.id,
- lastContent: note.content,
- length: note.content.length,
+ lastContent: note.content ? note.content : '',
+ length: note.content ? note.content.length : 0,
authorship: note.authorship
}).then(function (revision) {
Revision.finishSaveNoteRevision(note, revision, callback)
diff --git a/lib/realtime.js b/lib/realtime.js
index 361bbf09..c731e5b0 100644
--- a/lib/realtime.js
+++ b/lib/realtime.js
@@ -709,7 +709,7 @@ function connection (socket) {
return failConnection(404, 'note id not found', socket)
}
- if (isDuplicatedInSocketQueue(socket, connectionSocketQueue)) return
+ if (isDuplicatedInSocketQueue(connectionSocketQueue, socket)) return
// store noteId in this socket session
socket.noteId = noteId
@@ -723,8 +723,8 @@ function connection (socket) {
var maxrandomcount = 10
var found = false
do {
- Object.keys(notes[noteId].users).forEach(function (user) {
- if (user.color === color) {
+ Object.keys(notes[noteId].users).forEach(function (userId) {
+ if (notes[noteId].users[userId].color === color) {
found = true
}
})
@@ -781,7 +781,7 @@ function connection (socket) {
var note = notes[noteId]
// Only owner can change permission
if (note.owner && note.owner === socket.request.user.id) {
- if (permission === 'freely' && !config.allowanonymous) return
+ if (permission === 'freely' && !config.allowanonymous && !config.allowanonymousedits) return
note.permission = permission
models.Note.update({
permission: permission
diff --git a/lib/response.js b/lib/response.js
index 2e8924b7..1c04f9f6 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -60,6 +60,7 @@ function showIndex (req, res, next) {
url: config.serverurl,
useCDN: config.usecdn,
allowAnonymous: config.allowanonymous,
+ allowAnonymousEdits: config.allowanonymousedits,
facebook: config.isFacebookEnable,
twitter: config.isTwitterEnable,
github: config.isGitHubEnable,
@@ -93,6 +94,7 @@ function responseHackMD (res, note) {
title: title,
useCDN: config.usecdn,
allowAnonymous: config.allowanonymous,
+ allowAnonymousEdits: config.allowanonymousedits,
facebook: config.isFacebookEnable,
twitter: config.isTwitterEnable,
github: config.isGitHubEnable,
diff --git a/locales/de.json b/locales/de.json
index de76b590..73ffe0e6 100644
--- a/locales/de.json
+++ b/locales/de.json
@@ -29,6 +29,8 @@
"Import from browser": "Vom Browser importieren",
"Releases": "Versionen",
"Are you sure?": "Sind sie sicher?",
+ "Do you really want to delete this note?": "Möchten Sie diese Notiz wirklich löschen?",
+ "All users will lose their connection.": "Alle Benutzer werden getrennt.",
"Cancel": "Abbrechen",
"Yes, do it!": "Ja, mach es!",
"Choose method": "Methode wählen",
@@ -60,6 +62,7 @@
"Refresh": "Neu laden",
"Contacts": "Kontakte",
"Report an issue": "Fehlerbericht senden",
+ "Meet us on Gitter": "Triff uns auf Gitter",
"Send us email": "Kontakt",
"Documents": "Dokumente",
"Features": "Funktionen",
@@ -100,5 +103,6 @@
"Select From Available Snippets": "Aus verfügbaren Snippets wählen",
"OR": "Oder",
"Export to Snippet": "Zu Snippet exportieren",
- "Select Visibility Level": "Sichtbarkeit bestimmen"
+ "Select Visibility Level": "Sichtbarkeit bestimmen",
+ "Night Theme": "Nachtmodus"
}
diff --git a/locales/en.json b/locales/en.json
index 6b2a2066..e6a966d7 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -62,6 +62,7 @@
"Refresh": "Refresh",
"Contacts": "Contacts",
"Report an issue": "Report an issue",
+ "Meet us on Gitter": "Meet us on Gitter",
"Send us email": "Send us email",
"Documents": "Documents",
"Features": "Features",
@@ -102,5 +103,6 @@
"Select From Available Snippets": "Select From Available Snippets",
"OR": "OR",
"Export to Snippet": "Export to Snippet",
- "Select Visibility Level": "Select Visibility Level"
+ "Select Visibility Level": "Select Visibility Level",
+ "Night Theme": "Night Theme"
}
diff --git a/locales/zh-CN.json b/locales/zh-CN.json
index 97602c82..eeb01291 100644
--- a/locales/zh-CN.json
+++ b/locales/zh-CN.json
@@ -60,6 +60,7 @@
"Refresh": "重新整理",
"Contacts": "联络方式",
"Report an issue": "报告问题",
+ "Meet us on Gitter": "在 Gitter 上联系我们",
"Send us email": "寄信给我们",
"Documents": "文件",
"Features": "功能简介",
@@ -101,4 +102,4 @@
"OR": "或是",
"Export to Snippet": "导出到 Snippet",
"Select Visibility Level": "选择可见层级"
-} \ No newline at end of file
+}
diff --git a/locales/zh-TW.json b/locales/zh-TW.json
index a3bb7774..c55758ab 100644
--- a/locales/zh-TW.json
+++ b/locales/zh-TW.json
@@ -60,6 +60,7 @@
"Refresh": "重新整理",
"Contacts": "聯絡方式",
"Report an issue": "回報問題",
+ "Meet us on Gitter": "透過 Gitter 聯絡我們",
"Send us email": "寄信給我們",
"Documents": "文件",
"Features": "功能簡介",
@@ -101,4 +102,4 @@
"OR": "或是",
"Export to Snippet": "匯出到 Snippet",
"Select Visibility Level": "選擇可見層級"
-} \ No newline at end of file
+}
diff --git a/package.json b/package.json
index 43668883..bdb59635 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "hackmd",
- "version": "0.5.1",
+ "version": "1.0.1-ce",
"description": "Realtime collaborative markdown notes on all platforms.",
"main": "app.js",
"license": "AGPL-3.0",
diff --git a/public/css/extra.css b/public/css/extra.css
index 169a1a5a..1b132901 100644
--- a/public/css/extra.css
+++ b/public/css/extra.css
@@ -179,6 +179,11 @@
border-left: 1px solid black;
}
+.night .ui-toc-dropdown .nav>li>a:focus, .night .ui-toc-dropdown .nav>li>a:hover{
+ color: white;
+ border-left-color: white;
+}
+
.ui-toc-dropdown[dir='rtl'] .nav>li>a:focus,.ui-toc-dropdown[dir='rtl'] .nav>li>a:hover {
padding-right: 19px;
border-left: none;
@@ -192,6 +197,10 @@
background-color: transparent;
border-left: 2px solid black;
}
+.night .ui-toc-dropdown .nav>.active:focus>a,.night .ui-toc-dropdown .nav>.active:hover>a,.night .ui-toc-dropdown .nav>.active>a {
+ color: white;
+ border-left: 2px solid white;
+}
.ui-toc-dropdown[dir='rtl'] .nav>.active:focus>a,.ui-toc-dropdown[dir='rtl'] .nav>.active:hover>a,.ui-toc-dropdown[dir='rtl'] .nav>.active>a {
padding-right: 18px;
@@ -216,6 +225,10 @@
font-weight: 400;
}
+.night .ui-toc-dropdown .nav > li > a{
+ color: #aaa;
+}
+
.ui-toc-dropdown[dir='rtl'] .nav .nav>li>a {
padding-right: 30px;
}
@@ -350,13 +363,23 @@ small .dropdown a:focus, small .dropdown a:hover {
}
.unselectable {
- -moz-user-select: none;
- -khtml-user-select: none;
- -webkit-user-select: none;
+ -moz-user-select: none;
+ -khtml-user-select: none;
+ -webkit-user-select: none;
-o-user-select: none;
user-select: none;
}
+.night .navbar{
+ background: #333;
+ border-bottom-color: #333;
+ color: #eee;
+}
+
+.night .navbar a{
+ color: #eee;
+}
+
@media print {
div, table, img, pre, blockquote {
page-break-inside: avoid !important;
@@ -364,4 +387,4 @@ small .dropdown a:focus, small .dropdown a:hover {
a[href]:after {
font-size: 12px !important;
}
-} \ No newline at end of file
+}
diff --git a/public/css/github-extract.css b/public/css/github-extract.css
index 4d2650d4..7f7058a0 100644
--- a/public/css/github-extract.css
+++ b/public/css/github-extract.css
@@ -68,6 +68,9 @@
color: #777;
border-left: 0.25em solid #ddd;
}
+.night .markdown-body blockquote{
+ color: #bcbcbc;
+}
.markdown-body blockquote>:first-child {
margin-top: 0;
@@ -107,6 +110,15 @@
line-height: 1.25;
}
+.night .markdown-body h1,
+.night .markdown-body h2,
+.night .markdown-body h3,
+.night .markdown-body h4,
+.night .markdown-body h5,
+.night .markdown-body h6 {
+ color: #ddd;
+}
+
.markdown-body h1 .octicon-link,
.markdown-body h2 .octicon-link,
.markdown-body h3 .octicon-link,
@@ -118,6 +130,15 @@
visibility: hidden;
}
+.night .markdown-body h1 .octicon-link,
+.night .markdown-body h2 .octicon-link,
+.night .markdown-body h3 .octicon-link,
+.night .markdown-body h4 .octicon-link,
+.night .markdown-body h5 .octicon-link,
+.night .markdown-body h6 .octicon-link {
+ color: #fff;
+}
+
.markdown-body h1:hover .anchor,
.markdown-body h2:hover .anchor,
.markdown-body h3:hover .anchor,
@@ -180,6 +201,8 @@
color: #777
}
+
+
.markdown-body ul,
.markdown-body ol {
padding-left: 2em
@@ -246,11 +269,19 @@
background-color: #fff;
border-top: 1px solid #ccc;
}
+.night .markdown-body table tr {
+ background-color: #5f5f5f;
+}
.markdown-body table tr:nth-child(2n) {
background-color: #f8f8f8;
}
+.night .markdown-body table tr:nth-child(2n){
+
+ background-color: #4f4f4f;
+}
+
.markdown-body img {
max-width: 100%;
box-sizing: content-box;
@@ -370,6 +401,14 @@
border-radius: 3px;
}
+.night .markdown-body code,
+.night .markdown-body tt {
+
+ color: #eee;
+ background-color: rgba(230, 230, 230, 0.36);
+
+}
+
.markdown-body code::before,
.markdown-body code::after,
.markdown-body tt::before,
@@ -512,4 +551,4 @@
margin: 0.31em 0 0.2em -1.3em !important;
vertical-align: middle;
cursor: default !important;
-} \ No newline at end of file
+}
diff --git a/public/css/index.css b/public/css/index.css
index 8f483aa7..b00eba41 100644
--- a/public/css/index.css
+++ b/public/css/index.css
@@ -10,6 +10,16 @@ body {
padding-top: 51px;
/*overflow: hidden;*/
}
+
+.night a,
+.night .open-files-container li.selected a {
+ color: #5EB7E0;
+}
+
+body.night{
+ background: #333 !important;
+}
+
.CodeMirror {
font-family: "Source Code Pro", Consolas, monaco, monospace;
letter-spacing: 0.025em;
@@ -117,6 +127,11 @@ body {
margin-left: 0;
margin-right: 0;
}
+
+.night .ui-content{
+ background-color: #333;
+}
+
.ui-edit-area {
height: 100%;
/*padding-left: 15px;*/
@@ -144,6 +159,12 @@ body {
.ui-edit-area .ui-sync-toggle:active {
box-shadow: inset 0 3px 5px rgba(0,0,0,.125), 2px 0px 2px #e7e7e7;
}
+
+.night .ui-edit-area .ui-resizable-handle.ui-resizable-e{
+ background: #3c3c3c;
+ box-shadow: 3px 0px 6px #353535;
+}
+
.ui-view-area {
/*overflow-y: scroll;*/
-webkit-overflow-scrolling: touch;
@@ -154,6 +175,13 @@ body {
padding-right: 15px;
}
}
+
+.night .ui-view-area{
+ background: #333;
+ color: #ededed;
+}
+
+
.ui-scrollable {
height: 100%;
overflow-x: hidden;
@@ -238,12 +266,32 @@ body {
.navbar-nav > li > a {
cursor: pointer;
}
+
+.night .navbar-default .navbar-nav > li > a:focus,
+.night .navbar-default .navbar-nav > li > a:hover,
+.night .navbar-default .navbar-brand:focus,
+.night .navbar-default .navbar-brand:hover{
+ color: #fff;
+}
+
+.night .navbar-default .navbar-nav > .open > a,
+.night .navbar-default .navbar-nav > .open > a:focus,
+.night .navbar-default .navbar-nav > .open > a:hover {
+ color: white;
+ background: #000;
+
+}
.dropdown-menu > li > a {
cursor: pointer;
text-overflow: ellipsis;
max-width: calc(100vw - 30px);
overflow: hidden;
}
+
+.night .dropdown-menu{
+ background: #222;
+}
+
.dropdown-menu.CodeMirror-other-cursor {
transition: none;
}
@@ -276,8 +324,8 @@ div[contenteditable]:empty:not(:focus):before{
max-height: 40vh;
overflow: auto;
}
-.dropdown-menu.list::-webkit-scrollbar {
- display: none;
+.dropdown-menu.list::-webkit-scrollbar {
+ display: none;
}
.dropdown-menu .emoji {
margin-bottom: 0 !important;
@@ -292,6 +340,16 @@ div[contenteditable]:empty:not(:focus):before{
background: inherit;
}
+.night .navbar .btn-default{
+ background-color: #333;
+ border-color: #565656;
+ color: #eee;
+}
+
+.night .btn.btn-default.ui-view.active{
+ background: #202020;
+}
+
.btn-file {
position: relative;
overflow: hidden;
@@ -312,6 +370,12 @@ div[contenteditable]:empty:not(:focus):before{
display: block;
}
+.night .btn.focus,
+.night .btn:focus,
+.night .btn:hover{
+ color: #fff;
+}
+
.info-label {
width: 36%;
text-align: right;
@@ -481,8 +545,8 @@ div[contenteditable]:empty:not(:focus):before{
border: 1px solid #2893ef;
}
-.status-bar .indent-width-input::-webkit-inner-spin-button,
-.status-bar .indent-width-input::-webkit-outer-spin-button {
+.status-bar .indent-width-input::-webkit-inner-spin-button,
+.status-bar .indent-width-input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
@@ -524,4 +588,4 @@ div[contenteditable]:empty:not(:focus):before{
.CodeMirror {
height: auto !important;
}
-} \ No newline at end of file
+}
diff --git a/public/css/markdown.css b/public/css/markdown.css
index ad3a655f..6741729d 100644
--- a/public/css/markdown.css
+++ b/public/css/markdown.css
@@ -69,6 +69,12 @@
border-collapse: inherit !important;
}
+.night .markdown-body .gist table tr:nth-child(2n){
+
+ background-color: #ddd;
+
+}
+
.markdown-body code[data-gist-id] {
background: none;
padding: 0;
@@ -93,6 +99,7 @@
.markdown-body code[data-gist-id] table tr {
background: unset;
+
}
/*fixed style for rtl in pre and code*/
@@ -121,6 +128,16 @@
white-space: inherit;
}
+.night .markdown-body pre.graphviz .graph > polygon{
+ fill: #333;
+}
+
+.night .markdown-body pre.mermaid .titleText,
+.night .markdown-body pre.mermaid text,
+.night .markdown-body pre.mermaid .sectionTitle{
+ fill: white;
+}
+
.markdown-body pre.flow-chart > code,
.markdown-body pre.sequence-diagram > code,
.markdown-body pre.graphviz > code,
@@ -138,6 +155,27 @@
height: 100%;
}
+.night .markdown-body .abc path{
+ fill: #eee;
+}
+
+.night .markdown-body .abc path.note_selected{
+ fill: ##4DD0E1;
+}
+
+.night tspan{
+ fill: #fefefe;
+}
+
+.night pre rect{
+ fill: transparent;
+}
+
+.night pre.flow-chart rect,
+.night pre.flow-chart path{
+ stroke: white;
+}
+
.markdown-body pre > code.wrap {
white-space: pre-wrap; /* Since CSS 2.1 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
diff --git a/public/docs/features.md b/public/docs/features.md
index a894c087..01340fd7 100644
--- a/public/docs/features.md
+++ b/public/docs/features.md
@@ -3,13 +3,12 @@ Features
Introduction
===
-<i class="fa fa-file-text"></i> **HackMD** is a realtime, multiplatform collaborative markdown note editor.
+<i class="fa fa-file-text"></i> **HackMD** is a realtime, multi-platform collaborative markdown note editor.
This means that you can write notes with other people on your **desktop**, **tablet** or even on the **phone**.
-You can sign-in via **Facebook**, **Twitter**, **GitHub**, or **Dropbox** in the [_homepage_](/).
+You can sign-in via multiple auth providers like **Facebook**, **Twitter**, **GitHub** and many more on the [_homepage_](/).
-Note that this service is still in an early stage, and thus still has some [_issues_](https://github.com/hackmdio/hackmd/issues?q=is%3Aopen+is%3Aissue+label%3Abug).
-Please report new issues in [GitHub](https://github.com/hackmdio/hackmd/issues/new).
-If you need instant help, please send us a [Facebook message](https://www.messenger.com/t/hackmdio).
+If you experience any _issues_, feel free to report it on [**GitHub**](https://github.com/hackmdio/hackmd/issues).
+Or meet us on [**Gitter**](https://gitter.im/hackmdio/hackmd) for dev-talk and interactive help.
**Thank you very much!**
Workspace
@@ -137,7 +136,7 @@ alert(s);
function $initHighlight(block, cls) {
try {
if (cls.search(/\bno\-highlight\b/) != -1)
- return process(block, true, 0x0F) +
+ return process(block, true, 0x0F) +
' class=""';
} catch (e) {
/* handle exception */
@@ -157,7 +156,7 @@ alert(s);
function $initHighlight(block, cls) {
try {
if (cls.search(/\bno\-highlight\b/) != -1)
- return process(block, true, 0x0F) +
+ return process(block, true, 0x0F) +
' class=""';
} catch (e) {
/* handle exception */
@@ -259,7 +258,7 @@ cond(no)->op2
digraph hierarchy {
nodesep=1.0 // increases the separation between nodes
-
+
node [color=Red,fontname=Courier,shape=box] //All nodes will this shape and colour
edge [color=Blue, style=dashed] //All the lines look like this
@@ -386,7 +385,7 @@ Subscript: H~2~O
> Blockquotes can also be nested...
>> ...by using additional greater-than signs right next to each other...
-> > > ...or with spaces between arrows.
+> > > ...or with spaces between arrows.
### Lists
diff --git a/public/docs/release-notes.md b/public/docs/release-notes.md
index 2e0a71c6..70510b19 100644
--- a/public/docs/release-notes.md
+++ b/public/docs/release-notes.md
@@ -1,6 +1,59 @@
Release Notes
===
+<i class="fa fa-tag"></i> 1.0.1-ce <i class="fa fa-clock-o"></i> 2018-01-19 15:00
+---
+
+### Security
+* Fix Dropbox client secret leak
+
+### Enhancements
+* Improve version handling
+* It's 2018!
+
+### Fixes
+* Fix image alt-tag rendering
+* Fix Dropbox appkey
+
+<i class="fa fa-tag"></i> 1.0.0-ce <i class="fa fa-clock-o"></i> 2018-01-18 12:00
+---
+### License
+* Switch from MIT to AGPL
+
+### Enhancements
+* Improve language support
+* Allow themes for reveal
+* Add dark theme for editor and view
+* Add danish translation
+* Add simplified chinese translation
+* Provide new permission table
+* Make HSTS configurable
+* Make PDF export configurable
+* Add Mattermost auth support
+* Add SAML support
+
+### Fixes
+* Fix regex for speaker notes
+* Fix S3 endpoint support
+* Fix German translation
+* Fix English translation
+* Fix broken profile images
+* Fix XSS attacks
+* Fix history order
+* Fix missing boolean settings
+* Fix LDAP auth
+* Fix too long notes droping content
+* Fix mermaid compatiblity with new version
+* Fix SSL CA path parsing
+
+### Refactor
+* Refactor main page
+* Refactor status pages
+* Refactor config handling
+* Refactor auth backend
+* Refactor code styling
+* Refactor middleware to modules
+
<i class="fa fa-tag"></i> 0.5.1 `Doppio` <i class="fa fa-clock-o"></i> 2017-03-23 00:20
---
### Enhancements
@@ -636,4 +689,4 @@ Release Notes
+ Preview html
+ Realtime collaborate
+ Cross-platformed
-+ Recently used history \ No newline at end of file
++ Recently used history
diff --git a/public/js/extra.js b/public/js/extra.js
index 13b8924c..ec7d39da 100644
--- a/public/js/extra.js
+++ b/public/js/extra.js
@@ -156,7 +156,11 @@ export function renderTags (view) {
}
function slugifyWithUTF8 (text) {
- let newText = S(text.toLowerCase()).trim().stripTags().dasherize().s
+ // remove html tags and trim spaces
+ let newText = S(text).trim().stripTags().s
+ // replace all spaces in between to dashes
+ newText = newText.replace(/\s+/g, '-')
+ // slugify string to make it valid for attribute
newText = newText.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '')
return newText
}
@@ -1003,9 +1007,10 @@ md.use(markdownitContainer, 'info', { render: renderContainer })
md.use(markdownitContainer, 'warning', { render: renderContainer })
md.use(markdownitContainer, 'danger', { render: renderContainer })
+let defaultImageRender = md.renderer.rules.image
md.renderer.rules.image = function (tokens, idx, options, env, self) {
tokens[idx].attrJoin('class', 'raw')
- return self.renderToken(...arguments)
+ return defaultImageRender(...arguments)
}
md.renderer.rules.list_item_open = function (tokens, idx, options, env, self) {
tokens[idx].attrJoin('class', 'raw')
diff --git a/public/js/index.js b/public/js/index.js
index b336af90..5ff716fd 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -1633,6 +1633,10 @@ ui.toolbar.view.click(function () {
ui.toolbar.both.click(function () {
changeMode(modeType.both)
})
+
+ui.toolbar.night.click(function () {
+ toggleNightMode()
+})
// permission
// freely
ui.infobar.permission.freely.click(function () {
@@ -1666,6 +1670,17 @@ $('.ui-delete-modal-confirm').click(function () {
socket.emit('delete')
})
+function toggleNightMode () {
+ var $body = $('body')
+ var isActive = ui.toolbar.night.hasClass('active')
+ if (isActive) {
+ $body.removeClass('night')
+ appState.nightMode = false
+ } else {
+ $body.addClass('night')
+ appState.nightMode = true
+ }
+}
function emitPermission (_permission) {
if (_permission !== permission) {
socket.emit('permission', _permission)
diff --git a/public/js/lib/appState.js b/public/js/lib/appState.js
index fb8030e1..87aaf737 100644
--- a/public/js/lib/appState.js
+++ b/public/js/lib/appState.js
@@ -2,7 +2,8 @@ import modeType from './modeType'
let state = {
syncscroll: true,
- currentMode: modeType.view
+ currentMode: modeType.view,
+ nightMode: false
}
export default state
diff --git a/public/js/lib/editor/ui-elements.js b/public/js/lib/editor/ui-elements.js
index 0d330d77..88a1e3ca 100644
--- a/public/js/lib/editor/ui-elements.js
+++ b/public/js/lib/editor/ui-elements.js
@@ -37,6 +37,7 @@ export const getUIElements = () => ({
edit: $('.ui-edit'),
view: $('.ui-view'),
both: $('.ui-both'),
+ night: $('.ui-night'),
uploadImage: $('.ui-upload-image')
},
infobar: {
diff --git a/public/views/hackmd/body.ejs b/public/views/hackmd/body.ejs
index 91343ef6..49604379 100644
--- a/public/views/hackmd/body.ejs
+++ b/public/views/hackmd/body.ejs
@@ -15,7 +15,7 @@
<a id="permissionLabel" class="ui-permission-label text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
</a>
<ul class="dropdown-menu" aria-labelledby="permissionLabel">
- <li class="ui-permission-freely"<% if(!allowAnonymous) { %> style="display: none;"<% } %>><a><i class="fa fa-leaf fa-fw"></i> Freely - Anyone can edit</a></li>
+ <li class="ui-permission-freely"<% if(!allowAnonymous && !allowAnonymousEdits) { %> style="display: none;"<% } %>><a><i class="fa fa-leaf fa-fw"></i> Freely - Anyone can edit</a></li>
<li class="ui-permission-editable"><a><i class="fa fa-shield fa-fw"></i> Editable - Signed-in people can edit</a></li>
<li class="ui-permission-limited"><a><i class="fa fa-id-card fa-fw"></i> Limited - Signed-in people can edit (forbid guests)</a></li>
<li class="ui-permission-locked"><a><i class="fa fa-lock fa-fw"></i> Locked - Only owner can edit</a></li>
diff --git a/public/views/hackmd/header.ejs b/public/views/hackmd/header.ejs
index 80df2c77..b87f21fa 100644
--- a/public/views/hackmd/header.ejs
+++ b/public/views/hackmd/header.ejs
@@ -96,6 +96,11 @@
<input type="radio" name="mode" autocomplete="off"><i class="fa fa-pencil"></i>
</label>
</div>
+ <div class="btn-group" data-toggle="buttons">
+ <label class="btn ui-night" title="<%= __('Night Theme') %>">
+ <input type="checkbox" name="night"><i class="fa fa-moon-o"></i>
+ </label>
+ </div>
<span class="btn btn-link btn-file ui-help" title="<%= __('Help') %>" data-toggle="modal" data-target=".help-modal">
<i class="fa fa-question-circle"></i>
</span>
diff --git a/public/views/index/body.ejs b/public/views/index/body.ejs
index d7b4458e..82d83f02 100644
--- a/public/views/index/body.ejs
+++ b/public/views/index/body.ejs
@@ -39,6 +39,7 @@
<div id="home" class="section"<% if(signin) { %> style="display:none;"<% } %>>
<div class="inner cover">
<h1 class="cover-heading"><i class="fa fa-file-text"></i> HackMD</h1>
+ <p class="lead"><strong>Community Edition</strong></p>
<p class="lead">
<%= __('Best way to write and share your knowledge in markdown.') %>
</p>
@@ -126,7 +127,7 @@
<iframe src="//ghbtns.com/github-btn.html?user=hackmdio&repo=hackmd&type=star&count=true" frameborder="0" scrolling="0" width="104px" height="20px"></iframe>
</h6>
<p>
- &copy; 2017 <a href="https://www.facebook.com/hackmdio" target="_blank"><i class="fa fa-facebook-square"></i> HackMD</a> | <a href="<%- url %>/s/release-notes" target="_blank"><%= __('Releases') %></a>
+ &copy; 2018 <a href="https://www.facebook.com/hackmdio" target="_blank"><i class="fa fa-facebook-square"></i> HackMD</a> | <a href="<%- url %>/s/release-notes" target="_blank"><%= __('Releases') %></a>
</p>
<select class="ui-locale">
<option value="en">English</option>
diff --git a/public/views/shared/help-modal.ejs b/public/views/shared/help-modal.ejs
index b1ea681d..f5dc55c2 100644
--- a/public/views/shared/help-modal.ejs
+++ b/public/views/shared/help-modal.ejs
@@ -15,9 +15,9 @@
<h3 class="panel-title"><%= __('Contacts') %></h3>
</div>
<div class="panel-body">
- <a href="https://github.com/hackmdio/hackmd/issues" target="_blank"><i class="fa fa-tag fa-fw"></i> <%= __('Report an issue') %></a>
+ <a href="https://github.com/hackmdio/hackmd/issues" target="_blank"><i class="fa fa-tag fa-fw"></i> <%= __('Report an issue') %></a>
<br>
- <a href="mailto:hackmdio@gmail.com"><i class="fa fa-envelope fa-fw"></i> <%= __('Send us email') %></a>
+ <a href="https://gitter.im/hackmdio/hackmd" target="_blank"><i class="fa fa-comments fa-fw"></i> <%= __('Meet us on Gitter') %></a>
</div>
</div>
<div class="panel panel-default">
@@ -144,4 +144,4 @@
letter-spacing: 0.025em;
line-height: 1.25;
}
-</style> \ No newline at end of file
+</style>
diff --git a/yarn.lock b/yarn.lock
index d80251f5..b9f9e9d7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -282,6 +282,12 @@ async@^2.1.4:
dependencies:
lodash "^4.14.0"
+async@^2.1.5:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
+ dependencies:
+ lodash "^4.14.0"
+
async@~0.2.6:
version "0.2.10"
resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
@@ -1972,7 +1978,7 @@ ejs-loader@^0.3.0:
loader-utils "^0.2.7"
lodash "^3.6.0"
-ejs@^2.5.5:
+ejs@^2.5.5, ejs@^2.5.6:
version "2.5.7"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a"
@@ -4734,6 +4740,10 @@ node-dir@^0.1.10:
dependencies:
minimatch "^3.0.2"
+node-forge@^0.7.0:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300"
+
node-libs-browser@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-0.7.0.tgz#3e272c0819e308935e26674408d7af0e1491b83b"
@@ -5183,7 +5193,19 @@ passport-oauth@^1.0.0:
passport-oauth1 "1.x.x"
passport-oauth2 "1.x.x"
-passport-strategy@1.x.x:
+passport-saml@^0.31.0:
+ version "0.31.0"
+ resolved "https://registry.yarnpkg.com/passport-saml/-/passport-saml-0.31.0.tgz#e4d654cab30f018bfd39056efe7bcfa770aab463"
+ dependencies:
+ passport-strategy "*"
+ q "^1.5.0"
+ xml-crypto "^0.10.1"
+ xml-encryption "^0.11.0"
+ xml2js "0.4.x"
+ xmlbuilder "^9.0.4"
+ xmldom "0.1.x"
+
+passport-strategy@*, passport-strategy@1.x.x:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
@@ -5748,6 +5770,10 @@ q@^1.0.1, q@^1.1.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"
+q@^1.5.0:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
+
qs@2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-2.3.3.tgz#e9e85adbe75da0bbe4c8e0476a086290f863b404"
@@ -7565,6 +7591,23 @@ xml-char-classes@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/xml-char-classes/-/xml-char-classes-1.0.0.tgz#64657848a20ffc5df583a42ad8a277b4512bbc4d"
+xml-crypto@^0.10.1:
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/xml-crypto/-/xml-crypto-0.10.1.tgz#f832f74ccf56f24afcae1163a1fcab44d96774a8"
+ dependencies:
+ xmldom "=0.1.19"
+ xpath.js ">=0.0.3"
+
+xml-encryption@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/xml-encryption/-/xml-encryption-0.11.0.tgz#458c2cb7d0300ff62d304c74eb3ded08ca97456b"
+ dependencies:
+ async "^2.1.5"
+ ejs "^2.5.6"
+ node-forge "^0.7.0"
+ xmldom "~0.1.15"
+ xpath "0.0.24"
+
xml-name-validator@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
@@ -7576,16 +7619,31 @@ xml2js@0.4.17:
sax ">=0.6.0"
xmlbuilder "^4.1.0"
+xml2js@0.4.x:
+ version "0.4.19"
+ resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
+ dependencies:
+ sax ">=0.6.0"
+ xmlbuilder "~9.0.1"
+
xmlbuilder@4.2.1, xmlbuilder@^4.1.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.2.1.tgz#aa58a3041a066f90eaa16c2f5389ff19f3f461a5"
dependencies:
lodash "^4.0.0"
-xmldom@0.1.x:
+xmlbuilder@^9.0.4, xmlbuilder@~9.0.1:
+ version "9.0.4"
+ resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f"
+
+xmldom@0.1.x, xmldom@~0.1.15:
version "0.1.27"
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
+xmldom@=0.1.19:
+ version "0.1.19"
+ resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc"
+
xmlhttprequest-ssl@1.5.3:
version "1.5.3"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d"
@@ -7594,6 +7652,14 @@ xmlhttprequest@>=1.5.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
+xpath.js@>=0.0.3:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1"
+
+xpath@0.0.24:
+ version "0.0.24"
+ resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.24.tgz#1ade162e1cc523c8d39fc7d06afc16ea216f29fb"
+
xregexp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"