List item marker (#290)

* add bullet list marker to preference file

* list marker

* update change log

* update change log

* code style and remove debug codes
This commit is contained in:
冉四夕 2018-05-22 10:19:37 +08:00 committed by GitHub
parent 2b05619ac2
commit 30caf53d08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 138 additions and 77 deletions

View File

@ -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**

View File

@ -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 *

View File

@ -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 {

View File

@ -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()
}

View File

@ -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)

View File

@ -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)
})
}
}

View File

@ -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 + '</' + type + '>\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 <p> padding
classes += loose ? ` ${CLASS_OR_ID['AG_LOOSE_LIST_ITEM']}"` : ` ${CLASS_OR_ID['AG_TIGHT_LIST_ITEM']}"`;
if (checked === undefined) {
return '<li ' + classes + '>' + text + '</li>\n';
return '<li ' + classes + ' data-marker="' + bulletListItemMarker + '">' + text + '</li>\n';
}
return '<li ' + classes + '>' +
return '<li ' + classes + ' data-marker="' + bulletListItemMarker + '">' +
'<input type="checkbox" class="task-list-item-checkbox"' +
(checked ? ' checked' : '') +
'> ' +
@ -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':
{

View File

@ -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') {

View File

@ -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] ' : '[ ] '

View File

@ -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)

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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,