diff --git a/browser/lib/markdown-toc-generator.js b/browser/lib/markdown-toc-generator.js index 363a58ce..716be83a 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()) } - updateCallback(toc.insert(currentValue, {slugify: caseSensitiveSlugify})) + + 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() + } +} + +/** + * 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 + generate, + generateInEditor } - diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index b0cdbb65..74542266 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 f4459f84..cf4df18c 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 d7615c01..3e3232b7 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 9fcc1d8d..60568741 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,148 +173,8 @@ this is a level one text + [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', ` @@ -346,8 +186,7 @@ this is a level one text # one modified -## one one - +## one one `, ` @@ -356,9 +195,6 @@ this is a level one text * [one one](#one-one) - -# one modified -## one one ` ], [ @@ -374,9 +210,6 @@ this is a level one text * [oNe one](#oNe-one) - -# onE -## oNe one ` ], [ @@ -397,19 +230,12 @@ this is a text ## oNe one `, ` -# title - -this is a text - - [onE](#onE) * [oNe one](#oNe-one) - -# onE -## oNe one ` ], [ @@ -424,11 +250,7 @@ this is a text - [hoge](#hoge) - - -# hoge - -## + ` ] ] @@ -436,9 +258,411 @@ this is a text 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 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 + 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) +- [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 + ` + ], + [ + `***************************** 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 + 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 + ` + ], + [ + `***************************** 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 + ` + ], + [ + `***************************** Two level note, cursor just before 'text' word`, + {line: 1, ch: 8}, + ` +# 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 + + +- [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 + ` + ], + [ + `***************************** 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 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 + ` + ], + [ + `***************************** Already generated TOC, needs updating in place`, + {line: 0, ch: 0}, + ` +# header + header text + + +- [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 +# 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-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 +# 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 + ` + ], + [ + `***************************** Document with cursor at the last line, expecting empty TOC `, + {line: 13, ch: 30}, + ` +# 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 + `, + ` +# 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 + + + + + + ` + ], + [ + `***************************** Empty, not actual TOC , should be supplemented with two new points beneath`, + {line: 0, ch: 0}, + ` +# 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 + + + + + +# new point included in toc +## new subpoint + `, + ` +# 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 + + +- [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 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}`) }) })