From dbc73a73e2b4267afe92aeaf6381516fddf6f3f0 Mon Sep 17 00:00:00 2001 From: antelle Date: Sat, 14 Sep 2019 22:12:02 +0200 Subject: [PATCH] fix #713: markdown notes --- app/scripts/comp/kdbx-to-html.js | 18 ++++++++++++---- app/scripts/util/md-to-html.js | 23 +++++++++++++++++++++ app/scripts/views/details/details-view.js | 11 ++++++++++ app/scripts/views/fields/field-view-text.js | 9 +++++++- app/styles/areas/_details.scss | 13 ++++++++++++ app/templates/export/entry.hbs | 6 +++++- package-lock.json | 10 +++++++++ package.json | 2 ++ release-notes.md | 1 + webpack.config.js | 5 ++++- 10 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 app/scripts/util/md-to-html.js diff --git a/app/scripts/comp/kdbx-to-html.js b/app/scripts/comp/kdbx-to-html.js index e24acdd5..e9bc119c 100644 --- a/app/scripts/comp/kdbx-to-html.js +++ b/app/scripts/comp/kdbx-to-html.js @@ -1,8 +1,9 @@ +const kdbxweb = require('kdbxweb'); const Format = require('../util/format'); const Locale = require('../util/locale'); +const MdToHtml = require('../util/md-to-html'); const Links = require('../const/links'); const RuntimeInfo = require('./runtime-info'); -const kdbxweb = require('kdbxweb'); const Templates = { db: require('templates/export/db.hbs'), @@ -13,7 +14,7 @@ const FieldMapping = [ { name: 'UserName', locStr: 'user' }, { name: 'Password', locStr: 'password', protect: true }, { name: 'URL', locStr: 'website' }, - { name: 'Notes', locStr: 'notes' } + { name: 'Notes', locStr: 'notes', markdown: true } ]; const KnownFields = { 'Title': true }; @@ -38,12 +39,21 @@ function walkEntry(db, entry, parents) { const path = parents.map(group => group.name).join(' / '); const fields = []; for (const field of FieldMapping) { - const value = entryField(entry, field.name); + let value = entryField(entry, field.name); if (value) { + let html = false; + if (field.markdown) { + const converted = MdToHtml.convert(value); + if (converted !== value) { + value = converted; + html = true; + } + } fields.push({ title: Format.capFirst(Locale[field.locStr]), value, - protect: field.protect + protect: field.protect, + html }); } } diff --git a/app/scripts/util/md-to-html.js b/app/scripts/util/md-to-html.js new file mode 100644 index 00000000..bd7c73cc --- /dev/null +++ b/app/scripts/util/md-to-html.js @@ -0,0 +1,23 @@ +const marked = require('marked'); +const dompurify = require('dompurify'); + +const whiteSpaceRegex = /<\/?p>|
|\r|\n/g; + +const MdToHtml = { + convert(md) { + if (!md) { + return ''; + } + const html = marked(md); + const htmlWithoutLineBreaks = html.replace(whiteSpaceRegex, ''); + const mdWithoutLineBreaks = md.replace(whiteSpaceRegex, ''); + if (htmlWithoutLineBreaks === mdWithoutLineBreaks) { + return md; + } else { + const sanitized = dompurify.sanitize(html); + return `
${sanitized}
`; + } + } +}; + +module.exports = MdToHtml; diff --git a/app/scripts/views/details/details-view.js b/app/scripts/views/details/details-view.js index 33e6760c..28df37c3 100644 --- a/app/scripts/views/details/details-view.js +++ b/app/scripts/views/details/details-view.js @@ -233,6 +233,7 @@ const DetailsView = Backbone.View.extend({ name: '$Notes', title: Format.capFirst(Locale.notes), multiline: 'true', + markdown: true, value() { return model.notes; } @@ -1016,6 +1017,13 @@ const DetailsView = Backbone.View.extend({ } options.push({ value: 'det-add-new', icon: 'plus', text: Locale.detMenuAddNewField }); options.push({ value: 'det-clone', icon: 'clone', text: Locale.detClone }); + if (canCopy) { + options.push({ + value: 'copy-to-clipboard', + icon: 'copy', + text: Locale.detCopyEntryToClipboard + }); + } if (AutoType.enabled) { options.push({ value: 'det-auto-type', icon: 'keyboard-o', text: Locale.detAutoType }); } @@ -1039,6 +1047,9 @@ const DetailsView = Backbone.View.extend({ case 'det-auto-type': this.autoType(); break; + case 'copy-to-clipboard': + this.copyToClipboard(); + break; } }, diff --git a/app/scripts/views/fields/field-view-text.js b/app/scripts/views/fields/field-view-text.js index 86e2c739..90079cea 100644 --- a/app/scripts/views/fields/field-view-text.js +++ b/app/scripts/views/fields/field-view-text.js @@ -1,15 +1,22 @@ const Backbone = require('backbone'); +const kdbxweb = require('kdbxweb'); const FieldView = require('./field-view'); const GeneratorView = require('../generator-view'); const KeyHandler = require('../../comp/key-handler'); const Keys = require('../../const/keys'); const PasswordGenerator = require('../../util/password-generator'); const FeatureDetector = require('../../util/feature-detector'); -const kdbxweb = require('kdbxweb'); const Tip = require('../../util/tip'); +const MdToHtml = require('../../util/md-to-html'); const FieldViewText = FieldView.extend({ renderValue(value) { + if (this.model.markdown) { + if (value && value.isProtected) { + value = value.getText(); + } + return MdToHtml.convert(value); + } return value && value.isProtected ? PasswordGenerator.presentValueWithLineBreaks(value) : _.escape(value || '').replace(/\n/g, '
'); diff --git a/app/styles/areas/_details.scss b/app/styles/areas/_details.scss index c2f699de..4c9558f7 100644 --- a/app/styles/areas/_details.scss +++ b/app/styles/areas/_details.scss @@ -385,6 +385,19 @@ width: 100%; padding: 0 $base-padding-h; } + .markdown { + white-space: normal; + h6 { font-size: 1rem; } + h5 { font-size: modular-scale(1, 1rem, 1.05); } + h4 { font-size: modular-scale(2, 1rem, 1.05); } + h3 { font-size: modular-scale(3, 1rem, 1.05); } + h2 { font-size: modular-scale(4, 1rem, 1.05); } + h1 { font-size: $small-header-font-size; } + ul, ol { margin-bottom: 1em; } + ul { list-style-type: initial; } + ol { list-style-type: decimal; } + li { margin-left: 2em; } + } } &--no-select { diff --git a/app/templates/export/entry.hbs b/app/templates/export/entry.hbs index 64d805a6..51587221 100644 --- a/app/templates/export/entry.hbs +++ b/app/templates/export/entry.hbs @@ -9,7 +9,11 @@ {{#if field.protect}} {{field.value}} {{else}} - {{field.value}} + {{#if field.html}} + {{{field.value}}} + {{else}} + {{field.value}} + {{/if}} {{/if}} diff --git a/package-lock.json b/package-lock.json index a462149d..e73c8710 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3628,6 +3628,11 @@ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" }, + "dompurify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.0.0.tgz", + "integrity": "sha512-i8LWSIMDpGmv7AbOcQOyy54L4TrRhjs6yrSessoNeYspsAtgaKiiGeBAG5959qLfhGvyndkHeyZWxx0dd4iDxw==" + }, "domutils": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", @@ -7897,6 +7902,11 @@ "object-visit": "^1.0.0" } }, + "marked": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", + "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==" + }, "math-random": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", diff --git a/package.json b/package.json index a88c7999..95fda725 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "baron": "3.0.3", "base64-loader": "1.0.0", "bourbon": "^6.0.0", + "dompurify": "^2.0.0", "electron": "^6.0.9", "eslint": "^6.4.0", "eslint-config-prettier": "^6.3.0", @@ -57,6 +58,7 @@ "jsqrcode": "github:antelle/jsqrcode#0.1.3", "kdbxweb": "1.4.2", "load-grunt-tasks": "5.1.0", + "marked": "^0.7.0", "mini-css-extract-plugin": "^0.8.0", "node-sass": "^4.12.0", "node-stream-zip": "1.8.2", diff --git a/release-notes.md b/release-notes.md index 1202d443..d18ec410 100644 --- a/release-notes.md +++ b/release-notes.md @@ -8,6 +8,7 @@ Release notes `*` #502: increased the default value of encryption rounds `+` #348: configurable system-wide shortcuts `+` #743: copying entry fields to clipboard +`+` #713: markdown notes `*` devtools are now opened with alt-cmd-I `-` fix #764: multiple attachments display `-` fix multi-line fields display in history diff --git a/webpack.config.js b/webpack.config.js index aeaaa0e1..59de7c08 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -30,7 +30,8 @@ function config(grunt, mode = 'production') { 'pikaday', 'jsqrcode', 'argon2-wasm', - 'argon2' + 'argon2', + 'marked' ] }, output: { @@ -64,6 +65,8 @@ function config(grunt, mode = 'production') { baron: `baron/baron${devMode ? '.min' : ''}.js`, qrcode: `jsqrcode/dist/qrcode${devMode ? '.min' : ''}.js`, argon2: 'argon2-browser/dist/argon2.js', + marked: devMode ? 'marked/lib/marked.js' : 'marked/dist/marked.min.js', + dompurify: `dompurify/dist/purify${devMode ? '.min' : ''}.js`, hbs: 'handlebars/runtime.js', 'argon2-wasm': 'argon2-browser/dist/argon2.wasm', templates: path.join(__dirname, 'app/templates'),