1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 17:56:25 +00:00

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.
This commit is contained in:
Maciek
2018-08-26 00:34:35 +02:00
parent ede733888d
commit 1bb841d5c5
5 changed files with 496 additions and 207 deletions

View File

@@ -6,6 +6,8 @@ import toc from 'markdown-toc'
import diacritics from 'diacritics-map' import diacritics from 'diacritics-map'
import stripColor from 'strip-color' import stripColor from 'strip-color'
const EOL = require('os').EOL
/** /**
* @caseSensitiveSlugify Custom slugify function * @caseSensitiveSlugify Custom slugify function
* Same implementation that the original used by markdown-toc (node_modules/markdown-toc/lib/utils.js), * 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 return diacritics[ch] || ch
}) })
} }
function getTitle (str) { function getTitle (str) {
if (/^\[[^\]]+\]\(/.test(str)) { if (/^\[[^\]]+\]\(/.test(str)) {
var m = /^\[([^\]]+)\]/.exec(str) var m = /^\[([^\]]+)\]/.exec(str)
@@ -24,6 +27,7 @@ function caseSensitiveSlugify (str) {
} }
return str return str
} }
str = getTitle(str) str = getTitle(str)
str = stripColor(str) str = stripColor(str)
// str = str.toLowerCase() //let's be case sensitive // str = str.toLowerCase() //let's be case sensitive
@@ -38,15 +42,59 @@ function caseSensitiveSlugify (str) {
return str return str
} }
export function generate (currentValue, updateCallback) { const TOC_MARKER_START = '<!-- toc -->'
const TOC_MARKER = '<!-- toc -->' const TOC_MARKER_END = '<!-- tocstop -->'
if (!currentValue.includes(TOC_MARKER)) {
currentValue = TOC_MARKER + currentValue /**
* 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 { export default {
generate generate,
generateInEditor
} }

View File

@@ -267,8 +267,8 @@ class MarkdownNoteDetail extends React.Component {
} }
handleGenerateToc () { handleGenerateToc () {
markdownToc.generate(this.refs.content.value, const editor = this.refs.content.refs.code.editor
(modifiedValue) => this.refs.content.refs.code.setValue(modifiedValue)) markdownToc.generateInEditor(editor)
} }
handleFocus (e) { handleFocus (e) {

View File

@@ -100,9 +100,8 @@ class SnippetNoteDetail extends React.Component {
handleGenerateToc () { handleGenerateToc () {
const currentMode = this.state.note.snippets[this.state.snippetIndex].mode const currentMode = this.state.note.snippets[this.state.snippetIndex].mode
if (currentMode.includes('Markdown')) { if (currentMode.includes('Markdown')) {
const currentValue = this.refs['code-' + this.state.snippetIndex].value
const currentEditor = this.refs['code-' + this.state.snippetIndex].refs.code.editor const currentEditor = this.refs['code-' + this.state.snippetIndex].refs.code.editor
markdownToc.generate(currentValue, (modifiedValue) => currentEditor.setValue(modifiedValue)) markdownToc.generateInEditor(currentEditor)
} }
} }

View File

@@ -1,5 +1,23 @@
import browserEnv from 'browser-env' 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 = { window.localStorage = {
// polyfill // polyfill

View File

@@ -1,19 +1,22 @@
/** /**
* @fileoverview Unit test for browser/lib/markdown-toc-generator * @fileoverview Unit test for browser/lib/markdown-toc-generator
*/ */
import CodeMirror from 'codemirror'
require('codemirror/addon/search/searchcursor.js')
const test = require('ava') const test = require('ava')
const markdownToc = require('browser/lib/markdown-toc-generator') const markdownToc = require('browser/lib/markdown-toc-generator')
const EOL = require('os').EOL const EOL = require('os').EOL
test(t => { test(t => {
/** /**
* @testCases Contains array of test cases in format : * Contains array of test cases in format :
* [ * [
* test title * test title
* input markdown, * input markdown,
* expected output markdown with toc * expected toc
* ] * ]
* * @type {*[]}
*/ */
const testCases = [ const testCases = [
[ [
@@ -39,8 +42,6 @@ test(t => {
- [one](#one) - [one](#one)
<!-- tocstop --> <!-- tocstop -->
# one
` `
], ],
[ [
@@ -56,9 +57,6 @@ test(t => {
- [two](#two) - [two](#two)
<!-- tocstop --> <!-- tocstop -->
# one
# two
` `
], ],
[ [
@@ -82,13 +80,6 @@ test(t => {
* [three three](#three-three) * [three three](#three-three)
<!-- tocstop --> <!-- tocstop -->
# 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) + [three three three three three three](#three-three-three-three-three-three)
<!-- tocstop --> <!-- tocstop -->
# 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) + [three three three three three three](#three-three-three-three-three-three)
<!-- tocstop --> <!-- tocstop -->
# 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',
`
<!-- 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)
<!-- tocstop -->
# 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
`,
`
<!-- 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)
<!-- tocstop -->
# 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',
`
<!-- toc -->
# one
## one one
`,
`
<!-- toc -->
- [one](#one)
* [one one](#one-one)
<!-- tocstop -->
# one
## one one
`
],
[
'***************************** note with just a closing TOC marker',
`
<!-- tocstop -->
# one
## one one
`,
`
<!-- toc -->
- [one](#one)
* [one one](#one-one)
<!-- tocstop -->
# one
## one one
`
],
[ [
'***************************** outdated TOC', '***************************** outdated TOC',
` `
@@ -347,7 +187,6 @@ this is a level one text
# one modified # one modified
## one one ## one one
`, `,
` `
<!-- toc --> <!-- toc -->
@@ -356,9 +195,6 @@ this is a level one text
* [one one](#one-one) * [one one](#one-one)
<!-- tocstop --> <!-- tocstop -->
# one modified
## one one
` `
], ],
[ [
@@ -374,9 +210,6 @@ this is a level one text
* [oNe one](#oNe-one) * [oNe one](#oNe-one)
<!-- tocstop --> <!-- tocstop -->
# onE
## oNe one
` `
], ],
[ [
@@ -397,19 +230,12 @@ this is a text
## oNe one ## oNe one
`, `,
` `
# title
this is a text
<!-- toc --> <!-- toc -->
- [onE](#onE) - [onE](#onE)
* [oNe one](#oNe-one) * [oNe one](#oNe-one)
<!-- tocstop --> <!-- tocstop -->
# onE
## oNe one
` `
], ],
[ [
@@ -425,10 +251,6 @@ this is a text
- [hoge](#hoge) - [hoge](#hoge)
<!-- tocstop --> <!-- tocstop -->
# hoge
##
` `
] ]
] ]
@@ -436,9 +258,411 @@ this is a text
testCases.forEach(testCase => { testCases.forEach(testCase => {
const title = testCase[0] const title = testCase[0]
const inputMd = testCase[1].trim() const inputMd = testCase[1].trim()
const expectedOutput = testCase[2].trim() const expectedToc = testCase[2].trim()
let generatedOutput const generatedToc = markdownToc.generate(inputMd)
markdownToc.generate(inputMd, (o) => { generatedOutput = o.trim() })
t.is(generatedOutput, expectedOutput, `Test ${title} , generated : ${EOL}${generatedOutput}, expected : ${EOL}${expectedOutput}`) 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},
``,
`
<!-- toc -->
<!-- tocstop -->
`
],
[
`***************************** 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
`,
`
<!-- toc -->
- [one](#one)
* [one one](#one-one)
- [two](#two)
* [two two](#two-two)
<!-- tocstop -->
# 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
<!-- toc -->
- [one](#one)
* [one one](#one-one)
- [two](#two)
* [two two](#two-two)
<!-- tocstop -->
# 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
<!-- toc -->
- [one](#one)
* [one one](#one-one)
- [two](#two)
* [two two](#two-two)
<!-- tocstop -->
# 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
<!-- toc -->
- [one](#one)
* [one one](#one-one)
- [two](#two)
* [two two](#two-two)
<!-- tocstop -->
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
<!-- toc -->
- [one](#one)
* [one one](#one-one)
- [two](#two)
* [two two](#two-two)
<!-- tocstop -->
# 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
<!-- toc -->
- [one](#one)
* [one one](#one-one)
- [two](#two)
* [two two](#two-two)
<!-- tocstop -->
# 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
<!-- toc -->
- [one](#one)
* [one one](#one-one)
- [two](#two)
* [two two](#two-two)
<!-- tocstop -->
# 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
<!-- toc -->
- [This is the one](#This-is-the-one)
* [one one](#one-one)
- [two](#two)
* [two two](#two-two)
<!-- tocstop -->
# 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
<!-- toc -->
<!-- tocstop -->
`
],
[
`***************************** 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
<!-- toc -->
<!-- tocstop -->
# 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
<!-- toc -->
- [new point included in toc](#new-point-included-in-toc)
* [new subpoint](#new-subpoint)
<!-- tocstop -->
# 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}`)
}) })
}) })