mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 01:09:23 +08:00
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:
parent
e594705223
commit
4d7d850969
@ -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 = {
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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 })
|
||||
|
@ -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':
|
||||
{
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
])
|
||||
},
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user