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_MATH',
'AG_MATH_RENDER',
'AG_MATH_ERROR'
'AG_MATH_ERROR',
'AG_LOOSE_LIST_ITEM',
'AG_TIGHT_LIST_ITEM'
])
export const codeMirrorConfig = {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import { CLASS_OR_ID } from '../config'
/**
* marked - a markdown parser
* Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed)
@ -155,7 +157,7 @@ Lexer.prototype.lex = function(src) {
Lexer.prototype.token = function(src, top, bq) {
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) {
// newline
@ -288,14 +290,18 @@ Lexer.prototype.token = function(src, top, bq) {
listType: bull.length > 1 ? (/\d/.test(bull) ? 'order' : 'task') : 'bullet'
});
let next = false;
let prevNext = true;
let listItemIndices = [];
// Get each top-level item.
cap = cap[0].match(this.rules.item);
next = false;
l = cap.length;
i = 0;
for (; i < l; i++) {
item = cap[i];
const itemWithBullet = cap[i];
item = itemWithBullet;
// Remove the list item's bullet
// 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.
// Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
// for discount behavior.
loose = next || /\n\n(?!\s*$)/.test(item);
if (i !== l - 1) {
next = item.charAt(item.length - 1) === '\n';
if (!loose) loose = next;
var prevItem = '';
if (i === 0) {
prevItem = item;
} else {
prevItem = cap[i - 1];
}
// 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({
@ -828,19 +854,26 @@ Renderer.prototype.list = function(body, ordered, taskList) {
return '<' + type + classes + '>\n' + body + '</' + type + '>\n';
};
Renderer.prototype.listitem = function(text, checked, listItemType) {
var classes
Renderer.prototype.listitem = function(text, checked, listItemType, loose) {
var classes;
switch (listItemType) {
case 'order':
classes = ' class="order-list-item"'
break
classes = ' class="order-list-item';
break;
case 'task':
classes = ' class="task-list-item"'
break
classes = ' class="task-list-item';
break;
case 'bullet':
classes = ' class="bullet-list-item"'
break
classes = ' class="bullet-list-item';
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) {
return '<li ' + classes + '>' + text + '</li>\n';
}
@ -1107,7 +1140,7 @@ Parser.prototype.tok = function() {
this.tok();
}
return this.renderer.listitem(body, checked, listItemType);
return this.renderer.listitem(body, checked, listItemType, false);
}
case 'loose_item_start':
{
@ -1119,7 +1152,7 @@ Parser.prototype.tok = function() {
body += this.tok();
}
return this.renderer.listitem(body, checked, listItemType);
return this.renderer.listitem(body, checked, listItemType, true);
}
case 'html':
{

View File

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

View File

@ -153,9 +153,11 @@ const importRegister = ContentState => {
break
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.listItemType = parent.nodeName === 'ul' ? (isTask ? 'task' : 'bullet') : 'order'
block.isLooseListItem = isLoose
this.appendChild(parent, block)
travel(block, child.childNodes)
break

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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