Add loose and tight list compatibility (#74)

* Add loose and tight list compatibility

* Fix 'false' preference booleans are not handled
This commit is contained in:
Felix Häusler 2018-03-29 19:52:54 +02:00 committed by 冉四夕
parent e594705223
commit 4d7d850969
14 changed files with 148 additions and 54 deletions

View File

@ -89,7 +89,9 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_HIGHLIGHT', 'AG_HIGHLIGHT',
'AG_MATH', 'AG_MATH',
'AG_MATH_RENDER', 'AG_MATH_RENDER',
'AG_MATH_ERROR' 'AG_MATH_ERROR',
'AG_LOOSE_LIST_ITEM',
'AG_TIGHT_LIST_ITEM'
]) ])
export const codeMirrorConfig = { export const codeMirrorConfig = {

View File

@ -187,6 +187,7 @@ const enterCtrl = ContentState => {
newBlock = this.createBlockLi(post) newBlock = this.createBlockLi(post)
newBlock.listItemType = block.listItemType newBlock.listItemType = block.listItemType
} }
newBlock.isLooseListItem = block.isLooseListItem
} else { } else {
block.text = pre block.text = pre
newBlock = this.createBlock(type, post) newBlock = this.createBlock(type, post)
@ -218,6 +219,7 @@ const enterCtrl = ContentState => {
newBlock = this.createBlockLi() newBlock = this.createBlockLi()
newBlock.listItemType = parent.listItemType newBlock.listItemType = parent.listItemType
} }
newBlock.isLooseListItem = parent.isLooseListItem
this.insertAfter(newBlock, parent) this.insertAfter(newBlock, parent)
const index = this.findIndex(parent.children, block) const index = this.findIndex(parent.children, block)
const partChildren = parent.children.splice(index + 1) const partChildren = parent.children.splice(index + 1)
@ -245,6 +247,7 @@ const enterCtrl = ContentState => {
newBlock = this.createBlockLi() newBlock = this.createBlockLi()
newBlock.listItemType = block.listItemType newBlock.listItemType = block.listItemType
} }
newBlock.isLooseListItem = block.isLooseListItem
} else { } else {
newBlock = this.createBlock('p') newBlock = this.createBlock('p')
} }

View File

@ -30,7 +30,7 @@ const updateCtrl = ContentState => {
return false return false
} }
ContentState.prototype.checkInlineUpdate = function (block) { ContentState.prototype.checkInlineUpdate = function (block, preferLooseListItem) {
if (/th|td|figure/.test(block.type)) return false if (/th|td|figure/.test(block.type)) return false
const { text } = block const { text } = block
const parent = this.getParent(block) const parent = this.getParent(block)
@ -43,11 +43,11 @@ const updateCtrl = ContentState => {
return true return true
case !!bullet: case !!bullet:
this.updateList(block, 'bullet', bullet) this.updateList(block, 'bullet', preferLooseListItem, bullet)
return true return true
case !!tasklist && parent && parent.listItemType === 'bullet': // only `bullet` list item can be update to `task` list item case !!tasklist && parent && parent.listItemType === 'bullet': // only `bullet` list item can be update to `task` list item
this.updateTaskListItem(block, 'tasklist', tasklist) this.updateTaskListItem(block, 'tasklist', preferLooseListItem, tasklist)
return true return true
case !!order: case !!order:
@ -144,7 +144,7 @@ const updateCtrl = ContentState => {
this.render() this.render()
} }
ContentState.prototype.updateList = function (block, type, marker = '') { ContentState.prototype.updateList = function (block, type, preferLooseListItem, marker = '') {
const parent = this.getParent(block) const parent = this.getParent(block)
const preSibling = this.getPreSibling(block) const preSibling = this.getPreSibling(block)
const nextSibling = this.getNextSibling(block) const nextSibling = this.getNextSibling(block)
@ -155,6 +155,7 @@ const updateCtrl = ContentState => {
const endOffset = end.offset const endOffset = end.offset
const newBlock = this.createBlockLi(newText, block.type) const newBlock = this.createBlockLi(newText, block.type)
newBlock.listItemType = type newBlock.listItemType = type
newBlock.isLooseListItem = preferLooseListItem
if (preSibling && preSibling.listType === type && nextSibling && nextSibling.listType === type) { if (preSibling && preSibling.listType === type && nextSibling && nextSibling.listType === type) {
this.appendChild(preSibling, newBlock) this.appendChild(preSibling, newBlock)
@ -224,7 +225,7 @@ const updateCtrl = ContentState => {
block.type = 'hr' block.type = 'hr'
} }
ContentState.prototype.updateState = function (event) { ContentState.prototype.updateState = function (event, preferLooseListItem) {
const { floatBox } = this const { floatBox } = this
const { start, end } = selection.getCursorRange() const { start, end } = selection.getCursorRange()
const { start: oldStart, end: oldEnd } = this.cursor const { start: oldStart, end: oldEnd } = this.cursor
@ -233,7 +234,7 @@ const updateCtrl = ContentState => {
} }
if (event.type === 'click' && start.key !== end.key) { if (event.type === 'click' && start.key !== end.key) {
setTimeout(() => { setTimeout(() => {
this.updateState(event) this.updateState(event, preferLooseListItem)
}) })
} }
if (event.type === 'input' && oldStart.key !== oldEnd.key) { if (event.type === 'input' && oldStart.key !== oldEnd.key) {
@ -326,7 +327,7 @@ const updateCtrl = ContentState => {
this.cursor = { start, end } this.cursor = { start, end }
const checkMarkedUpdate = this.checkNeedRender(block) const checkMarkedUpdate = this.checkNeedRender(block)
const checkInlineUpdate = this.checkInlineUpdate(block) const checkInlineUpdate = this.checkInlineUpdate(block, preferLooseListItem)
if (checkMarkedUpdate || checkInlineUpdate || needRender) { if (checkMarkedUpdate || checkInlineUpdate || needRender) {
this.render() this.render()

View File

@ -19,7 +19,7 @@ import './assets/symbolIcon/index.css'
class Aganippe { class Aganippe {
constructor (container, options) { constructor (container, options) {
const { focusMode = false, theme = 'light', markdown = '' } = options const { focusMode = false, theme = 'light', markdown = '', preferLooseListItem = true } = options
this.container = container this.container = container
const eventCenter = this.eventCenter = new EventCenter() const eventCenter = this.eventCenter = new EventCenter()
const floatBox = this.floatBox = new FloatBox(eventCenter) const floatBox = this.floatBox = new FloatBox(eventCenter)
@ -31,6 +31,7 @@ class Aganippe {
this.markdown = markdown this.markdown = markdown
this.fontSize = 16 this.fontSize = 16
this.lineHeight = 1.6 this.lineHeight = 1.6
this.preferLooseListItem = preferLooseListItem
// private property // private property
this._isEditChinese = false this._isEditChinese = false
this.init() this.init()
@ -342,7 +343,7 @@ class Aganippe {
// const style = getComputedStyle(target) // const style = getComputedStyle(target)
// if (event.type === 'click' && !style.contenteditable) return // if (event.type === 'click' && !style.contenteditable) return
if (!this._isEditChinese) { if (!this._isEditChinese) {
this.contentState.updateState(event) this.contentState.updateState(event, this.preferLooseListItem)
} }
if (event.type === 'click' || event.type === 'keyup') { if (event.type === 'click' || event.type === 'keyup') {
const selectionChanges = this.getSelection() const selectionChanges = this.getSelection()

View File

@ -113,6 +113,7 @@ class StateRender {
default: default:
break break
} }
blockSelector += block.isLooseListItem ? `.${CLASS_OR_ID['AG_LOOSE_LIST_ITEM']}` : `.${CLASS_OR_ID['AG_TIGHT_LIST_ITEM']}`
} }
if (block.type === 'ol') { if (block.type === 'ol') {
Object.assign(data.attrs, { start: block.start }) Object.assign(data.attrs, { start: block.start })

View File

@ -1,3 +1,5 @@
import { CLASS_OR_ID } from '../config'
/** /**
* marked - a markdown parser * marked - a markdown parser
* Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed)
@ -155,7 +157,7 @@ Lexer.prototype.lex = function(src) {
Lexer.prototype.token = function(src, top, bq) { Lexer.prototype.token = function(src, top, bq) {
var src = src.replace(/^ +$/gm, ''), var src = src.replace(/^ +$/gm, ''),
next, loose, cap, bull, b, item, space, i, l, checked; loose, cap, bull, b, item, space, i, l, checked;
while (src) { while (src) {
// newline // newline
@ -288,14 +290,18 @@ Lexer.prototype.token = function(src, top, bq) {
listType: bull.length > 1 ? (/\d/.test(bull) ? 'order' : 'task') : 'bullet' listType: bull.length > 1 ? (/\d/.test(bull) ? 'order' : 'task') : 'bullet'
}); });
let next = false;
let prevNext = true;
let listItemIndices = [];
// Get each top-level item. // Get each top-level item.
cap = cap[0].match(this.rules.item); cap = cap[0].match(this.rules.item);
next = false;
l = cap.length; l = cap.length;
i = 0; i = 0;
for (; i < l; i++) { for (; i < l; i++) {
item = cap[i]; const itemWithBullet = cap[i];
item = itemWithBullet;
// Remove the list item's bullet // Remove the list item's bullet
// so it is seen as the next token. // so it is seen as the next token.
@ -331,13 +337,33 @@ Lexer.prototype.token = function(src, top, bq) {
} }
} }
// Determine whether item is loose or not. var prevItem = '';
// Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ if (i === 0) {
// for discount behavior. prevItem = item;
loose = next || /\n\n(?!\s*$)/.test(item); } else {
if (i !== l - 1) { prevItem = cap[i - 1];
next = item.charAt(item.length - 1) === '\n'; }
if (!loose) loose = next;
// Determine whether item is loose or not. If previous item is loose
// this item is also loose.
loose = next = next || /^ *([*+-]|\d+\.) +\S+\n\n(?!\s*$)/.test(itemWithBullet);
// Check if previous line ends with a new line.
if (!loose && (i !== 0 || l > 1) && prevItem.length !== 0 && prevItem.charAt(prevItem.length - 1) === '\n') {
loose = next = true;
}
// A list is either loose or tight, so update previous list items.
if (next && prevNext !== next) {
for(const index of listItemIndices) {
this.tokens[index].type = 'loose_item_start'
}
listItemIndices = [];
}
prevNext = next;
if (!loose) {
listItemIndices.push(this.tokens.length);
} }
this.tokens.push({ this.tokens.push({
@ -828,19 +854,26 @@ Renderer.prototype.list = function(body, ordered, taskList) {
return '<' + type + classes + '>\n' + body + '</' + type + '>\n'; return '<' + type + classes + '>\n' + body + '</' + type + '>\n';
}; };
Renderer.prototype.listitem = function(text, checked, listItemType) { Renderer.prototype.listitem = function(text, checked, listItemType, loose) {
var classes var classes;
switch (listItemType) { switch (listItemType) {
case 'order': case 'order':
classes = ' class="order-list-item"' classes = ' class="order-list-item';
break break;
case 'task': case 'task':
classes = ' class="task-list-item"' classes = ' class="task-list-item';
break break;
case 'bullet': case 'bullet':
classes = ' class="bullet-list-item"' classes = ' class="bullet-list-item';
break break;
default:
throw new
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) { if (checked === undefined) {
return '<li ' + classes + '>' + text + '</li>\n'; return '<li ' + classes + '>' + text + '</li>\n';
} }
@ -1107,7 +1140,7 @@ Parser.prototype.tok = function() {
this.tok(); this.tok();
} }
return this.renderer.listitem(body, checked, listItemType); return this.renderer.listitem(body, checked, listItemType, false);
} }
case 'loose_item_start': case 'loose_item_start':
{ {
@ -1119,7 +1152,7 @@ Parser.prototype.tok = function() {
body += this.tok(); body += this.tok();
} }
return this.renderer.listitem(body, checked, listItemType); return this.renderer.listitem(body, checked, listItemType, true);
} }
case 'html': case 'html':
{ {

View File

@ -4,6 +4,8 @@ class ExportMarkdown {
constructor (blocks) { constructor (blocks) {
this.blocks = blocks this.blocks = blocks
this.listType = [] // 'ul' or 'ol' this.listType = [] // 'ul' or 'ol'
// helper to translate the first tight item in a nested list
this.isLooseParentList = true
} }
generate () { generate () {
@ -20,7 +22,7 @@ class ExportMarkdown {
switch (block.type) { switch (block.type) {
case 'p': case 'p':
case 'hr': case 'hr':
this.insertLineBreak(result, indent) this.insertLineBreak(result, indent, true)
result.push(this.normalizeParagraphText(block, indent)) result.push(this.normalizeParagraphText(block, indent))
break break
@ -30,43 +32,58 @@ class ExportMarkdown {
case 'h4': case 'h4':
case 'h5': case 'h5':
case 'h6': case 'h6':
this.insertLineBreak(result, indent) this.insertLineBreak(result, indent, true)
result.push(this.normalizeHeaderText(block, indent)) result.push(this.normalizeHeaderText(block, indent))
break break
case 'figure': case 'figure':
this.insertLineBreak(result, indent) this.insertLineBreak(result, indent, true)
const table = block.children[1] const table = block.children[1]
result.push(this.normalizeTable(table, indent)) result.push(this.normalizeTable(table, indent))
break break
case 'li': case 'li': {
this.insertLineBreak(result, indent) const insertNewLine = block.isLooseListItem
result.push(this.normalizeListItem(block, indent))
break
case 'ul': // helper variable to correct the first tight item in a nested list
this.insertLineBreak(result, indent) this.isLooseParentList = insertNewLine
this.insertLineBreak(result, indent, insertNewLine)
result.push(this.normalizeListItem(block, indent))
this.isLooseParentList = true
break
}
case 'ul': {
const insertNewLine = this.isLooseParentList
this.isLooseParentList = true
this.insertLineBreak(result, indent, insertNewLine)
this.listType.push({ type: 'ul' }) this.listType.push({ type: 'ul' })
result.push(this.normalizeList(block, indent)) result.push(this.normalizeList(block, indent))
this.listType.pop() this.listType.pop()
break break
}
case 'ol': case 'ol': {
this.insertLineBreak(result, indent) const insertNewLine = this.isLooseParentList
this.isLooseParentList = true
this.insertLineBreak(result, indent, insertNewLine)
const listCount = block.start !== undefined ? block.start : 1 const listCount = block.start !== undefined ? block.start : 1
this.listType.push({ type: 'ol', listCount }) this.listType.push({ type: 'ol', listCount })
result.push(this.normalizeList(block, indent)) result.push(this.normalizeList(block, indent))
this.listType.pop() this.listType.pop()
break break
}
case 'pre': case 'pre':
this.insertLineBreak(result, indent) this.insertLineBreak(result, indent, true)
result.push(this.normalizeCodeBlock(block, indent)) result.push(this.normalizeCodeBlock(block, indent))
break break
case 'blockquote': case 'blockquote':
this.insertLineBreak(result, indent) this.insertLineBreak(result, indent, true)
result.push(this.normalizeBlockquote(block, indent)) result.push(this.normalizeBlockquote(block, indent))
break break
default: default:
@ -77,12 +94,13 @@ class ExportMarkdown {
return result.join('') return result.join('')
} }
insertLineBreak (result, indent) { insertLineBreak (result, indent, insertNewLine) {
const newLine = insertNewLine ? '\n' : ''
if (result.length > 0) { if (result.length > 0) {
if (/\S/.test(indent)) { if (/\S/.test(indent)) {
result.push(`${indent}\n`) result.push(`${indent}${newLine}`)
} else { } else if (insertNewLine) {
result.push('\n') result.push(newLine)
} }
} }
} }

View File

@ -153,9 +153,11 @@ const importRegister = ContentState => {
break break
case 'li': case 'li':
const isTask = child.attrs.some(attr => attr.name === 'class' && attr.value === 'task-list-item') 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 = this.createBlock('li')
block.listItemType = parent.nodeName === 'ul' ? (isTask ? 'task' : 'bullet') : 'order' block.listItemType = parent.nodeName === 'ul' ? (isTask ? 'task' : 'bullet') : 'order'
block.isLooseListItem = isLoose
this.appendChild(parent, block) this.appendChild(parent, block)
travel(block, child.childNodes) travel(block, child.childNodes)
break break

View File

@ -70,7 +70,7 @@
computed: { computed: {
...mapState([ ...mapState([
'pathname', 'filename', 'isSaved', 'windowActive', 'wordCount', 'pathname', 'filename', 'isSaved', 'windowActive', 'wordCount',
'typewriter', 'focus', 'sourceCode', 'markdown', 'typewriter', 'focus', 'sourceCode', 'markdown', 'preferLooseListItem',
'cursor', 'theme', 'platform', 'lightColor', 'darkColor', 'fontSize', 'lineHeight' 'cursor', 'theme', 'platform', 'lightColor', 'darkColor', 'fontSize', 'lineHeight'
]) ])
}, },

View File

@ -59,6 +59,7 @@
</template> </template>
<script> <script>
import { mapState } from 'vuex'
import Aganippe from '../../editor' import Aganippe from '../../editor'
import bus from '../bus' import bus from '../bus'
import { animatedScrollTo } from '../util' import { animatedScrollTo } from '../util'
@ -94,6 +95,11 @@
lightColor: String, lightColor: String,
darkColor: String darkColor: String
}, },
computed: {
...mapState([
'preferLooseListItem'
])
},
data () { data () {
return { return {
selectionChange: null, selectionChange: null,
@ -139,8 +145,9 @@
created () { created () {
this.$nextTick(() => { this.$nextTick(() => {
const ele = this.$refs.editor const ele = this.$refs.editor
const { theme, focus: focusMode, markdown, typewriter } = this const { theme, focus: focusMode, markdown, preferLooseListItem, typewriter } = this
const { container } = this.editor = new Aganippe(ele, { theme, focusMode, markdown })
const { container } = this.editor = new Aganippe(ele, { theme, focusMode, markdown, preferLooseListItem })
if (typewriter) { if (typewriter) {
this.scrollToCursor() this.scrollToCursor()
@ -162,6 +169,7 @@
bus.$on('insert-image', this.handleSelect) bus.$on('insert-image', this.handleSelect)
bus.$on('content-in-source-mode', this.handleMarkdownChange) bus.$on('content-in-source-mode', this.handleMarkdownChange)
bus.$on('editor-blur', this.blurEditor) bus.$on('editor-blur', this.blurEditor)
bus.$on('update-prefer-loose-list-item', this.handlePreferLooseListItemChange)
bus.$on('image-auto-path', this.handleImagePath) bus.$on('image-auto-path', this.handleImagePath)
this.editor.on('insert-image', type => { this.editor.on('insert-image', type => {
@ -324,6 +332,12 @@
blurEditor () { blurEditor () {
this.editor.blur() this.editor.blur()
},
handlePreferLooseListItemChange (preferLooseListItem) {
if (this.editor) {
this.editor.preferLooseListItem = preferLooseListItem
}
} }
}, },
@ -339,6 +353,7 @@
bus.$off('find', this.handleFind) bus.$off('find', this.handleFind)
bus.$off('dotu-select', this.handleSelect) bus.$off('dotu-select', this.handleSelect)
bus.$off('editor-blur', this.blurEditor) bus.$off('editor-blur', this.blurEditor)
bus.$off('update-pref-list-item-type', this.handlePreferLooseListItemChange)
bus.$on('image-auto-path', this.handleImagePath) bus.$on('image-auto-path', this.handleImagePath)
this.editor.destroy() this.editor.destroy()

View File

@ -17,6 +17,7 @@ const state = {
lightColor: '#303133', // color in light theme lightColor: '#303133', // color in light theme
darkColor: 'rgb(217, 217, 217)', // color in dark theme darkColor: 'rgb(217, 217, 217)', // color in dark theme
autoSave: false, autoSave: false,
preferLooseListItem: true, // prefer loose or tight list items
// edit mode // edit mode
typewriter: false, // typewriter mode typewriter: false, // typewriter mode
focus: false, // focus mode focus: false, // focus mode
@ -66,7 +67,7 @@ const mutations = {
}, },
SET_USER_PREFERENCE (state, preference) { SET_USER_PREFERENCE (state, preference) {
Object.keys(preference).forEach(key => { Object.keys(preference).forEach(key => {
if (preference[key]) { if (typeof preference[key] !== 'undefined' && typeof state[key] !== 'undefined') {
state[key] = preference[key] state[key] = preference[key]
} }
}) })
@ -101,9 +102,15 @@ const actions = {
ipcRenderer.send('AGANI::ask-for-user-preference') ipcRenderer.send('AGANI::ask-for-user-preference')
ipcRenderer.on('AGANI::user-preference', (e, preference) => { ipcRenderer.on('AGANI::user-preference', (e, preference) => {
const { autoSave } = preference const { autoSave } = preference
const { preferLooseListItem } = state // old value
commit('SET_USER_PREFERENCE', preference) commit('SET_USER_PREFERENCE', preference)
if (typeof preference.preferLooseListItem !== 'undefined' &&
preference.preferLooseListItem !== preferLooseListItem) {
bus.$emit('update-prefer-loose-list-item', preference.preferLooseListItem)
}
// handle autoSave // handle autoSave
if (autoSave) { if (autoSave) {
const { pathname, markdown } = state const { pathname, markdown } = state

View File

@ -14,7 +14,8 @@ Edit and save to update preferences, You can only change the json bellow!
"lineHeight": "1.6", "lineHeight": "1.6",
"theme": "light", "theme": "light",
"autoSave": false, "autoSave": false,
"aidou": false "aidou": false,
"preferLooseListItem": true
} }
``` ```

View File

@ -248,6 +248,11 @@ li p.first {
display: inline-block; display: inline-block;
} }
li.ag-tight-list-item > p {
padding: 0;
margin: 0;
}
ul, ul,
ol { ol {
padding-left: 30px; padding-left: 30px;

View File

@ -221,6 +221,11 @@ li p.first {
display: inline-block; display: inline-block;
} }
li.ag-tight-list-item > p {
padding: 0;
margin: 0;
}
ul, ul,
ol { ol {
padding-left: 30px; padding-left: 30px;