diff --git a/.editorconfig b/.editorconfig index 0bd9d653..9f89e705 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,4 +8,3 @@ end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 - diff --git a/.vscode/settings.json b/.vscode/settings.json index d35f1bbd..da73dd34 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,8 @@ { "editor.insertSpaces": true, "editor.tabSize": 2, - "editor.trimAutoWhitespace": true, + // Issues with unit test files + //"editor.trimAutoWhitespace": false, "files.eol": "\n", "files.insertFinalNewline": true, diff --git a/doc/SETTINGS.md b/doc/SETTINGS.md index e8bc9909..05b9d61e 100644 --- a/doc/SETTINGS.md +++ b/doc/SETTINGS.md @@ -10,6 +10,10 @@ - **codeFontFamily**: The code block font family name. - **lineHeight**: The line height of the editor. - **tabSize**: The number of spaces a tab is equal to. +- **listIndentation**: The list indentation of list items (`"dfm"`, `"tab"` or number `1-4`) + - `tab`: Indent subsequent paragraphs by one tab. + - `dfm`: Each subsequent paragraph in a list item must be indented by either 4 spaces or one tab, we are using 4 spaces (used by Bitbucket and Daring Fireball Markdown Spec). + - `number`: Dynamic indent subsequent paragraphs by the given number (1-4) plus list marker width (default). - **autoPairBracket**: If `true` the editor automatically closes brackets. - **autoPairMarkdownSyntax**: If `true` the editor automatically closes inline markdown like `*` or `_`. - **autoPairQuote**: If `true` the editor automatically closes quotes (`'` and `"`). diff --git a/src/main/preference.js b/src/main/preference.js index f56dbc10..c7de3ebf 100644 --- a/src/main/preference.js +++ b/src/main/preference.js @@ -48,7 +48,6 @@ class Preference { this.validateSettings(userSetting) } else { userSetting = this.loadJson(userDataPath) - this.validateSettings(userSetting) // Update outdated settings const requiresUpdate = !hasSameKeys(defaultSettings, userSetting) @@ -65,8 +64,11 @@ class Preference { userSetting[key] = defaultSettings[key] } } + this.validateSettings(userSetting) this.writeJson(userSetting, false) .catch(log) + } else { + this.validateSettings(userSetting) } } @@ -75,7 +77,6 @@ class Preference { userSetting = defaultSettings this.validateSettings(userSetting) } - this.cache = userSetting } @@ -104,7 +105,7 @@ class Preference { writeJson (json, async = true) { const { userDataPath } = this return new Promise((resolve, reject) => { - const content = fs.readFileSync(userDataPath, 'utf-8') + const content = fs.readFileSync(this.staticPath, 'utf-8') const tokens = content.split('```') const newContent = tokens[0] + '```json\n' + @@ -139,14 +140,35 @@ class Preference { brokenSettings = true settings.theme = 'light' } + if (!settings.bulletListMarker || (settings.bulletListMarker && !/^(?:\+|-|\*)$/.test(settings.bulletListMarker))) { brokenSettings = true settings.bulletListMarker = '-' } + if (!settings.titleBarStyle || !/^(?:native|csd|custom)$/.test(settings.titleBarStyle)) { settings.titleBarStyle = 'csd' } + + if (!settings.tabSize || typeof settings.tabSize !== 'number') { + settings.tabSize = 4 + } else if (settings.tabSize < 1) { + settings.tabSize = 1 + } else if (settings.tabSize > 4) { + settings.tabSize = 4 + } + + if (!settings.listIndentation) { + settings.listIndentation = 1 + } else if (typeof settings.listIndentation === 'number') { + if (settings.listIndentation < 1 || settings.listIndentation > 4) { + settings.listIndentation = 1 + } + } else if (settings.listIndentation !== 'tab' && settings.listIndentation !== 'dfm') { + settings.listIndentation = 1 + } + if (brokenSettings) { log('Broken settings detected; fallback to default value(s).') } diff --git a/src/muya/lib/config/index.js b/src/muya/lib/config/index.js index 26ba3248..12270a71 100644 --- a/src/muya/lib/config/index.js +++ b/src/muya/lib/config/index.js @@ -236,6 +236,8 @@ export const MUYA_DEFAULT_OPTION = { bulletListMarker: '-', orderListMarker: '.', tabSize: 4, + // bullet/list marker width + listIndentation, tab or Daring Fireball Markdown (4 spaces) --> list indentation + listIndentation: 1, sequenceTheme: 'hand', // hand or simple mermaidTheme: 'forest', // dark or forest hideQuickInsertHint: false diff --git a/src/muya/lib/contentState/copyCutCtrl.js b/src/muya/lib/contentState/copyCutCtrl.js index 8e3f1316..d96b54a5 100644 --- a/src/muya/lib/contentState/copyCutCtrl.js +++ b/src/muya/lib/contentState/copyCutCtrl.js @@ -155,7 +155,8 @@ const copyCutCtrl = ContentState => { case 'copyTable': { const table = this.getTableBlock() if (!table) return - const markdown = new ExportMarkdown([ table ]).generate() + const listIndentation = this.listIndentation + const markdown = new ExportMarkdown([ table ], listIndentation).generate() event.clipboardData.setData('text/html', '') event.clipboardData.setData('text/plain', markdown) break diff --git a/src/muya/lib/contentState/paragraphCtrl.js b/src/muya/lib/contentState/paragraphCtrl.js index 2baa77c2..16f4730d 100644 --- a/src/muya/lib/contentState/paragraphCtrl.js +++ b/src/muya/lib/contentState/paragraphCtrl.js @@ -251,7 +251,7 @@ const paragraphCtrl = ContentState => { this.appendChild(startBlock, codeBlock) const { key } = inputBlock const offset = 0 - + this.cursor = { start: { key, offset }, end: { key, offset } @@ -270,7 +270,8 @@ const paragraphCtrl = ContentState => { const codeBlock = this.createBlock('code') preBlock.functionType = 'fencecode' preBlock.lang = codeBlock.lang = '' - const markdown = new ExportMarkdown(children.slice(startIndex, endIndex + 1)).generate() + const listIndentation = this.listIndentation + const markdown = new ExportMarkdown(children.slice(startIndex, endIndex + 1), listIndentation).generate() markdown.split(LINE_BREAKS_REG).forEach(text => { const codeLine = this.createBlock('span', text) diff --git a/src/muya/lib/index.js b/src/muya/lib/index.js index 3da5e1f4..0918f5f5 100644 --- a/src/muya/lib/index.js +++ b/src/muya/lib/index.js @@ -59,7 +59,8 @@ class Muya { getMarkdown () { const blocks = this.contentState.getBlocks() - return new ExportMarkdown(blocks).generate() + const listIndentation = this.contentState.listIndentation + return new ExportMarkdown(blocks, listIndentation).generate() } getHistory () { @@ -141,11 +142,23 @@ class Muya { tabSize = 4 } else if (tabSize < 1) { tabSize = 1 + } else if (tabSize > 4) { + tabSize = 4 } - this.contentState.tabSize = tabSize } + setListIndentation (listIndentation) { + if (typeof listIndentation === 'number') { + if (listIndentation < 1 || listIndentation > 4) { + listIndentation = 1 + } + } else if (listIndentation !== 'tab' && listIndentation !== 'dfm') { + listIndentation = 1 + } + this.contentState.listIndentation = listIndentation + } + updateParagraph (type) { this.contentState.updateParagraph(type) } diff --git a/src/muya/lib/utils/exportMarkdown.js b/src/muya/lib/utils/exportMarkdown.js index b6180458..46db95df 100644 --- a/src/muya/lib/utils/exportMarkdown.js +++ b/src/muya/lib/utils/exportMarkdown.js @@ -7,14 +7,29 @@ * and GitHub Flavored Markdown Spec: https://github.github.com/gfm/ * The output markdown needs to obey the standards of the two Spec. */ -// const LINE_BREAKS = /\n/ class ExportMarkdown { - constructor (blocks) { + constructor (blocks, listIndentation = 1) { this.blocks = blocks this.listType = [] // 'ul' or 'ol' // helper to translate the first tight item in a nested list this.isLooseParentList = true + + // set and validate settings + if (listIndentation === 'tab') { + this.listIndentation = '\t' + this.listIndentationCount = null + } else if (listIndentation === 'dfm') { + // static 4 spaces + this.listIndentation = ' ' + this.listIndentationCount = null + } else if (typeof listIndentation === 'number') { + this.listIndentation = null + this.listIndentationCount = Math.min(Math.max(listIndentation, 1), 4) + } else { + this.listIndentation = null + this.listIndentationCount = 1 + } } generate () { @@ -302,26 +317,45 @@ class ExportMarkdown { normalizeListItem (block, indent) { const result = [] const listInfo = this.listType[this.listType.length - 1] + const isUnorderedList = listInfo.type === 'ul' let { children, bulletMarkerOrDelimiter } = block let itemMarker - if (listInfo.type === 'ul') { + if (isUnorderedList) { itemMarker = bulletMarkerOrDelimiter ? `${bulletMarkerOrDelimiter} ` : '- ' - if (block.listItemType === 'task') { - const firstChild = children[0] - itemMarker += firstChild.checked ? '[x] ' : '[ ] ' - children = children.slice(1) - } } else { const delimiter = bulletMarkerOrDelimiter ? bulletMarkerOrDelimiter : '.' itemMarker = `${listInfo.listCount++}${delimiter} ` } - const newIndent = indent + ' '.repeat(itemMarker.length) + // We already added one space to the indentation + const listIndent = this.getListIndentation(itemMarker.length - 1) + const newIndent = indent + listIndent + + if (isUnorderedList && block.listItemType === 'task') { + const firstChild = children[0] + itemMarker += firstChild.checked ? '[x] ' : '[ ] ' + children = children.slice(1) + } + result.push(`${indent}${itemMarker}`) result.push(this.translateBlocks2Markdown(children, newIndent).substring(newIndent.length)) return result.join('') } + + getListIndentation (listMarkerWidth) { + // listIndentation: + // tab: Indent subsequent paragraphs by one tab. + // dfm: Each subsequent paragraph in a list item must be indented by either 4 spaces or one tab (used by Bitbucket and Daring Fireball). + // number: Dynamic indent subsequent paragraphs by the given number (1-4) plus list marker width. + + if (this.listIndentation) { + return this.listIndentation + } else if (this.listIndentationCount) { + return ' '.repeat(listMarkerWidth + this.listIndentationCount) + } + return ' '.repeat(listMarkerWidth) + } } export default ExportMarkdown diff --git a/src/muya/lib/utils/importMarkdown.js b/src/muya/lib/utils/importMarkdown.js index 66813ca6..06b4be9f 100644 --- a/src/muya/lib/utils/importMarkdown.js +++ b/src/muya/lib/utils/importMarkdown.js @@ -273,7 +273,8 @@ const importRegister = ContentState => { const block = this.getBlock(key) const { text } = block block.text = text.substring(0, offset) + CURSOR_DNA + text.substring(offset) - const markdown = new ExportMarkdown(blocks).generate() + const listIndentation = this.listIndentation + const markdown = new ExportMarkdown(blocks, listIndentation).generate() const cursor = markdown.split('\n').reduce((acc, line, index) => { const ch = line.indexOf(CURSOR_DNA) if (ch > -1) { diff --git a/src/renderer/components/editorWithTabs/editor.vue b/src/renderer/components/editorWithTabs/editor.vue index d8ecb0e5..34ce44c9 100644 --- a/src/renderer/components/editorWithTabs/editor.vue +++ b/src/renderer/components/editorWithTabs/editor.vue @@ -121,6 +121,7 @@ 'autoPairQuote': state => state.preferences.autoPairQuote, 'bulletListMarker': state => state.preferences.bulletListMarker, 'tabSize': state => state.preferences.tabSize, + 'listIndentation': state => state.preferences.listIndentation, 'lineHeight': state => state.preferences.lineHeight, 'fontSize': state => state.preferences.fontSize, 'lightColor': state => state.preferences.lightColor, @@ -181,6 +182,12 @@ if (value !== oldValue && editor) { editor.setTabSize(value) } + }, + listIndentation: function (value, oldValue) { + const { editor } = this + if (value !== oldValue && editor) { + editor.setListIndentation(value) + } } }, created () { @@ -197,6 +204,7 @@ autoPairQuote, bulletListMarker, tabSize, + listIndentation, hideQuickInsertHint } = this @@ -218,6 +226,7 @@ autoPairQuote, bulletListMarker, tabSize, + listIndentation, hideQuickInsertHint }) diff --git a/src/renderer/store/preferences.js b/src/renderer/store/preferences.js index 480f4bbd..3df14cba 100644 --- a/src/renderer/store/preferences.js +++ b/src/renderer/store/preferences.js @@ -18,6 +18,8 @@ const state = { autoPairMarkdownSyntax: true, autoPairQuote: true, tabSize: 4, + // bullet/list marker width + listIndentation, tab or Daring Fireball Markdown (4 spaces) --> list indentation + listIndentation: 1, hideQuickInsertHint: false, titleBarStyle: 'csd', // edit modes (they are not in preference.md, but still put them here) diff --git a/static/preference.md b/static/preference.md index be5085d1..44ffd0d9 100755 --- a/static/preference.md +++ b/static/preference.md @@ -8,6 +8,8 @@ Edit and save to update preferences. You can only change the JSON below! - **endOfLine**: *String* `lf`, `crlf` or `default` +- **listIndentation**: `"dfm"`, `"tab"` or number (`1-4`) + - **bulletListMarker**: *String* `+`,`-` or `*` - **textDirection**: *String* `ltr` or `rtl` @@ -34,6 +36,7 @@ Edit and save to update preferences. You can only change the JSON below! "autoPairQuote": true, "endOfLine": "default", "tabSize": 4, + "listIndentation": 1, "textDirection": "ltr", "titleBarStyle": "csd", "openFilesInNewWindow": true diff --git a/test/unit/data/.editorconfig b/test/unit/data/.editorconfig new file mode 100644 index 00000000..16a8552b --- /dev/null +++ b/test/unit/data/.editorconfig @@ -0,0 +1,2 @@ +[*.md] +trim_trailing_whitespace = false diff --git a/test/unit/data/common/Escapes.md b/test/unit/data/common/Escapes.md index b8d3ae22..21276b0f 100644 --- a/test/unit/data/common/Escapes.md +++ b/test/unit/data/common/Escapes.md @@ -9,3 +9,9 @@ \`\`\` This isn't a code block without language identifier. \`\`\` + +\$ You can also escape math \$. + +\$\$ +This isn't a math block. +\$\$ diff --git a/test/unit/data/common/Lists.md b/test/unit/data/common/Lists.md index c75bc67c..dd9387e4 100644 --- a/test/unit/data/common/Lists.md +++ b/test/unit/data/common/Lists.md @@ -165,6 +165,8 @@ Foo - baz ``` +Issue `-` is replaced by `- `: + ``` - foo - diff --git a/test/unit/data/gfm/BasicTextFormatting.md b/test/unit/data/gfm/BasicTextFormatting.md index 2d93d227..d052b99a 100644 --- a/test/unit/data/gfm/BasicTextFormatting.md +++ b/test/unit/data/gfm/BasicTextFormatting.md @@ -1,3 +1,5 @@ # Basic Text Formatting ~~this is strike through text~~ + +\~\~and this not\~\~ diff --git a/test/unit/specs/markdown-list-indentation.spec.js b/test/unit/specs/markdown-list-indentation.spec.js new file mode 100644 index 00000000..6bdbeb5e --- /dev/null +++ b/test/unit/specs/markdown-list-indentation.spec.js @@ -0,0 +1,223 @@ +import ContentState from '../../../src/muya/lib/contentState' +import EventCenter from '../../../src/muya/lib/eventHandler/event' +import ExportMarkdown from '../../../src/muya/lib/utils/exportMarkdown' +import { MUYA_DEFAULT_OPTION } from '../../../src/muya/lib/config' + +const createMuyaContext = listIdentation => { + const ctx = {} + ctx.options = Object.assign({}, MUYA_DEFAULT_OPTION, { listIdentation }) + ctx.eventCenter = new EventCenter() + ctx.contentState = new ContentState(ctx, ctx.options) + return ctx +} + +// ---------------------------------------------------------------------------- +// Muya parser (Markdown to HTML to Markdown) +// + +const verifyMarkdown = (expectedMarkdown, listIdentation) => { + const markdown = `start + +- foo +- foo + - foo + - foo + - foo + - foo + - foo + - foo +- foo + +sep + +1. foo +2. foo + 1. foo + 2. foo + 1. foo + 3. foo +3. foo + 20. foo + 141. foo + 1. foo +` + + const ctx = createMuyaContext(listIdentation) + ctx.contentState.importMarkdown(markdown) + + const blocks = ctx.contentState.getBlocks() + const exportedMarkdown = new ExportMarkdown(blocks, listIdentation).generate() + expect(exportedMarkdown).to.equal(expectedMarkdown) +} + +describe('Muya tab identation', () => { + it('Indent by 1 space', () => { + const md = `start + +- foo +- foo + - foo + - foo + - foo + - foo + - foo + - foo +- foo + +sep + +1. foo +2. foo + 1. foo + 2. foo + 1. foo + 3. foo +3. foo + 20. foo + 141. foo + 1. foo +` + verifyMarkdown(md, 1) + }) + it('Indent by 2 spaces', () => { + const md = `start + +- foo +- foo + - foo + - foo + - foo + - foo + - foo + - foo +- foo + +sep + +1. foo +2. foo + 1. foo + 2. foo + 1. foo + 3. foo +3. foo + 20. foo + 141. foo + 1. foo +` + verifyMarkdown(md, 2) + }) + it('Indent by 3 spaces', () => { + const md = `start + +- foo +- foo + - foo + - foo + - foo + - foo + - foo + - foo +- foo + +sep + +1. foo +2. foo + 1. foo + 2. foo + 1. foo + 3. foo +3. foo + 20. foo + 141. foo + 1. foo +` + verifyMarkdown(md, 3) + }) + it('Indent by 4 spaces', () => { + const md = `start + +- foo +- foo + - foo + - foo + - foo + - foo + - foo + - foo +- foo + +sep + +1. foo +2. foo + 1. foo + 2. foo + 1. foo + 3. foo +3. foo + 20. foo + 141. foo + 1. foo +` + + verifyMarkdown(md, 4) + }) + it('Indent by one tab', () => { + const md = `start + +- foo +- foo +\t- foo +\t- foo +\t\t- foo +\t\t- foo +\t\t\t- foo +\t- foo +- foo + +sep + +1. foo +2. foo +\t1. foo +\t2. foo +\t\t1. foo +\t3. foo +3. foo +\t20. foo +\t\t141. foo +\t\t\t1. foo +` + verifyMarkdown(md, "tab") + }) + it('Indent using Daring Fireball Markdown Spec', () => { + const md = `start + +- foo +- foo + - foo + - foo + - foo + - foo + - foo + - foo +- foo + +sep + +1. foo +2. foo + 1. foo + 2. foo + 1. foo + 3. foo +3. foo + 20. foo + 141. foo + 1. foo +` + verifyMarkdown(md, "dfm") + }) +})