From 83da07a9417e9c7eb3db3959b798c37f1a7dbd83 Mon Sep 17 00:00:00 2001 From: Nikolay Lopin Date: Sun, 17 Dec 2017 21:39:34 +0300 Subject: [PATCH 1/5] Export note with local images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Looks through the note and searches for local images. Copies them to ‘images’ folder in the export path and replaces affected links. #1261 --- browser/components/MarkdownPreview.js | 32 ----------- browser/main/Detail/MarkdownNoteDetail.js | 39 +++++++++++++ browser/main/lib/dataApi/exportImage.js | 37 +++++++++++++ browser/main/lib/dataApi/exportNote.js | 67 +++++++++++++++++++++++ 4 files changed, 143 insertions(+), 32 deletions(-) mode change 100644 => 100755 browser/components/MarkdownPreview.js mode change 100644 => 100755 browser/main/Detail/MarkdownNoteDetail.js create mode 100755 browser/main/lib/dataApi/exportImage.js create mode 100755 browser/main/lib/dataApi/exportNote.js diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js old mode 100644 new mode 100755 index a3e7bb93..c2693312 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -8,7 +8,6 @@ import Raphael from 'raphael' import flowchart from 'flowchart' import SequenceDiagram from 'js-sequence-diagrams' import eventEmitter from 'browser/main/lib/eventEmitter' -import fs from 'fs' import htmlTextHelper from 'browser/lib/htmlTextHelper' import copy from 'copy-to-clipboard' import mdurl from 'mdurl' @@ -116,8 +115,6 @@ export default class MarkdownPreview extends React.Component { this.mouseUpHandler = (e) => this.handleMouseUp(e) this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e) this.checkboxClickHandler = (e) => this.handleCheckboxClick(e) - this.saveAsTextHandler = () => this.handleSaveAsText() - this.saveAsMdHandler = () => this.handleSaveAsMd() this.printHandler = () => this.handlePrint() this.linkClickHandler = this.handlelinkClick.bind(this) @@ -165,35 +162,10 @@ export default class MarkdownPreview extends React.Component { if (this.props.onMouseUp != null) this.props.onMouseUp(e) } - handleSaveAsText () { - this.exportAsDocument('txt') - } - - handleSaveAsMd () { - this.exportAsDocument('md') - } - handlePrint () { this.refs.root.contentWindow.print() } - exportAsDocument (fileType) { - const options = { - filters: [ - { name: 'Documents', extensions: [fileType] } - ], - properties: ['openFile', 'createDirectory'] - } - dialog.showSaveDialog(remote.getCurrentWindow(), options, - (filename) => { - if (filename) { - fs.writeFile(filename, this.props.value, (err) => { - if (err) throw err - }) - } - }) - } - fixDecodedURI (node) { if (node && node.children.length === 1 && typeof node.children[0] === 'string') { const { innerText, href } = node @@ -221,8 +193,6 @@ export default class MarkdownPreview extends React.Component { this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler) this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler) this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler) - eventEmitter.on('export:save-text', this.saveAsTextHandler) - eventEmitter.on('export:save-md', this.saveAsMdHandler) eventEmitter.on('print', this.printHandler) } @@ -232,8 +202,6 @@ export default class MarkdownPreview extends React.Component { this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler) this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler) this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler) - eventEmitter.off('export:save-text', this.saveAsTextHandler) - eventEmitter.off('export:save-md', this.saveAsMdHandler) eventEmitter.off('print', this.printHandler) } diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js old mode 100644 new mode 100755 index 25c993d0..956efb63 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -23,6 +23,7 @@ import InfoPanelTrashed from './InfoPanelTrashed' import { formatDate } from 'browser/lib/date-formatter' import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus' import striptags from 'striptags' +import exportNote from 'browser/main/lib/dataApi/exportNote' const electron = require('electron') const { remote } = electron @@ -44,6 +45,8 @@ class MarkdownNoteDetail extends React.Component { this.dispatchTimer = null this.toggleLockButton = this.handleToggleLockButton.bind(this) + this.saveAsText = this.handleSaveAsText.bind(this) + this.saveAsMd = this.handleSaveAsMd.bind(this) } focus () { @@ -52,6 +55,8 @@ class MarkdownNoteDetail extends React.Component { componentDidMount () { ee.on('topbar:togglelockbutton', this.toggleLockButton) + ee.on('export:save-text', this.saveAsText) + ee.on('export:save-md', this.saveAsMd) } componentWillReceiveProps (nextProps) { @@ -72,6 +77,8 @@ class MarkdownNoteDetail extends React.Component { componentDidUnmount () { ee.off('topbar:togglelockbutton', this.toggleLockButton) + ee.off('export:save-text', this.saveAsTextHandler) + ee.off('export:save-md', this.saveAsMdHandler) } handleChange (e) { @@ -170,6 +177,30 @@ class MarkdownNoteDetail extends React.Component { ee.emit('export:save-text') } + exportAsDocument (fileType) { + const options = { + filters: [ + { name: 'Documents', extensions: [fileType] } + ], + properties: ['openFile', 'createDirectory'] + } + + dialog.showSaveDialog(remote.getCurrentWindow(), options, + (filename) => { + if (filename) { + const note = this.props.note + + exportNote(note.storage, note.content, filename) + .then((res) => { + dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`}) + }).catch((err) => { + dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export') + throw err + }) + } + }) + } + handleTrashButtonClick (e) { const { note } = this.state const { isTrashed } = note @@ -207,6 +238,14 @@ class MarkdownNoteDetail extends React.Component { ee.emit('list:next') } + handleSaveAsText () { + this.exportAsDocument('txt') + } + + handleSaveAsMd () { + this.exportAsDocument('md') + } + handleUndoButtonClick (e) { const { note } = this.state diff --git a/browser/main/lib/dataApi/exportImage.js b/browser/main/lib/dataApi/exportImage.js new file mode 100755 index 00000000..a1c84390 --- /dev/null +++ b/browser/main/lib/dataApi/exportImage.js @@ -0,0 +1,37 @@ +const fs = require('fs') +const path = require('path') + +/** + * @description Export an image + * @param {String} storagePath + * @param {String} srcFilename + * @param {String} dstPath + * @param {String} dstFilename if not present, destination filename will be equal to srcFilename + * @return {Promise} an image path + */ +function exportImage (storagePath, srcFilename, dstPath, dstFilename = '') { + dstFilename = dstFilename || srcFilename + + const src = path.join(storagePath, 'images', srcFilename) + + if (!path.extname(dstFilename)) { + dstFilename += path.extname(srcFilename) + } + + const dstImagesFolder = path.join(dstPath, 'images') + const dst = path.join(dstImagesFolder, dstFilename) + + return new Promise((resolve, reject) => { + if (!fs.existsSync(dstImagesFolder)) fs.mkdirSync(dstImagesFolder) + + const input = fs.createReadStream(src) + const output = fs.createWriteStream(dst) + + output.on('error', reject) + input.on('error', reject) + input.on('end', resolve) + input.pipe(output) + }) +} + +module.exports = exportImage diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js new file mode 100755 index 00000000..368b43ca --- /dev/null +++ b/browser/main/lib/dataApi/exportNote.js @@ -0,0 +1,67 @@ +import exportImage from 'browser/main/lib/dataApi/exportImage' +import {findStorage} from 'browser/lib/findStorage' + +const fs = require('fs') +const path = require('path') + +/** + * Export note together with images + * + * If images is stored in the storage, creates 'images' subfolder in target directory + * and copies images to it. Changes links to images in the content of the note + * + * @param {String} storageKey + * @param {String} noteContent Content to export + * @param {String} targetPath Path to exported file + * @return {Promise.<*[]>} + */ +function exportNote (storageKey, noteContent, targetPath) { + const targetStorage = findStorage(storageKey) + const storagedImagesRe = /!\[(.*?)\]\(\s*?\/:storage\/(.*\.\S*?)\)/gi + const exportTasks = [] + const images = [] + + const exportedData = noteContent.replace(storagedImagesRe, (match, dstFilename, srcFilename) => { + if (!path.extname(dstFilename)) { + dstFilename += path.extname(srcFilename) + } + const imagePath = path.join('images', dstFilename) + + exportTasks.push( + exportImage(targetStorage.path, srcFilename, path.dirname(targetPath), dstFilename) + ) + images.push(imagePath) + return `![${dstFilename}](${imagePath})` + }) + + exportTasks.push(exportFile(exportedData, targetPath)) + return Promise.all(exportTasks) + .catch((err) => { + rollbackExport(images) + throw err + }) +} + +function exportFile (data, filename) { + return new Promise((resolve, reject) => { + fs.writeFile(filename, data, (err) => { + if (err) throw err + + resolve(filename) + }) + }) +} + +/** + * Remove exported images + * @param imagesPaths + */ +function rollbackExport (imagesPaths) { + imagesPaths.forEach((path) => { + if (fs.existsSync(path)) { + fs.unlink(path) + } + }) +} + +export default exportNote From f678a17505e6d120246f1e5085dbbdf137ad61b0 Mon Sep 17 00:00:00 2001 From: Nikolay Lopin Date: Mon, 5 Feb 2018 02:08:33 +0300 Subject: [PATCH 2/5] Export images together with document --- browser/components/MarkdownPreview.js | 98 +++++++++++++++++++------- browser/main/lib/dataApi/copyFile.js | 31 ++++---- browser/main/lib/dataApi/exportNote.js | 95 ++++++++++++++++++------- 3 files changed, 153 insertions(+), 71 deletions(-) diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index c1be9ef1..e305d1e0 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -9,10 +9,10 @@ import Raphael from 'raphael' import flowchart from 'flowchart' import SequenceDiagram from 'js-sequence-diagrams' import eventEmitter from 'browser/main/lib/eventEmitter' -import fs from 'fs' import htmlTextHelper from 'browser/lib/htmlTextHelper' import copy from 'copy-to-clipboard' import mdurl from 'mdurl' +import exportNote from 'browser/main/lib/dataApi/exportNote'; const { remote } = require('electron') const { app } = remote @@ -24,6 +24,11 @@ const appPath = 'file://' + (process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()) +const CSS_FILES = [ + `${appPath}/node_modules/katex/dist/katex.min.css`, + `${appPath}/node_modules/codemirror/lib/codemirror.css` +] + function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) { return ` @font-face { @@ -146,12 +151,10 @@ export default class MarkdownPreview extends React.Component { } handleContextMenu (e) { - if (!this.props.onContextMenu) return this.props.onContextMenu(e) } handleMouseDown (e) { - if (!this.props.onMouseDown) return if (e.target != null) { switch (e.target.tagName) { case 'A': @@ -179,8 +182,33 @@ export default class MarkdownPreview extends React.Component { } handleSaveAsHtml () { - this.exportAsDocument('html', (value) => { - return this.refs.root.contentWindow.document.documentElement.outerHTML + this.exportAsDocument('html', (noteContent, exportTasks) => { + const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams() + + const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber) + const body = markdown.render(noteContent) + const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] + + files.forEach((file) => { + file = file.replace('file://', '') + exportTasks.push({ + src: file, + dst: 'css' + }) + }) + + let styles = '' + files.forEach((file) => { + styles += `` + }) + + return ` + + + ${styles} + + ${body} + ` }) } @@ -188,23 +216,29 @@ export default class MarkdownPreview extends React.Component { this.refs.root.contentWindow.print() } - exportAsDocument (fileType, formatter) { + exportAsDocument (fileType, contentFormatter) { const options = { filters: [ - { name: 'Documents', extensions: [fileType] } + {name: 'Documents', extensions: [fileType]} ], properties: ['openFile', 'createDirectory'] } - const value = formatter ? formatter.call(this, this.props.value) : this.props.value dialog.showSaveDialog(remote.getCurrentWindow(), options, - (filename) => { - if (filename) { - fs.writeFile(filename, value, (err) => { - if (err) throw err + (filename) => { + if (filename) { + const content = this.props.value + const storage = this.props.storagePath + + exportNote(storage, content, filename, contentFormatter) + .then((res) => { + dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`}) + }).catch((err) => { + dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export') + throw err + }) + } }) - } - }) } fixDecodedURI (node) { @@ -221,12 +255,16 @@ export default class MarkdownPreview extends React.Component { this.refs.root.setAttribute('sandbox', 'allow-scripts') this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler) - this.refs.root.contentWindow.document.head.innerHTML = ` + let styles = ` - - ` + + CSS_FILES.forEach((file) => { + styles += `` + }) + + this.refs.root.contentWindow.document.head.innerHTML = styles this.rewriteIframe() this.applyStyle() @@ -266,25 +304,31 @@ export default class MarkdownPreview extends React.Component { } } - applyStyle () { - const { fontSize, lineNumber, codeBlockTheme } = this.props - let { fontFamily, codeBlockFontFamily } = this.props + getStyleParams () { + const {fontSize, lineNumber, codeBlockTheme} = this.props + let {fontFamily, codeBlockFontFamily} = this.props fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0 - ? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily) - : defaultFontFamily + ? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily) + : defaultFontFamily codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0 - ? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily) - : defaultCodeBlockFontFamily + ? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily) + : defaultCodeBlockFontFamily - this.setCodeTheme(codeBlockTheme) + return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} + } + + applyStyle () { + const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams() + + this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme) this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber) } - setCodeTheme (theme) { + GetCodeThemeLink (theme) { theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default' ? theme : 'elegant' - this.getWindow().document.getElementById('codeTheme').href = theme.startsWith('solarized') + return theme.startsWith('solarized') ? `${appPath}/node_modules/codemirror/theme/solarized.css` : `${appPath}/node_modules/codemirror/theme/${theme}.css` } diff --git a/browser/main/lib/dataApi/copyFile.js b/browser/main/lib/dataApi/copyFile.js index b46ffd6a..2dc66309 100755 --- a/browser/main/lib/dataApi/copyFile.js +++ b/browser/main/lib/dataApi/copyFile.js @@ -2,35 +2,30 @@ const fs = require('fs') const path = require('path') /** - * @description Export a file - * @param {String} storagePath - * @param {String} srcFilename + * @description Copy a file from source to destination + * @param {String} srcPath * @param {String} dstPath - * @param {String} dstFilename if not present, destination filename will be equal to srcFilename * @return {Promise} an image path */ -function exportFile (storagePath, srcFilename, dstPath, dstFilename = '') { - dstFilename = dstFilename || srcFilename - - const src = path.join(storagePath, 'images', srcFilename) - - if (!path.extname(dstFilename)) { - dstFilename += path.extname(srcFilename) +function copyFile (srcPath, dstPath) { + if (!path.extname(dstPath)) { + dstPath = path.join(dstPath, path.basename(srcPath)) } - const dst = path.join(dstPath, dstFilename) - return new Promise((resolve, reject) => { - if (!fs.existsSync(dstPath)) fs.mkdirSync(dstPath) + const dstFolder = path.dirname(dstPath) + if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder) - const input = fs.createReadStream(src) - const output = fs.createWriteStream(dst) + const input = fs.createReadStream(srcPath) + const output = fs.createWriteStream(dstPath) output.on('error', reject) input.on('error', reject) - input.on('end', resolve, dst) + input.on('end', () => { + resolve(dstPath) + }) input.pipe(output) }) } -module.exports = exportFile +module.exports = copyFile diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js index 368b43ca..9a3d21ac 100755 --- a/browser/main/lib/dataApi/exportNote.js +++ b/browser/main/lib/dataApi/exportNote.js @@ -1,48 +1,77 @@ -import exportImage from 'browser/main/lib/dataApi/exportImage' +import copyFile from 'browser/main/lib/dataApi/copyFile' import {findStorage} from 'browser/lib/findStorage' const fs = require('fs') const path = require('path') +const LOCAL_STORED_REGEX = /!\[(.*?)\]\(\s*?\/:storage\/(.*\.\S*?)\)/gi +const IMAGES_FOLDER_NAME = 'images' + /** * Export note together with images * * If images is stored in the storage, creates 'images' subfolder in target directory * and copies images to it. Changes links to images in the content of the note * - * @param {String} storageKey + * @param {String} storageKey or storage path * @param {String} noteContent Content to export * @param {String} targetPath Path to exported file + * @param {function} outputFormatter * @return {Promise.<*[]>} */ -function exportNote (storageKey, noteContent, targetPath) { - const targetStorage = findStorage(storageKey) - const storagedImagesRe = /!\[(.*?)\]\(\s*?\/:storage\/(.*\.\S*?)\)/gi +function exportNote (storageKey, noteContent, targetPath, outputFormatter) { + const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path const exportTasks = [] - const images = [] - const exportedData = noteContent.replace(storagedImagesRe, (match, dstFilename, srcFilename) => { + if (!storagePath) { + throw new Error('Storage path is not found') + } + + let exportedData = noteContent.replace(LOCAL_STORED_REGEX, (match, dstFilename, srcFilename) => { if (!path.extname(dstFilename)) { dstFilename += path.extname(srcFilename) } - const imagePath = path.join('images', dstFilename) - exportTasks.push( - exportImage(targetStorage.path, srcFilename, path.dirname(targetPath), dstFilename) - ) - images.push(imagePath) - return `![${dstFilename}](${imagePath})` + const dstRelativePath = path.join(IMAGES_FOLDER_NAME, dstFilename) + + exportTasks.push({ + src: path.join(IMAGES_FOLDER_NAME, srcFilename), + dst: dstRelativePath + }) + + return `![${dstFilename}](${dstRelativePath})` }) - exportTasks.push(exportFile(exportedData, targetPath)) - return Promise.all(exportTasks) - .catch((err) => { - rollbackExport(images) - throw err - }) + if (outputFormatter) { + exportedData = outputFormatter(exportedData, exportTasks) + } + + const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath)) + + return Promise.all(tasks.map((task) => copyFile(task.src, task.dst))) + .then(() => { + return saveToFile(exportedData, targetPath) + }).catch((err) => { + rollbackExport(tasks) + throw err + }) } -function exportFile (data, filename) { +function prepareTasks (tasks, storagePath, targetPath) { + return tasks.map((task) => { + if (!path.isAbsolute(task.src)) { + task.src = path.join(storagePath, task.src) + } + + if (!path.isAbsolute(task.dst)) { + task.dst = path.join(targetPath, task.dst) + } + + return task + }) +} + +function saveToFile (data, filename) { return new Promise((resolve, reject) => { fs.writeFile(filename, data, (err) => { if (err) throw err @@ -53,13 +82,27 @@ function exportFile (data, filename) { } /** - * Remove exported images - * @param imagesPaths + * Remove exported files + * @param tasks Array of copy task objects. Object consists of two mandatory fields – `src` and `dst` */ -function rollbackExport (imagesPaths) { - imagesPaths.forEach((path) => { - if (fs.existsSync(path)) { - fs.unlink(path) +function rollbackExport (tasks) { + const folders = new Set() + tasks.forEach((task) => { + let fullpath = task.dst + + if (!path.extname(task.dst)) { + fullpath = path.join(task.dst, path.basename(task.src)) + } + + if (fs.existsSync(fullpath)) { + fs.unlink(fullpath) + folders.add(path.dirname(fullpath)) + } + }) + + folders.forEach((folder) => { + if (fs.readdirSync(folder).length === 0) { + fs.rmdir(folder) } }) } From 338f9fb5a9bcefac961c5c5bfc92120c7b415ddf Mon Sep 17 00:00:00 2001 From: Nikolay Lopin Date: Mon, 5 Feb 2018 13:04:01 +0300 Subject: [PATCH 3/5] Semicolon fix --- browser/components/MarkdownPreview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index ec08372e..71e1761d 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -12,7 +12,7 @@ import eventEmitter from 'browser/main/lib/eventEmitter' import htmlTextHelper from 'browser/lib/htmlTextHelper' import copy from 'copy-to-clipboard' import mdurl from 'mdurl' -import exportNote from 'browser/main/lib/dataApi/exportNote'; +import exportNote from 'browser/main/lib/dataApi/exportNote' const { remote } = require('electron') const { app } = remote From 12447effc9bffc5c71fdcaf3719962e204cc1a43 Mon Sep 17 00:00:00 2001 From: Nikolay Lopin Date: Tue, 6 Feb 2018 22:19:06 +0300 Subject: [PATCH 4/5] Reject promise if write to file failed --- browser/main/lib/dataApi/exportNote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js index 9a3d21ac..3ed46b92 100755 --- a/browser/main/lib/dataApi/exportNote.js +++ b/browser/main/lib/dataApi/exportNote.js @@ -74,7 +74,7 @@ function prepareTasks (tasks, storagePath, targetPath) { function saveToFile (data, filename) { return new Promise((resolve, reject) => { fs.writeFile(filename, data, (err) => { - if (err) throw err + if (err) return reject(err) resolve(filename) }) From 5ec541c3c153b600f0f4641e1e854d2f096a6398 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Sat, 10 Feb 2018 18:29:57 +0900 Subject: [PATCH 5/5] Manipulate left margin of task item to hide circle --- browser/components/markdown.styl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index 4938f243..8b8d80c1 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -76,7 +76,7 @@ body justify-content left li label.taskListItem - margin-left -2em + margin-left -1.8em &.checked text-decoration line-through opacity 0.5