mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 05:52:33 +08:00
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:
parent
2b05619ac2
commit
30caf53d08
3
.github/CHANGELOG.md
vendored
3
.github/CHANGELOG.md
vendored
@ -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**
|
||||
|
||||
|
@ -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 *
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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':
|
||||
{
|
||||
|
@ -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') {
|
||||
|
@ -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] ' : '[ ] '
|
||||
|
@ -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)
|
||||
|
42
src/editor/utils/turndownService.js
Normal file
42
src/editor/utils/turndownService.js
Normal 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
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user