diff --git a/src/muya/lib/contentState/enterCtrl.js b/src/muya/lib/contentState/enterCtrl.js index 1af08140..1e323852 100644 --- a/src/muya/lib/contentState/enterCtrl.js +++ b/src/muya/lib/contentState/enterCtrl.js @@ -112,9 +112,7 @@ const enterCtrl = ContentState => { } else { newBlock = this.createBlockLi() newBlock.listItemType = parent.listItemType - if (parent.listItemType === 'bullet') { - newBlock.bulletListItemMarker = parent.bulletListItemMarker - } + newBlock.bulletMarkerOrDelimiter = parent.bulletMarkerOrDelimiter } newBlock.isLooseListItem = parent.isLooseListItem this.insertAfter(newBlock, parent) @@ -334,9 +332,7 @@ 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.bulletMarkerOrDelimiter = block.bulletMarkerOrDelimiter } newBlock.isLooseListItem = block.isLooseListItem } @@ -357,9 +353,7 @@ const enterCtrl = ContentState => { } else { newBlock = this.createBlockLi() newBlock.listItemType = block.listItemType - if (block.listItemType === 'bullet') { - newBlock.bulletListItemMarker = block.bulletListItemMarker - } + newBlock.bulletMarkerOrDelimiter = block.bulletMarkerOrDelimiter } newBlock.isLooseListItem = block.isLooseListItem } else { diff --git a/src/muya/lib/contentState/updateCtrl.js b/src/muya/lib/contentState/updateCtrl.js index 8fe645e4..703c0d02 100644 --- a/src/muya/lib/contentState/updateCtrl.js +++ b/src/muya/lib/contentState/updateCtrl.js @@ -5,7 +5,7 @@ import { CLASS_OR_ID } from '../config' const INLINE_UPDATE_FRAGMENTS = [ '^([*+-]\\s)', // Bullet list '^(\\[[x\\s]{1}\\]\\s)', // Task list - '^(\\d+\\.\\s)', // Order list + '^(\\d{1,9}(?:\\.|\\))\\s)', // Order list '^\\s{0,3}(#{1,6})(?=\\s{1,}|$)', // ATX headings '^\\s{0,3}(\\={3,}|\\-{3,})(?=\\s{1,}|$)', // Setext headings '^(>).+', // Block quote @@ -124,6 +124,8 @@ const updateCtrl = ContentState => { if (block.type === 'span') { block = this.getParent(block) } + + const cleanMarker = marker ? marker.trim() : null const { preferLooseListItem } = this const parent = this.getParent(block) const wrapperTag = type === 'order' ? 'ol' : 'ul' // `bullet` => `ul` and `order` => `ol` @@ -131,6 +133,7 @@ const updateCtrl = ContentState => { const startOffset = start.offset const endOffset = end.offset const newBlock = this.createBlock('li') + if (/^h\d$/.test(block.type)) { delete block.marker delete block.headingStyle @@ -156,18 +159,47 @@ const updateCtrl = ContentState => { this.insertBefore(paragraphBefore, block) } } + const preSibling = this.getPreSibling(block) const nextSibling = this.getNextSibling(block) newBlock.listItemType = type newBlock.isLooseListItem = preferLooseListItem - if (type === 'task' || type === 'bullet') { + let bulletMarkerOrDelimiter + if (type === 'order') { + bulletMarkerOrDelimiter = (cleanMarker && cleanMarker.length >= 2) ? cleanMarker.slice(-1) : '.' + } else { const { bulletListMarker } = this - const bulletListItemMarker = marker ? marker.charAt(0) : bulletListMarker - newBlock.bulletListItemMarker = bulletListItemMarker + bulletMarkerOrDelimiter = marker ? marker.charAt(0) : bulletListMarker + } + newBlock.bulletMarkerOrDelimiter = bulletMarkerOrDelimiter + + // Special cases for CommonMark 264 and 265: Changing the bullet or ordered list delimiter starts a new list. + let startNewList = false + if (preSibling && /^(ol|ul)$/.test(preSibling.type)) { + const bullet = preSibling.children[0].bulletMarkerOrDelimiter + startNewList = bulletMarkerOrDelimiter !== bullet + } + if (nextSibling && /^(ol|ul)$/.test(nextSibling.type)) { + const bullet = nextSibling.children[0].bulletMarkerOrDelimiter + startNewList = startNewList || bulletMarkerOrDelimiter !== bullet } - if ( + if (startNewList) { + // Create a new list when changing list type, bullet or list delimiter + const listBlock = this.createBlock(wrapperTag) + listBlock.listType = type + if (wrapperTag === 'ol') { + const start = cleanMarker ? cleanMarker.slice(0, -1) : 1 + listBlock.start = /^\d+$/.test(start) ? start : 1 + } + this.appendChild(listBlock, newBlock) + this.insertBefore(listBlock, block) + this.removeBlock(block) + + // -------------------------------- + // Same list type or new list + } else if ( preSibling && preSibling.listType === type && this.checkSameLooseType(preSibling, preferLooseListItem) && @@ -186,7 +218,6 @@ const updateCtrl = ContentState => { this.checkSameLooseType(preSibling, preferLooseListItem) ) { this.appendChild(preSibling, newBlock) - this.removeBlock(block) } else if ( nextSibling && @@ -201,13 +232,12 @@ const updateCtrl = ContentState => { this.checkSameLooseType(parent, preferLooseListItem) ) { this.insertBefore(newBlock, block) - this.removeBlock(block) } else { const listBlock = this.createBlock(wrapperTag) listBlock.listType = type if (wrapperTag === 'ol') { - const start = marker.split('.')[0] + const start = cleanMarker ? cleanMarker.slice(0, -1) : 1 listBlock.start = /^\d+$/.test(start) ? start : 1 } this.appendChild(listBlock, newBlock) diff --git a/src/muya/lib/parser/marked/README.md b/src/muya/lib/parser/marked/README.md new file mode 100644 index 00000000..fbba659f --- /dev/null +++ b/src/muya/lib/parser/marked/README.md @@ -0,0 +1,27 @@ +# Marked + +This folder contains a patched [Marked.js](https://github.com/markedjs/marked/) version based on `v0.6.1` commit [6eec528e5d6e08ea751251f9dc195d052caf4a79](https://github.com/markedjs/marked/commit/6eec528e5d6e08ea751251f9dc195d052caf4a79). + +## Changes + +### Features + +- Markdown Extra: frontmatter and inline and block math +- GFM like: emojis + +### (Inline) Lexer + +- `disableInline` mode +- Custom list and list item implementation based on an older marked.js version +- Slightly modified definition due `disableInline` +- More token information like list item bullet type + +### Renderer + +- Emoji renderer +- Frontmatter renderer +- Inline and block (`multiplemath`) math renderer + +## License + +[MIT](LICENSE) diff --git a/src/muya/lib/parser/marked/blockRules.js b/src/muya/lib/parser/marked/blockRules.js index 656e93c0..e70401ca 100644 --- a/src/muya/lib/parser/marked/blockRules.js +++ b/src/muya/lib/parser/marked/blockRules.js @@ -44,7 +44,7 @@ block.def = edit(block.def). getRegex() block.checkbox = /^\[([ xX])\] +/ -block.bullet = /(?:[*+-]|\d{1,9}\.)/ +block.bullet = /(?:[*+-]|\d{1,9}(?:\.|\)))/ // patched: support "(" as ordered list delimiter too block.item = /^( *)(bull) ?[^\n]*(?:\n(?!\1bull ?)[^\n]*)*/ block.item = edit(block.item, 'gm'). replace(/bull/g, block.bullet). diff --git a/src/muya/lib/parser/marked/lexer.js b/src/muya/lib/parser/marked/lexer.js index 6ab5347d..278a3cc3 100644 --- a/src/muya/lib/parser/marked/lexer.js +++ b/src/muya/lib/parser/marked/lexer.js @@ -198,18 +198,20 @@ Lexer.prototype.token = function (src, top) { continue } + // NOTE: Complete list lexer part is a custom implementation based on an older marked.js version. + // list cap = this.rules.list.exec(src) if (cap) { src = src.substring(cap[0].length) bull = cap[2] - const isordered = bull.length > 1 && /\d/.test(bull) + let isOrdered = bull.length > 1 && /\d{1,9}/.test(bull) this.tokens.push({ type: 'list_start', - ordered: isordered, - listType: bull.length > 1 ? (/\d/.test(bull) ? 'order' : 'task') : 'bullet', - start: isordered ? +bull : '' + ordered: isOrdered, + listType: bull.length > 1 ? (/\d{1,9}/.test(bull) ? 'order' : 'task') : 'bullet', + start: isOrdered ? +(bull.slice(0, -1)) : '' }) let next = false @@ -228,9 +230,38 @@ Lexer.prototype.token = function (src, top) { // Remove the list item's bullet // so it is seen as the next token. space = item.length - item = item.replace(/^ *([*+-]|\d+\.) */, '') + let newBull + item = item.replace(/^ *([*+-]|\d+(?:\.|\))) */, function (m, p1) { + // Get and remove list item bullet + newBull = p1 || bull + return '' + }) - if (this.options.gfm) { + // Changing the bullet or ordered list delimiter starts a new list (CommonMark 264 and 265) + // - unordered, unordered --> bull !== newBull --> new list (e.g "-" --> "*") + // - ordered, ordered --> lastChar !== lastChar --> new list (e.g "." --> ")") + // - else --> new list (e.g. ordered --> unordered) + const newIsOrdered = bull.length > 1 && /\d{1,9}/.test(newBull) + if (i !== 0 && + ((!isOrdered && !newIsOrdered && bull !== newBull) || + (isOrdered && newIsOrdered && bull.slice(-1) !== newBull.slice(-1)) || + ((isOrdered && !newIsOrdered) || (!isOrdered && newIsOrdered)))) { + this.tokens.push({ + type: 'list_end' + }) + + // Start a new list + bull = newBull + isOrdered = newIsOrdered + this.tokens.push({ + type: 'list_start', + ordered: isOrdered, + listType: bull.length > 1 ? (/\d{1,9}/.test(bull) ? 'order' : 'task') : 'bullet', + start: isOrdered ? +(bull.slice(0, -1)) : '' + }) + } + + if (!isOrdered && this.options.gfm) { checked = this.rules.checkbox.exec(item) if (checked) { checked = checked[1] === 'x' || checked[1] === 'X' @@ -269,7 +300,7 @@ Lexer.prototype.token = function (src, top) { // 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*$)|\n\n(?!\s*$))/.test(itemWithBullet) + loose = next = next || /^ *([*+-]|\d{1,9}(?:\.|\)))( +\S+\n\n(?!\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') { @@ -289,10 +320,11 @@ Lexer.prototype.token = function (src, top) { listItemIndices.push(this.tokens.length) } + const isOrderedListItem = /\d/.test(bull) this.tokens.push({ checked: checked, - listItemType: bull.length > 1 ? (/\d/.test(bull) ? 'order' : 'task') : 'bullet', - bulletListItemMarker: /\d/.test(bull) ? '' : bull.charAt(0), + listItemType: bull.length > 1 ? (isOrderedListItem ? 'order' : 'task') : 'bullet', + bulletMarkerOrDelimiter: isOrderedListItem ? bull.slice(-1) : bull.charAt(0), type: loose ? 'loose_item_start' : 'list_item_start' }) diff --git a/src/muya/lib/parser/marked/parser.js b/src/muya/lib/parser/marked/parser.js index 2b47cae6..dba46461 100644 --- a/src/muya/lib/parser/marked/parser.js +++ b/src/muya/lib/parser/marked/parser.js @@ -165,23 +165,23 @@ Parser.prototype.tok = function () { } case 'list_item_start': { let body = '' - const { checked, listItemType, bulletListItemMarker } = this.token + const { checked } = this.token while (this.next().type !== 'list_item_end') { body += this.token.type === 'text' ? this.parseText() : this.tok() } - return this.renderer.listitem(body, checked, listItemType, bulletListItemMarker, false) + return this.renderer.listitem(body, checked) } case 'loose_item_start': { let body = '' - const { checked, listItemType, bulletListItemMarker } = this.token + const { checked } = this.token while (this.next().type !== 'list_item_end') { body += this.tok() } - return this.renderer.listitem(body, checked, listItemType, bulletListItemMarker, true) + return this.renderer.listitem(body, checked) } case 'html': { // TODO parse inline content if parameter markdown=1 diff --git a/src/muya/lib/parser/marked/renderer.js b/src/muya/lib/parser/marked/renderer.js index 40cab1bd..2c3afa8c 100644 --- a/src/muya/lib/parser/marked/renderer.js +++ b/src/muya/lib/parser/marked/renderer.js @@ -96,7 +96,7 @@ Renderer.prototype.list = function (body, ordered, start, taskList) { return '<' + type + startatt + '>\n' + body + '' + type + '>\n' } -Renderer.prototype.listitem = function (text, checked, listItemType, bulletListItemMarker, loose) { +Renderer.prototype.listitem = function (text, checked) { // normal list if (checked === undefined) { return '