From 7804a229844c372f875ab498fb937744500bdbac Mon Sep 17 00:00:00 2001 From: Maciek Date: Fri, 10 Aug 2018 21:39:59 +0200 Subject: [PATCH 1/7] 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 000000000..363a58cef --- /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 82073162b..11197838e 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 652d1f531..f7a4dd3a9 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 cda964c58..b86552ace 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 fbbb025fb..062a9c6c3 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 aca0791fe..4d221f156 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', { From 3c14cc219e0d69d390fffb5bafad9bc776783275 Mon Sep 17 00:00:00 2001 From: Maciek Date: Sat, 11 Aug 2018 10:09:22 +0200 Subject: [PATCH 2/7] ESLint: fix let -> const warnings --- browser/main/Detail/SnippetNoteDetail.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index f7a4dd3a9..ce3d03e26 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -98,10 +98,10 @@ class SnippetNoteDetail extends React.Component { } handleGenerateToc () { - let currentMode = this.state.note.snippets[this.state.snippetIndex].mode + const 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 + const currentValue = this.refs['code-' + this.state.snippetIndex].value + const currentEditor = this.refs['code-' + this.state.snippetIndex].refs.code.editor markdownToc.generate(currentValue, (modifiedValue) => { currentEditor.setValue(modifiedValue) }) } } From ce3b29085f080ae705b8615c6dcecd0a4c3dc529 Mon Sep 17 00:00:00 2001 From: Maciek Date: Tue, 14 Aug 2018 22:32:22 +0200 Subject: [PATCH 3/7] Change menu position and accelerator for TOC gen. Due to the fact, that submenu "Edit" is visible only in macOS, let's move TOC generator to "File" menu. Also, change accelerator to SHIFT+CTRL+T which is working without conflicts and problems on all platforms. --- lib/main-menu.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/main-menu.js b/lib/main-menu.js index b86552ace..fed5eb153 100644 --- a/lib/main-menu.js +++ b/lib/main-menu.js @@ -145,6 +145,16 @@ const file = { { type: 'separator' }, + { + label: 'Generate/Update Markdown TOC', + accelerator: 'Shift+Ctrl+T', + click () { + mainWindow.webContents.send('code:generate-toc') + } + }, + { + type: 'separator' + }, { label: 'Print', accelerator: 'CommandOrControl+P', @@ -228,16 +238,6 @@ 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') - } } ] } From 5bb90babbcd06a71c0b9aad58ad9f830ff265b0c Mon Sep 17 00:00:00 2001 From: Maciek Date: Tue, 21 Aug 2018 00:02:25 +0200 Subject: [PATCH 4/7] Add tests for Markdown TOC generator --- tests/lib/markdown-toc-generator-test.js | 444 +++++++++++++++++++++++ 1 file changed, 444 insertions(+) create mode 100644 tests/lib/markdown-toc-generator-test.js diff --git a/tests/lib/markdown-toc-generator-test.js b/tests/lib/markdown-toc-generator-test.js new file mode 100644 index 000000000..9fcc1d8d8 --- /dev/null +++ b/tests/lib/markdown-toc-generator-test.js @@ -0,0 +1,444 @@ +/** + * @fileoverview Unit test for browser/lib/markdown-toc-generator + */ +const test = require('ava') +const markdownToc = require('browser/lib/markdown-toc-generator') +const EOL = require('os').EOL + +test(t => { + /** + * @testCases Contains array of test cases in format : + * [ + * test title + * input markdown, + * expected output markdown with toc + * ] + * + */ + const testCases = [ + [ + '***************************** empty note', + ` + `, + ` + + + + + + ` + ], + [ + '***************************** single level', + ` +# one + `, + ` + + +- [one](#one) + + + +# one + ` + ], + [ + '***************************** two levels', + ` +# one +# two + `, + ` + + +- [one](#one) +- [two](#two) + + + +# one +# two + ` + ], + [ + '***************************** 3 levels with children', + ` +# one +## one one +# two +## two two +# three +## three three + `, + ` + + +- [one](#one) + * [one one](#one-one) +- [two](#two) + * [two two](#two-two) +- [three](#three) + * [three three](#three-three) + + + +# one +## one one +# two +## two two +# three +## three three + ` + ], + [ + '***************************** 3 levels, 3rd with 6 sub-levels', + ` +# one +## one one +# two +## two two +# three +## three three +### three three three +#### three three three three +##### three three three three three +###### three three three three three three + `, + ` + + +- [one](#one) + * [one one](#one-one) +- [two](#two) + * [two two](#two-two) +- [three](#three) + * [three three](#three-three) + + [three three three](#three-three-three) + - [three three three three](#three-three-three-three) + * [three three three three three](#three-three-three-three-three) + + [three three three three three three](#three-three-three-three-three-three) + + + +# one +## one one +# two +## two two +# three +## three three +### three three three +#### three three three three +##### three three three three three +###### three three three three three three + ` + ], + [ + '***************************** multilevel with texts in between', + ` +# one +this is a level one text +this is a level one text +## one one +# two + this is a level two text + this is a level two text +## two two + this is a level two two text + this is a level two two text +# three + this is a level three three text + this is a level three three text +## three three + this is a text + this is a text +### three three three + this is a text + this is a text +### three three three 2 + this is a text + this is a text +#### three three three three + this is a text + this is a text +#### three three three three 2 + this is a text + this is a text +##### three three three three three + this is a text + this is a text +##### three three three three three 2 + this is a text + this is a text +###### three three three three three three + this is a text + this is a text + this is a text + `, + ` + + +- [one](#one) + * [one one](#one-one) +- [two](#two) + * [two two](#two-two) +- [three](#three) + * [three three](#three-three) + + [three three three](#three-three-three) + + [three three three 2](#three-three-three-2) + - [three three three three](#three-three-three-three) + - [three three three three 2](#three-three-three-three-2) + * [three three three three three](#three-three-three-three-three) + * [three three three three three 2](#three-three-three-three-three-2) + + [three three three three three three](#three-three-three-three-three-three) + + + +# one +this is a level one text +this is a level one text +## one one +# two + this is a level two text + this is a level two text +## two two + this is a level two two text + this is a level two two text +# three + this is a level three three text + this is a level three three text +## three three + this is a text + this is a text +### three three three + this is a text + this is a text +### three three three 2 + this is a text + this is a text +#### three three three three + this is a text + this is a text +#### three three three three 2 + this is a text + this is a text +##### three three three three three + this is a text + this is a text +##### three three three three three 2 + this is a text + this is a text +###### three three three three three three + this is a text + this is a text + this is a text + ` + ], + [ + '***************************** already generated toc', + ` + + +- [one](#one) + * [one one](#one-one) +- [two](#two) + * [two two](#two-two) +- [three](#three) + * [three three](#three-three) + + [three three three](#three-three-three) + - [three three three three](#three-three-three-three) + * [three three three three three](#three-three-three-three-three) + + [three three three three three three](#three-three-three-three-three-three) + + + +# one +## one one +# two +## two two +# three +## three three +### three three three +#### three three three three +##### three three three three three +###### three three three three three three + `, + ` + + +- [one](#one) + * [one one](#one-one) +- [two](#two) + * [two two](#two-two) +- [three](#three) + * [three three](#three-three) + + [three three three](#three-three-three) + - [three three three three](#three-three-three-three) + * [three three three three three](#three-three-three-three-three) + + [three three three three three three](#three-three-three-three-three-three) + + + +# one +## one one +# two +## two two +# three +## three three +### three three three +#### three three three three +##### three three three three three +###### three three three three three three + ` + ], + [ + '***************************** note with just an opening TOC marker', + ` + + + +# one +## one one + + `, + ` + + +- [one](#one) + * [one one](#one-one) + + + +# one +## one one + ` + ], + [ + '***************************** note with just a closing TOC marker', + ` + + +# one +## one one + `, + ` + + +- [one](#one) + * [one one](#one-one) + + + +# one +## one one + + ` + ], + + [ + '***************************** outdated TOC', + ` + + +- [one](#one) + * [one one](#one-one) + + + +# one modified +## one one + + `, + ` + + +- [one modified](#one-modified) + * [one one](#one-one) + + + +# one modified +## one one + ` + ], + [ + '***************************** properly generated case sensitive TOC', + ` +# onE +## oNe one + `, + ` + + +- [onE](#onE) + * [oNe one](#oNe-one) + + + +# onE +## oNe one + ` + ], + [ + '***************************** position of TOC is stable (do not use elements above toc marker)', + ` +# title + +this is a text + + + +- [onE](#onE) + * [oNe one](#oNe-one) + + + +# onE +## oNe one + `, + ` +# title + +this is a text + + + +- [onE](#onE) + * [oNe one](#oNe-one) + + + +# onE +## oNe one + ` + ], + [ + '***************************** properly handle generation of not completed TOC', + ` +# hoge + +## + `, + ` + + +- [hoge](#hoge) + + + +# hoge + +## + ` + ] + ] + + testCases.forEach(testCase => { + const title = testCase[0] + const inputMd = testCase[1].trim() + const expectedOutput = testCase[2].trim() + let generatedOutput + markdownToc.generate(inputMd, (o) => { generatedOutput = o.trim() }) + t.is(generatedOutput, expectedOutput, `Test ${title} , generated : ${EOL}${generatedOutput}, expected : ${EOL}${expectedOutput}`) + }) +}) From ede733888d7bd05e41c0fc6e6e92ad32488b01d7 Mon Sep 17 00:00:00 2001 From: Maciek Date: Sat, 25 Aug 2018 17:59:04 +0200 Subject: [PATCH 5/7] Code style: remove redundant brackets from lambda expression --- browser/main/Detail/MarkdownNoteDetail.js | 2 +- browser/main/Detail/SnippetNoteDetail.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index 11197838e..b0cdbb655 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -268,7 +268,7 @@ class MarkdownNoteDetail extends React.Component { handleGenerateToc () { markdownToc.generate(this.refs.content.value, - (modifiedValue) => { this.refs.content.refs.code.setValue(modifiedValue) }) + (modifiedValue) => this.refs.content.refs.code.setValue(modifiedValue)) } handleFocus (e) { diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index ce3d03e26..f4459f848 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -102,7 +102,7 @@ class SnippetNoteDetail extends React.Component { if (currentMode.includes('Markdown')) { const currentValue = this.refs['code-' + this.state.snippetIndex].value const currentEditor = this.refs['code-' + this.state.snippetIndex].refs.code.editor - markdownToc.generate(currentValue, (modifiedValue) => { currentEditor.setValue(modifiedValue) }) + markdownToc.generate(currentValue, (modifiedValue) => currentEditor.setValue(modifiedValue)) } } From 1bb841d5c50ba9662ee15926038ecc34f31048b2 Mon Sep 17 00:00:00 2001 From: Maciek Date: Sun, 26 Aug 2018 00:34:35 +0200 Subject: [PATCH 6/7] Create Markdown TOC at current cursor position If there is no TOC in the current document, it's created at current cursor position. Subsequent generation calls update TOC at existing position. Add additional tests with CodeMirror editor mock. --- browser/lib/markdown-toc-generator.js | 62 ++- browser/main/Detail/MarkdownNoteDetail.js | 4 +- browser/main/Detail/SnippetNoteDetail.js | 3 +- tests/helpers/setup-browser-env.js | 20 +- tests/lib/markdown-toc-generator-test.js | 536 +++++++++++++++------- 5 files changed, 457 insertions(+), 168 deletions(-) diff --git a/browser/lib/markdown-toc-generator.js b/browser/lib/markdown-toc-generator.js index 363a58cef..716be83a9 100644 --- a/browser/lib/markdown-toc-generator.js +++ b/browser/lib/markdown-toc-generator.js @@ -6,6 +6,8 @@ import toc from 'markdown-toc' import diacritics from 'diacritics-map' import stripColor from 'strip-color' +const EOL = require('os').EOL + /** * @caseSensitiveSlugify Custom slugify function * Same implementation that the original used by markdown-toc (node_modules/markdown-toc/lib/utils.js), @@ -17,6 +19,7 @@ function caseSensitiveSlugify (str) { return diacritics[ch] || ch }) } + function getTitle (str) { if (/^\[[^\]]+\]\(/.test(str)) { var m = /^\[([^\]]+)\]/.exec(str) @@ -24,6 +27,7 @@ function caseSensitiveSlugify (str) { } return str } + str = getTitle(str) str = stripColor(str) // str = str.toLowerCase() //let's be case sensitive @@ -38,15 +42,59 @@ function caseSensitiveSlugify (str) { return str } -export function generate (currentValue, updateCallback) { - const TOC_MARKER = '' - if (!currentValue.includes(TOC_MARKER)) { - currentValue = TOC_MARKER + currentValue +const TOC_MARKER_START = '' +const TOC_MARKER_END = '' + +/** + * Takes care of proper updating given editor with TOC. + * If TOC doesn't exit in the editor, it's inserted at current caret position. + * Otherwise,TOC is updated in place. + * @param editor CodeMirror editor to be updated with TOC + */ +export function generateInEditor (editor) { + const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`) + + function tocExistsInEditor () { + return tocRegex.test(editor.getValue()) + } + + function updateExistingToc () { + const toc = generate(editor.getValue()) + const search = editor.getSearchCursor(tocRegex) + while (search.findNext()) { + search.replace(toc) + } + } + + function addTocAtCursorPosition () { + const toc = generate(editor.getRange(editor.getCursor(), {line: Infinity})) + editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor()) + } + + if (tocExistsInEditor()) { + updateExistingToc() + } else { + addTocAtCursorPosition() } - updateCallback(toc.insert(currentValue, {slugify: caseSensitiveSlugify})) } -export default { - generate +/** + * Generates MD TOC based on MD document passed as string. + * @param markdownText MD document + * @returns generatedTOC String containing generated TOC + */ +export function generate (markdownText) { + const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify}) + return TOC_MARKER_START + EOL + EOL + generatedToc.content + EOL + EOL + TOC_MARKER_END +} + +function wrapTocWithEol (toc, editor) { + const leftWrap = editor.getCursor().ch === 0 ? '' : EOL + const rightWrap = editor.getLine(editor.getCursor().line).length === editor.getCursor().ch ? '' : EOL + return leftWrap + toc + rightWrap } +export default { + generate, + generateInEditor +} diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index b0cdbb655..745422665 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -267,8 +267,8 @@ class MarkdownNoteDetail extends React.Component { } handleGenerateToc () { - markdownToc.generate(this.refs.content.value, - (modifiedValue) => this.refs.content.refs.code.setValue(modifiedValue)) + const editor = this.refs.content.refs.code.editor + markdownToc.generateInEditor(editor) } handleFocus (e) { diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index f4459f848..cf4df18c2 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -100,9 +100,8 @@ class SnippetNoteDetail extends React.Component { handleGenerateToc () { const currentMode = this.state.note.snippets[this.state.snippetIndex].mode if (currentMode.includes('Markdown')) { - const currentValue = this.refs['code-' + this.state.snippetIndex].value const currentEditor = this.refs['code-' + this.state.snippetIndex].refs.code.editor - markdownToc.generate(currentValue, (modifiedValue) => currentEditor.setValue(modifiedValue)) + markdownToc.generateInEditor(currentEditor) } } diff --git a/tests/helpers/setup-browser-env.js b/tests/helpers/setup-browser-env.js index d7615c014..3e3232b7e 100644 --- a/tests/helpers/setup-browser-env.js +++ b/tests/helpers/setup-browser-env.js @@ -1,5 +1,23 @@ import browserEnv from 'browser-env' -browserEnv(['window', 'document']) +browserEnv(['window', 'document', 'navigator']) + +// for CodeMirror mockup +document.body.createTextRange = function () { + return { + setEnd: function () {}, + setStart: function () {}, + getBoundingClientRect: function () { + return {right: 0} + }, + getClientRects: function () { + return { + length: 0, + left: 0, + right: 0 + } + } + } +} window.localStorage = { // polyfill diff --git a/tests/lib/markdown-toc-generator-test.js b/tests/lib/markdown-toc-generator-test.js index 9fcc1d8d8..605687410 100644 --- a/tests/lib/markdown-toc-generator-test.js +++ b/tests/lib/markdown-toc-generator-test.js @@ -1,19 +1,22 @@ /** * @fileoverview Unit test for browser/lib/markdown-toc-generator */ + +import CodeMirror from 'codemirror' +require('codemirror/addon/search/searchcursor.js') const test = require('ava') const markdownToc = require('browser/lib/markdown-toc-generator') const EOL = require('os').EOL test(t => { /** - * @testCases Contains array of test cases in format : - * [ - * test title - * input markdown, - * expected output markdown with toc - * ] - * + * Contains array of test cases in format : + * [ + * test title + * input markdown, + * expected toc + * ] + * @type {*[]} */ const testCases = [ [ @@ -39,8 +42,6 @@ test(t => { - [one](#one) - -# one ` ], [ @@ -55,10 +56,7 @@ test(t => { - [one](#one) - [two](#two) - - -# one -# two + ` ], [ @@ -82,13 +80,6 @@ test(t => { * [three three](#three-three) - -# one -## one one -# two -## two two -# three -## three three ` ], [ @@ -120,17 +111,6 @@ test(t => { + [three three three three three three](#three-three-three-three-three-three) - -# one -## one one -# two -## two two -# three -## three three -### three three three -#### three three three three -##### three three three three three -###### three three three three three three ` ], [ @@ -193,10 +173,130 @@ this is a level one text + [three three three three three three](#three-three-three-three-three-three) + ` + ], + [ + '***************************** outdated TOC', + ` + + +- [one](#one) + * [one one](#one-one) + + + +# one modified +## one one + `, + ` + + +- [one modified](#one-modified) + * [one one](#one-one) + + + ` + ], + [ + '***************************** properly generated case sensitive TOC', + ` +# onE +## oNe one + `, + ` + + +- [onE](#onE) + * [oNe one](#oNe-one) + + + ` + ], + [ + '***************************** position of TOC is stable (do not use elements above toc marker)', + ` +# title + +this is a text + + + +- [onE](#onE) + * [oNe one](#oNe-one) + + + +# onE +## oNe one + `, + ` + + +- [onE](#onE) + * [oNe one](#oNe-one) + + + ` + ], + [ + '***************************** properly handle generation of not completed TOC', + ` +# hoge +## + `, + ` + + +- [hoge](#hoge) + + + ` + ] + ] + + testCases.forEach(testCase => { + const title = testCase[0] + const inputMd = testCase[1].trim() + const expectedToc = testCase[2].trim() + const generatedToc = markdownToc.generate(inputMd) + + t.is(generatedToc, expectedToc, `generate test : ${title} , generated : ${EOL}${generatedToc}, expected : ${EOL}${expectedToc}`) + }) +}) + +test(t => { + /** + * Contains array of test cases in format : + * [ + * title + * cursor + * inputMd + * expectedMd + * ] + * @type {*[]} + */ + const testCases = [ + [ + `***************************** Empty note, cursor at the top`, + {line: 0, ch: 0}, + ``, + ` + + + + + + ` + ], + [ + `***************************** Two level note,TOC at the beginning `, + {line: 0, ch: 0}, + ` # one this is a level one text this is a level one text + ## one one # two this is a level two text @@ -204,38 +304,7 @@ this is a level one text ## two two this is a level two two text this is a level two two text -# three - this is a level three three text - this is a level three three text -## three three - this is a text - this is a text -### three three three - this is a text - this is a text -### three three three 2 - this is a text - this is a text -#### three three three three - this is a text - this is a text -#### three three three three 2 - this is a text - this is a text -##### three three three three three - this is a text - this is a text -##### three three three three three 2 - this is a text - this is a text -###### three three three three three three - this is a text - this is a text - this is a text - ` - ], - [ - '***************************** already generated toc', + `, ` @@ -243,202 +312,357 @@ this is a level one text * [one one](#one-one) - [two](#two) * [two two](#two-two) -- [three](#three) - * [three three](#three-three) - + [three three three](#three-three-three) - - [three three three three](#three-three-three-three) - * [three three three three three](#three-three-three-three-three) - + [three three three three three three](#three-three-three-three-three-three) +# one +this is a level one text +this is a level one text + +## one one +# two + this is a level two text + this is a level two text +## two two + this is a level two two text + this is a level two two text + ` + ], + [ + `***************************** Two level note, cursor just after 'header text' `, + {line: 1, ch: 12}, + ` +# header + header text # one +this is a level one text +this is a level one text + ## one one # two + this is a level two text + this is a level two text ## two two -# three -## three three -### three three three -#### three three three three -##### three three three three three -###### three three three three three three - `, + this is a level two two text + this is a level two two text + `, ` +# header + header text - [one](#one) * [one one](#one-one) - [two](#two) * [two two](#two-two) -- [three](#three) - * [three three](#three-three) - + [three three three](#three-three-three) - - [three three three three](#three-three-three-three) - * [three three three three three](#three-three-three-three-three) - + [three three three three three three](#three-three-three-three-three-three) # one +this is a level one text +this is a level one text + ## one one # two + this is a level two text + this is a level two text ## two two -# three -## three three -### three three three -#### three three three three -##### three three three three three -###### three three three three three three - ` + this is a level two two text + this is a level two two text + ` ], [ - '***************************** note with just an opening TOC marker', + `***************************** Two level note, cursor at empty line under 'header text' `, + {line: 2, ch: 0}, ` - - +# header + header text # one +this is a level one text +this is a level one text + ## one one - - `, +# two + this is a level two text + this is a level two text +## two two + this is a level two two text + this is a level two two text + `, ` +# header + header text - [one](#one) * [one one](#one-one) +- [two](#two) + * [two two](#two-two) - # one +this is a level one text +this is a level one text + ## one one - ` +# two + this is a level two text + this is a level two text +## two two + this is a level two two text + this is a level two two text + ` ], [ - '***************************** note with just a closing TOC marker', + `***************************** Two level note, cursor just before 'text' word`, + {line: 1, ch: 8}, ` - +# header + header text # one -## one one - `, +this is a level one text +this is a level one text + +## one one +# two + this is a level two text + this is a level two text +## two two + this is a level two two text + this is a level two two text + `, ` +# header + header - [one](#one) * [one one](#one-one) +- [two](#two) + * [two two](#two-two) +text # one +this is a level one text +this is a level one text + ## one one - - ` +# two + this is a level two text + this is a level two text +## two two + this is a level two two text + this is a level two two text + ` ], - [ - '***************************** outdated TOC', + `***************************** Already generated TOC without header file, regenerate TOC in place, no changes`, + {line: 13, ch: 0}, ` +# header + header text - [one](#one) * [one one](#one-one) +- [two](#two) + * [two two](#two-two) +# one +this is a level one text +this is a level one text -# one modified ## one one - - `, +# two + this is a level two text + this is a level two text +## two two + this is a level two two text + this is a level two two text + `, ` +# header + header text -- [one modified](#one-modified) +- [one](#one) * [one one](#one-one) +- [two](#two) + * [two two](#two-two) +# one +this is a level one text +this is a level one text -# one modified -## one one - ` +## one one +# two + this is a level two text + this is a level two text +## two two + this is a level two two text + this is a level two two text + ` ], [ - '***************************** properly generated case sensitive TOC', - ` -# onE -## oNe one - `, + `***************************** Already generated TOC, needs updating in place`, + {line: 0, ch: 0}, ` +# header + header text -- [onE](#onE) - * [oNe one](#oNe-one) +- [one](#one) + * [one one](#one-one) +- [two](#two) + * [two two](#two-two) +# This is the one +this is a level one text +this is a level one text -# onE -## oNe one - ` - ], - [ - '***************************** position of TOC is stable (do not use elements above toc marker)', +## one one +# two + this is a level two text + this is a level two text +## two two + this is a level two two text + this is a level two two text + `, ` -# title - -this is a text - +# header + header text -- [onE](#onE) - * [oNe one](#oNe-one) +- [This is the one](#This-is-the-one) + * [one one](#one-one) +- [two](#two) + * [two two](#two-two) +# This is the one +this is a level one text +this is a level one text -# onE -## oNe one - `, +## one one +# two + this is a level two text + this is a level two text +## two two + this is a level two two text + this is a level two two text ` -# title + ], + [ + `***************************** Document with cursor at the last line, expecting empty TOC `, + {line: 13, ch: 30}, + ` +# header + header text -this is a text +# This is the one +this is a level one text +this is a level one text + +## one one +# two + this is a level two text + this is a level two text +## two two + this is a level two two text + this is a level two two text + `, + ` +# header + header text + +# This is the one +this is a level one text +this is a level one text +## one one +# two + this is a level two text + this is a level two text +## two two + this is a level two two text + this is a level two two text -- [onE](#onE) - * [oNe one](#oNe-one) - -# onE -## oNe one - ` + + ` ], [ - '***************************** properly handle generation of not completed TOC', + `***************************** Empty, not actual TOC , should be supplemented with two new points beneath`, + {line: 0, ch: 0}, ` -# hoge +# header + header text -## - `, - ` +# This is the one +this is a level one text +this is a level one text + +## one one +# two + this is a level two text + this is a level two text +## two two + this is a level two two text + this is a level two two text -- [hoge](#hoge) + +# new point included in toc +## new subpoint + `, + ` +# header + header text -# hoge +# This is the one +this is a level one text +this is a level one text -## - ` +## one one +# two + this is a level two text + this is a level two text +## two two + this is a level two two text + this is a level two two text + + +- [new point included in toc](#new-point-included-in-toc) + * [new subpoint](#new-subpoint) + + +# new point included in toc +## new subpoint + ` ] ] - testCases.forEach(testCase => { const title = testCase[0] - const inputMd = testCase[1].trim() - const expectedOutput = testCase[2].trim() - let generatedOutput - markdownToc.generate(inputMd, (o) => { generatedOutput = o.trim() }) - t.is(generatedOutput, expectedOutput, `Test ${title} , generated : ${EOL}${generatedOutput}, expected : ${EOL}${expectedOutput}`) + const cursor = testCase[1] + const inputMd = testCase[2].trim() + const expectedMd = testCase[3].trim() + + const editor = CodeMirror() + editor.setValue(inputMd) + editor.setCursor(cursor) + markdownToc.generateInEditor(editor) + + t.is(expectedMd, editor.getValue(), `generateInEditor test : ${title} , generated : ${EOL}${editor.getValue()}, expected : ${EOL}${expectedMd}`) }) }) From e9070fadab349fb74b62d74fa4a9fb623833cf6c Mon Sep 17 00:00:00 2001 From: Maciek Date: Sun, 2 Sep 2018 19:38:15 +0200 Subject: [PATCH 7/7] Refactoring : use object destructuring to retain file code style --- browser/main/Detail/SnippetNoteDetail.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index cf4df18c2..9a3946836 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -98,9 +98,10 @@ class SnippetNoteDetail extends React.Component { } handleGenerateToc () { - const currentMode = this.state.note.snippets[this.state.snippetIndex].mode + const { note, snippetIndex } = this.state + const currentMode = note.snippets[snippetIndex].mode if (currentMode.includes('Markdown')) { - const currentEditor = this.refs['code-' + this.state.snippetIndex].refs.code.editor + const currentEditor = this.refs[`code-${snippetIndex}`].refs.code.editor markdownToc.generateInEditor(currentEditor) } }