diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 4620dae2..fcc5a86d 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -1,4 +1,4 @@ -### 0.11.33 +### 0.11.35 **:cactus:Feature** @@ -9,6 +9,7 @@ - feature: Click filename to `rename` or `save` in title bar(**macOS ONLY**). - feature: Support YAML Front Matter - feature: Support `setext` heading but the default heading style is `atx` +- feature: User list item marker setting in preference file. **:butterfly:Optimization** diff --git a/src/editor/config.js b/src/editor/config.js index 20d4f55b..2df8e4d7 100644 --- a/src/editor/config.js +++ b/src/editor/config.js @@ -162,9 +162,9 @@ export const htmlBeautifyConfig = { export const CURSOR_DNA = getLongUniqueId() -export const turndownConfig = { +export const DEFAULT_TURNDOWN_CONFIG = { headingStyle: 'atx', // setext or atx - bulletListMarker: '*', // -, +, or * + bulletListMarker: '-', // -, +, or * codeBlockStyle: 'fenced', // fenced or indented fence: '```', // ``` or ~~~ emDelimiter: '*', // _ or * diff --git a/src/editor/contentState/enterCtrl.js b/src/editor/contentState/enterCtrl.js index 5e29a356..aab6dd87 100644 --- a/src/editor/contentState/enterCtrl.js +++ b/src/editor/contentState/enterCtrl.js @@ -101,6 +101,9 @@ const enterCtrl = ContentState => { } else { newBlock = this.createBlockLi() newBlock.listItemType = parent.listItemType + if (parent.listItemType === 'bullet') { + newBlock.bulletListItemMarker = parent.bulletListItemMarker + } } newBlock.isLooseListItem = parent.isLooseListItem this.insertAfter(newBlock, parent) @@ -310,6 +313,9 @@ const enterCtrl = ContentState => { newBlock = this.chopBlockByCursor(block.children[0], start.key, start.offset) newBlock = this.createBlockLi(newBlock) newBlock.listItemType = block.listItemType + if (block.listItemType === 'bullet') { + newBlock.bulletListItemMarker = block.bulletListItemMarker + } } newBlock.isLooseListItem = block.isLooseListItem } @@ -326,6 +332,9 @@ const enterCtrl = ContentState => { } else { newBlock = this.createBlockLi() newBlock.listItemType = block.listItemType + if (block.listItemType === 'bullet') { + newBlock.bulletListItemMarker = block.bulletListItemMarker + } } newBlock.isLooseListItem = block.isLooseListItem } else { diff --git a/src/editor/contentState/index.js b/src/editor/contentState/index.js index 18501bbf..f074d08b 100644 --- a/src/editor/contentState/index.js +++ b/src/editor/contentState/index.js @@ -1,4 +1,4 @@ -import { HAS_TEXT_BLOCK_REG } from '../config' +import { HAS_TEXT_BLOCK_REG, DEFAULT_TURNDOWN_CONFIG } from '../config' import { setCursorAtLastLine } from '../codeMirror' import { getUniqueId } from '../utils' import selection from '../selection' @@ -42,7 +42,7 @@ const prototypes = [ class ContentState { constructor (options) { - const { eventCenter } = options + const { eventCenter, bulletListMarker } = options Object.assign(this, options) // Use to cache the keys which you don't want to remove. this.exemption = new Set() @@ -55,6 +55,7 @@ class ContentState { this.prevCursor = null this.historyTimer = null this.history = new History(this) + this.turndownConfig = Object.assign(DEFAULT_TURNDOWN_CONFIG, { bulletListMarker }) this.init() } diff --git a/src/editor/contentState/updateCtrl.js b/src/editor/contentState/updateCtrl.js index 4effee4f..483b079e 100644 --- a/src/editor/contentState/updateCtrl.js +++ b/src/editor/contentState/updateCtrl.js @@ -118,6 +118,12 @@ const updateCtrl = ContentState => { newBlock.listItemType = type newBlock.isLooseListItem = preferLooseListItem + if (type === 'task' || type === 'bullet') { + const { bulletListMarker } = this + const bulletListItemMarker = marker ? marker.charAt(0) : bulletListMarker + newBlock.bulletListItemMarker = bulletListItemMarker + } + if ( preSibling && preSibling.listType === type && this.checkSameLooseType(preSibling, preferLooseListItem) && nextSibling && nextSibling.listType === type && this.checkSameLooseType(nextSibling, preferLooseListItem) diff --git a/src/editor/index.js b/src/editor/index.js index 32e1a334..58823184 100644 --- a/src/editor/index.js +++ b/src/editor/index.js @@ -21,14 +21,21 @@ class Aganippe { constructor (container, options) { const { focusMode = false, theme = 'light', markdown = '', preferLooseListItem = true, - autoPairBracket = true, autoPairMarkdownSyntax = true, autoPairQuote = true + autoPairBracket = true, autoPairMarkdownSyntax = true, autoPairQuote = true, bulletListMarker = '-' } = options this.container = container const eventCenter = this.eventCenter = new EventCenter() const floatBox = this.floatBox = new FloatBox(eventCenter) const tablePicker = this.tablePicker = new TablePicker(eventCenter) this.contentState = new ContentState({ - eventCenter, floatBox, tablePicker, preferLooseListItem, autoPairBracket, autoPairMarkdownSyntax, autoPairQuote + eventCenter, + floatBox, + tablePicker, + preferLooseListItem, + autoPairBracket, + autoPairMarkdownSyntax, + autoPairQuote, + bulletListMarker }) this.emoji = new Emoji() // emoji instance: has search(text) clear() methods. this.focusMode = focusMode @@ -359,7 +366,7 @@ class Aganippe { eventCenter.dispatch('selectionChange', selectionChanges) eventCenter.dispatch('selectionFormats', formats) this.dispatchChange() - }, 1000) + }) } } diff --git a/src/editor/parser/marked.js b/src/editor/parser/marked.js index 571cb037..0f22c96b 100644 --- a/src/editor/parser/marked.js +++ b/src/editor/parser/marked.js @@ -297,8 +297,7 @@ Lexer.prototype.token = function(src, top, bq) { // list if (cap = this.rules.tasklist.exec(src) || this.rules.orderlist.exec(src) || this.rules.bulletlist.exec(src)) { src = src.substring(cap[0].length); - bull = cap[2]; - + bull = cap[2] const ordered = bull.length > 1 && /\d/.test(bull) this.tokens.push({ @@ -387,6 +386,7 @@ Lexer.prototype.token = function(src, top, bq) { this.tokens.push({ checked: checked, listItemType: bull.length > 1 ? (/\d/.test(bull) ? 'order' : 'task') : 'bullet', + bulletListItemMarker: /\d/.test(bull) ? '' : bull.charAt(0), type: loose ? 'loose_item_start' : 'list_item_start' @@ -875,31 +875,31 @@ Renderer.prototype.list = function(body, ordered, start, taskList) { return '<' + type + classes + startatt + '>\n' + body + '\n' } -Renderer.prototype.listitem = function(text, checked, listItemType, loose) { - var classes; +Renderer.prototype.listitem = function(text, checked, listItemType, bulletListItemMarker, loose) { + let classes switch (listItemType) { case 'order': - classes = ' class="order-list-item'; + classes = ' class="order-list-item' break; case 'task': - classes = ' class="task-list-item'; + classes = ' class="task-list-item' break; case 'bullet': - classes = ' class="bullet-list-item'; + classes = ' class="bullet-list-item' break; default: throw new - Error('Invalid state'); + Error('Invalid state') } // "tight-list-item" is only used to remove

padding classes += loose ? ` ${CLASS_OR_ID['AG_LOOSE_LIST_ITEM']}"` : ` ${CLASS_OR_ID['AG_TIGHT_LIST_ITEM']}"`; if (checked === undefined) { - return '

  • ' + text + '
  • \n'; + return '
  • ' + text + '
  • \n'; } - return '
  • ' + + return '
  • ' + ' ' + @@ -1152,29 +1152,27 @@ Parser.prototype.tok = function() { } case 'list_item_start': { - var body = '', - checked = this.token.checked, - listItemType = this.token.listItemType; + let body = '' + const { checked, listItemType, bulletListItemMarker } = this.token while (this.next().type !== 'list_item_end') { body += this.token.type === 'text' ? this.parseText() : - this.tok(); + this.tok() } - return this.renderer.listitem(body, checked, listItemType, false); + return this.renderer.listitem(body, checked, listItemType, bulletListItemMarker, false) } case 'loose_item_start': { - var body = '', - checked = this.token.checked, - listItemType = this.token.listItemType; + let body = '' + const { checked, listItemType, bulletListItemMarker } = this.token while (this.next().type !== 'list_item_end') { - body += this.tok(); + body += this.tok() } - return this.renderer.listitem(body, checked, listItemType, true); + return this.renderer.listitem(body, checked, listItemType, bulletListItemMarker, true) } case 'html': { diff --git a/src/editor/parser/render/renderBlock/renderContainerBlock.js b/src/editor/parser/render/renderBlock/renderContainerBlock.js index c788e670..68db6b57 100644 --- a/src/editor/parser/render/renderBlock/renderContainerBlock.js +++ b/src/editor/parser/render/renderBlock/renderContainerBlock.js @@ -64,6 +64,9 @@ export default function renderContainerBlock (block, cursor, activeBlocks, match default: break } + if (block.bulletListItemMarker) { + Object.assign(data.dataset, { marker: block.bulletListItemMarker }) + } selector += block.isLooseListItem ? `.${CLASS_OR_ID['AG_LOOSE_LIST_ITEM']}` : `.${CLASS_OR_ID['AG_TIGHT_LIST_ITEM']}` } if (block.type === 'ol') { diff --git a/src/editor/utils/exportMarkdown.js b/src/editor/utils/exportMarkdown.js index ee67e60d..82de4f1f 100644 --- a/src/editor/utils/exportMarkdown.js +++ b/src/editor/utils/exportMarkdown.js @@ -1,5 +1,8 @@ /** - * Before you edit or update codes in this file, make sure you have read the + * Hi contributors! + * + * Before you edit or update codes in this file, + * make sure you have read this bellow: * Commonmark Spec: https://spec.commonmark.org/0.28/ * and GitHub Flavored Markdown Spec: https://github.github.com/gfm/ * The output markdown needs to obey the standards of the two Spec. @@ -245,11 +248,12 @@ class ExportMarkdown { normalizeListItem (block, indent) { const result = [] const listInfo = this.listType[this.listType.length - 1] - let { children } = block + let { children, bulletListItemMarker } = block let itemMarker if (listInfo.type === 'ul') { - itemMarker = '- ' + // console.log(block) + itemMarker = bulletListItemMarker ? `${bulletListItemMarker} ` : '- ' if (block.listItemType === 'task') { const firstChild = children[0] itemMarker += firstChild.checked ? '[x] ' : '[ ] ' diff --git a/src/editor/utils/importMarkdown.js b/src/editor/utils/importMarkdown.js index 188c00d4..6d902fad 100644 --- a/src/editor/utils/importMarkdown.js +++ b/src/editor/utils/importMarkdown.js @@ -4,53 +4,15 @@ * Both of them add a p block in li block, use the CSS style to distinguish loose and tight. */ import parse5 from 'parse5' -import TurndownService from 'turndown' import marked from '../parser/marked' import ExportMarkdown from './exportMarkdown' +import TurndownService, { usePluginAddRules } from './turndownService' // To be disabled rules when parse markdown, Because content state don't need to parse inline rules -import { turndownConfig, CLASS_OR_ID, CURSOR_DNA, TABLE_TOOLS, BLOCK_TYPE7, LINE_BREAK } from '../config' - -const turndownPluginGfm = require('turndown-plugin-gfm') +import { CLASS_OR_ID, CURSOR_DNA, TABLE_TOOLS, BLOCK_TYPE7 } from '../config' const LINE_BREAKS_REG = /\n/ -// turn html to markdown -const turndownService = new TurndownService(turndownConfig) -const gfm = turndownPluginGfm.gfm -// Use the gfm plugin -turndownService.use(gfm) -// because the strikethrough rule in gfm is single `~`, So need rewrite the strikethrough rule. -turndownService.addRule('strikethrough', { - filter: ['del', 's', 'strike'], - replacement (content) { - return '~~' + content + '~~' - } -}) - -// handle `soft line break` and `hard line break` -// add `LINE_BREAK` to the end of soft line break and hard line break. -turndownService.addRule('lineBreak', { - filter (node, options) { - return node.nodeName === 'SPAN' && node.classList.contains(CLASS_OR_ID['AG_LINE']) && node.nextElementSibling - }, - replacement (content, node, options) { - return content + LINE_BREAK - } -}) - -// remove `\` in text when paste -turndownService.addRule('normalText', { - filter (node, options) { - return (node.nodeName === 'SPAN' && - node.classList.contains(CLASS_OR_ID['AG_EMOJI_MARKED_TEXT'])) || - node.classList.contains('plain-text') - }, - replacement (content, node, options) { - return content.replace(/\\(?!\\)/g, '') - } -}) - const checkIsHTML = value => { const trimedValue = value.trim() const match = /^<([a-zA-Z\d-]+)(?=\s|>).*>/.exec(trimedValue) @@ -215,9 +177,16 @@ const importRegister = ContentState => { case 'li': const isTask = child.attrs.some(attr => attr.name === 'class' && attr.value.includes('task-list-item')) const isLoose = child.attrs.some(attr => attr.name === 'class' && attr.value.includes(CLASS_OR_ID['AG_LOOSE_LIST_ITEM'])) + block = this.createBlock('li') - block.listItemType = parent.nodeName === 'ul' ? (isTask ? 'task' : 'bullet') : 'order' + block.listItemType = parent.type === 'ul' ? (isTask ? 'task' : 'bullet') : 'order' block.isLooseListItem = isLoose + + if (/task|bullet/.test(block.listItemType)) { + const bulletListItemMarker = child.attrs.find(attr => attr.name === 'data-marker').value + if (bulletListItemMarker) block.bulletListItemMarker = bulletListItemMarker + } + this.appendChild(parent, block) travel(block, child.childNodes) break @@ -331,6 +300,10 @@ const importRegister = ContentState => { } // transform `paste's text/html data` to content state blocks. ContentState.prototype.html2State = function (html) { + // turn html to markdown + const { turndownConfig } = this + const turndownService = new TurndownService(turndownConfig) + usePluginAddRules(turndownService) // remove double `\\` in Math but I dont know why there are two '\' when paste. @jocs const markdown = turndownService.turndown(html).replace(/(\\)\\/g, '$1') return this.getStateFragment(markdown) diff --git a/src/editor/utils/turndownService.js b/src/editor/utils/turndownService.js new file mode 100644 index 00000000..ecf72b79 --- /dev/null +++ b/src/editor/utils/turndownService.js @@ -0,0 +1,42 @@ +import TurndownService from 'turndown' +import { CLASS_OR_ID, LINE_BREAK } from '../config' + +const turndownPluginGfm = require('turndown-plugin-gfm') + +export const usePluginAddRules = turndownService => { + // Use the gfm plugin + const { gfm } = turndownPluginGfm + turndownService.use(gfm) + // because the strikethrough rule in gfm is single `~`, So need rewrite the strikethrough rule. + turndownService.addRule('strikethrough', { + filter: ['del', 's', 'strike'], + replacement (content) { + return '~~' + content + '~~' + } + }) + + // handle `soft line break` and `hard line break` + // add `LINE_BREAK` to the end of soft line break and hard line break. + turndownService.addRule('lineBreak', { + filter (node, options) { + return node.nodeName === 'SPAN' && node.classList.contains(CLASS_OR_ID['AG_LINE']) && node.nextElementSibling + }, + replacement (content, node, options) { + return content + LINE_BREAK + } + }) + + // remove `\` in text when paste + turndownService.addRule('normalText', { + filter (node, options) { + return (node.nodeName === 'SPAN' && + node.classList.contains(CLASS_OR_ID['AG_EMOJI_MARKED_TEXT'])) || + node.classList.contains('plain-text') + }, + replacement (content, node, options) { + return content.replace(/\\(?!\\)/g, '') + } + }) +} + +export default TurndownService diff --git a/src/renderer/components/editor.vue b/src/renderer/components/editor.vue index d5ac28da..e4b58cac 100644 --- a/src/renderer/components/editor.vue +++ b/src/renderer/components/editor.vue @@ -100,7 +100,7 @@ }, computed: { ...mapState([ - 'preferLooseListItem', 'autoPairBracket', 'autoPairMarkdownSyntax', 'autoPairQuote' + 'preferLooseListItem', 'autoPairBracket', 'autoPairMarkdownSyntax', 'autoPairQuote', 'bulletListItemMarker' ]) }, data () { @@ -156,12 +156,26 @@ this.$nextTick(() => { const ele = this.$refs.editor const { - theme, focus: focusMode, markdown, preferLooseListItem, typewriter, - autoPairBracket, autoPairMarkdownSyntax, autoPairQuote + theme, + focus: focusMode, + markdown, + preferLooseListItem, + typewriter, + autoPairBracket, + autoPairMarkdownSyntax, + autoPairQuote, + bulletListMarker } = this const { container } = this.editor = new Aganippe(ele, { - theme, focusMode, markdown, preferLooseListItem, autoPairBracket, autoPairMarkdownSyntax, autoPairQuote + theme, + focusMode, + markdown, + preferLooseListItem, + autoPairBracket, + autoPairMarkdownSyntax, + autoPairQuote, + bulletListMarker }) if (typewriter) { diff --git a/src/renderer/store/editor.js b/src/renderer/store/editor.js index ceeeff1a..c311a280 100644 --- a/src/renderer/store/editor.js +++ b/src/renderer/store/editor.js @@ -19,6 +19,7 @@ const state = { darkColor: 'rgb(217, 217, 217)', // color in dark theme autoSave: false, preferLooseListItem: true, // prefer loose or tight list items + bulletListMarker: '-', autoPairBracket: true, autoPairMarkdownSyntax: true, autoPairQuote: true, @@ -123,7 +124,6 @@ const actions = { ipcRenderer.send('AGANI::ask-for-user-preference') ipcRenderer.on('AGANI::user-preference', (e, preference) => { const { autoSave } = preference - commit('SET_USER_PREFERENCE', preference) // handle autoSave diff --git a/static/preference.md b/static/preference.md index 50400778..bc9f1adf 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` +- **bulletListMarker**: *String* `+`,`-` or `*` + ```json { "fontSize": "16px", @@ -19,6 +21,7 @@ Edit and save to update preferences. You can only change the JSON below! "autoSave": false, "aidou": false, "preferLooseListItem": true, + "bulletListMarker": "-", "autoPairBracket": true, "autoPairMarkdownSyntax": true, "autoPairQuote": true,