From 7804a229844c372f875ab498fb937744500bdbac Mon Sep 17 00:00:00 2001 From: Maciek Date: Fri, 10 Aug 2018 21:39:59 +0200 Subject: [PATCH] Automatic table of contents generation for Markdown Adds table of contents for any Markdown note or Markdown snippet. Consequent generations update existing TOC. Generated TOC is case sensitive to handle #2067 Shortcut : CommandOrControl+Alt+T Menu : Edit/Generate/Update Markdown TOC --- browser/lib/markdown-toc-generator.js | 52 +++++++++++++++++++++++ browser/main/Detail/MarkdownNoteDetail.js | 9 ++++ browser/main/Detail/SnippetNoteDetail.js | 15 ++++++- lib/main-menu.js | 10 +++++ package.json | 1 + webpack-skeleton.js | 1 + 6 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 browser/lib/markdown-toc-generator.js diff --git a/browser/lib/markdown-toc-generator.js b/browser/lib/markdown-toc-generator.js new file mode 100644 index 00000000..363a58ce --- /dev/null +++ b/browser/lib/markdown-toc-generator.js @@ -0,0 +1,52 @@ +/** + * @fileoverview Markdown table of contents generator + */ + +import toc from 'markdown-toc' +import diacritics from 'diacritics-map' +import stripColor from 'strip-color' + +/** + * @caseSensitiveSlugify Custom slugify function + * Same implementation that the original used by markdown-toc (node_modules/markdown-toc/lib/utils.js), + * but keeps original case to properly handle https://github.com/BoostIO/Boostnote/issues/2067 + */ +function caseSensitiveSlugify (str) { + function replaceDiacritics (str) { + return str.replace(/[À-ž]/g, function (ch) { + return diacritics[ch] || ch + }) + } + function getTitle (str) { + if (/^\[[^\]]+\]\(/.test(str)) { + var m = /^\[([^\]]+)\]/.exec(str) + if (m) return m[1] + } + return str + } + str = getTitle(str) + str = stripColor(str) + // str = str.toLowerCase() //let's be case sensitive + + // `.split()` is often (but not always) faster than `.replace()` + str = str.split(' ').join('-') + str = str.split(/\t/).join('--') + str = str.split(/<\/?[^>]+>/).join('') + str = str.split(/[|$&`~=\\\/@+*!?({[\]})<>=.,;:'"^]/).join('') + str = str.split(/[。?!,、;:“”【】()〔〕[]﹃﹄“ ”‘’﹁﹂—…-~《》〈〉「」]/).join('') + str = replaceDiacritics(str) + return str +} + +export function generate (currentValue, updateCallback) { + const TOC_MARKER = '' + if (!currentValue.includes(TOC_MARKER)) { + currentValue = TOC_MARKER + currentValue + } + updateCallback(toc.insert(currentValue, {slugify: caseSensitiveSlugify})) +} + +export default { + generate +} + diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index 82073162..11197838 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -29,6 +29,7 @@ import { formatDate } from 'browser/lib/date-formatter' import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus' import striptags from 'striptags' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' +import markdownToc from 'browser/lib/markdown-toc-generator' class MarkdownNoteDetail extends React.Component { constructor (props) { @@ -47,6 +48,7 @@ class MarkdownNoteDetail extends React.Component { this.dispatchTimer = null this.toggleLockButton = this.handleToggleLockButton.bind(this) + this.generateToc = () => this.handleGenerateToc() } focus () { @@ -59,6 +61,7 @@ class MarkdownNoteDetail extends React.Component { const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT' this.handleSwitchMode(reversedType) }) + ee.on('code:generate-toc', this.generateToc) } componentWillReceiveProps (nextProps) { @@ -75,6 +78,7 @@ class MarkdownNoteDetail extends React.Component { componentWillUnmount () { ee.off('topbar:togglelockbutton', this.toggleLockButton) + ee.off('code:generate-toc', this.generateToc) if (this.saveQueue != null) this.saveNow() } @@ -262,6 +266,11 @@ class MarkdownNoteDetail extends React.Component { } } + handleGenerateToc () { + markdownToc.generate(this.refs.content.value, + (modifiedValue) => { this.refs.content.refs.code.setValue(modifiedValue) }) + } + handleFocus (e) { this.focus() } diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 652d1f53..f7a4dd3a 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -29,6 +29,7 @@ import InfoPanelTrashed from './InfoPanelTrashed' import { formatDate } from 'browser/lib/date-formatter' import i18n from 'browser/lib/i18n' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' +import markdownToc from 'browser/lib/markdown-toc-generator' const electron = require('electron') const { remote } = electron @@ -52,6 +53,7 @@ class SnippetNoteDetail extends React.Component { } this.scrollToNextTabThreshold = 0.7 + this.generateToc = () => this.handleGenerateToc() } componentDidMount () { @@ -65,6 +67,7 @@ class SnippetNoteDetail extends React.Component { enableLeftArrow: allTabs.offsetLeft !== 0 }) } + ee.on('code:generate-toc', this.generateToc) } componentWillReceiveProps (nextProps) { @@ -91,6 +94,16 @@ class SnippetNoteDetail extends React.Component { componentWillUnmount () { if (this.saveQueue != null) this.saveNow() + ee.off('code:generate-toc', this.generateToc) + } + + handleGenerateToc () { + let currentMode = this.state.note.snippets[this.state.snippetIndex].mode + if (currentMode.includes('Markdown')) { + let currentValue = this.refs['code-' + this.state.snippetIndex].value + let currentEditor = this.refs['code-' + this.state.snippetIndex].refs.code.editor + markdownToc.generate(currentValue, (modifiedValue) => { currentEditor.setValue(modifiedValue) }) + } } handleChange (e) { @@ -441,7 +454,7 @@ class SnippetNoteDetail extends React.Component { const isSuper = global.process.platform === 'darwin' ? e.metaKey : e.ctrlKey - if (isSuper && !e.shiftKey) { + if (isSuper && !e.shiftKey && !e.altKey) { e.preventDefault() this.addSnippet() } diff --git a/lib/main-menu.js b/lib/main-menu.js index cda964c5..b86552ac 100644 --- a/lib/main-menu.js +++ b/lib/main-menu.js @@ -228,6 +228,16 @@ const edit = { click () { mainWindow.webContents.send('editor:add-tag') } + }, + { + type: 'separator' + }, + { + label: 'Generate/Update Markdown TOC', + accelerator: 'CommandOrControl+Alt+T', + click () { + mainWindow.webContents.send('code:generate-toc') + } } ] } diff --git a/package.json b/package.json index fbbb025f..062a9c6c 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "markdown-it-named-headers": "^0.0.4", "markdown-it-plantuml": "^1.1.0", "markdown-it-smartarrows": "^1.0.1", + "markdown-toc": "^1.2.0", "mdurl": "^1.0.1", "mermaid": "^8.0.0-rc.8", "moment": "^2.10.3", diff --git a/webpack-skeleton.js b/webpack-skeleton.js index aca0791f..4d221f15 100644 --- a/webpack-skeleton.js +++ b/webpack-skeleton.js @@ -37,6 +37,7 @@ var config = { 'markdown-it-kbd', 'markdown-it-plantuml', 'markdown-it-admonition', + 'markdown-toc', 'devtron', '@rokt33r/season', {