diff --git a/doc/Block addition properties and its value.md b/doc/Block addition properties and its value.md
new file mode 100644
index 00000000..9501314d
--- /dev/null
+++ b/doc/Block addition properties and its value.md
@@ -0,0 +1,161 @@
+### Block addition properties and its value
+
+##### 1. span
+
+- functionType
+
+ - languageInput
+
+ - codeLine
+
+ - atxLine (can not contain soft line break and hard line break use in atx heading)
+
+ - thematicBreakLine (can not contain soft line break and hard line break use in thematic break)
+
+ - paragraphContent (defaultValue use in paragraph and setext heading)
+
+- lang - only when it's code line
+
+ - All prismjs support language or empty string
+
+##### 2. div
+
+used for preview `block math`, `mermaid`, `flowchart`, `vega-lite`, `sequence` and `html block`.
+
+- functionType
+
+ - multiplemath
+
+ - mermaid
+
+ - flowchart
+
+ - vega-lite
+
+ - sequence
+
+ - html
+
+##### 3. figure
+
+The container block of `table`, `html`, `block math`, `mermaid`,`flowchart`,`vega-lite`,`sequence`.
+
+- functionType
+
+ - table
+
+ - html
+
+ - multiplemath
+
+ - mermaid
+
+ - flowchart
+
+ - vega-lite
+
+ - sequence
+
+##### 4. pre
+
+Used for `html`,`block math`,`mermaid`,`flowchart`,`vega-lite`,`sequence` `code block`.
+
+- functionType
+
+ - html
+
+ - multiplemath
+
+ - mermaid
+
+ - flowchart
+
+ - vega-lite
+
+ - sequence
+
+ - fencecode
+
+ - indentcode
+
+ - frontmatter
+
+- lang
+
+ - all prismjs support language or empty string
+
+##### 5. code
+
+- lang
+
+ - all prismjs support language or empty string
+
+##### ul
+
+- listType
+
+ - bullet
+
+ - task
+
+##### ol
+
+- listType
+
+ - order
+
+- start
+
+ - 0-999999999
+
+##### li
+
+- listItemType
+
+ - order
+
+ - bullet
+
+ - task
+
+- isLooseListItem
+
+ - true
+
+ - false
+
+- bulletMarkerOrDelimiter
+
+ - bulletMarker:`-`, `+`, `*`
+
+ - Delimiter: `)`, `.`
+
+##### h1~6
+
+- headingStyle
+
+ - atx
+
+ - setext
+
+- marker - only setext heading has marker
+
+##### input
+
+- checked
+
+ - true
+
+ - false
+
+##### table
+
+- row
+
+- column
+
+##### th/td
+
+- align
+
+- column
diff --git a/package.json b/package.json
index e43bdc3b..6d70f6c5 100644
--- a/package.json
+++ b/package.json
@@ -189,7 +189,7 @@
"keyboard-layout": "^2.0.15",
"mermaid": "^8.0.0",
"popper.js": "^1.15.0",
- "prismjs": "^1.16.0",
+ "prismjs2": "^1.15.1",
"snabbdom": "^0.7.3",
"snabbdom-to-html": "^5.1.1",
"snapsvg": "^0.5.1",
diff --git a/src/main/menu/actions/paragraph.js b/src/main/menu/actions/paragraph.js
index 10d59dc9..605a2318 100644
--- a/src/main/menu/actions/paragraph.js
+++ b/src/main/menu/actions/paragraph.js
@@ -35,11 +35,11 @@ const setParagraphMenuItemStatus = bool => {
.forEach(item => (item.enabled = bool))
}
-const disableNoMultiple = disableLabels => {
+const setMultipleStatus = (list, status) => {
const paragraphMenuItem = getMenuItemById('paragraphMenuEntry')
paragraphMenuItem.submenu.items
- .filter(item => item.id && disableLabels.includes(item.id))
- .forEach(item => (item.enabled = false))
+ .filter(item => item.id && list.includes(item.id))
+ .forEach(item => (item.enabled = status))
}
const setCheckedMenuItem = affiliation => {
@@ -105,15 +105,17 @@ ipcMain.on('AGANI::selection-change', (e, { start, end, affiliation }) => {
(end.type === 'span' && end.block.functionType === 'codeLine')
) {
setParagraphMenuItemStatus(false)
+
if (start.block.functionType === 'codeLine' || end.block.functionType === 'codeLine') {
+ setMultipleStatus(['codeFencesMenuItem'], true)
formatMenuItem.submenu.items.forEach(item => (item.enabled = false))
}
} else if (start.key !== end.key) {
formatMenuItem.submenu.items
.filter(item => item.id && DISABLE_LABELS.includes(item.id))
.forEach(item => (item.enabled = false))
- disableNoMultiple(DISABLE_LABELS)
+ setMultipleStatus(DISABLE_LABELS, false)
} else if (!affiliation.slice(0, 3).some(p => /ul|ol/.test(p.type))) {
- disableNoMultiple(['looseListItemMenuItem'])
+ setMultipleStatus(['looseListItemMenuItem'], false)
}
})
diff --git a/src/muya/lib/assets/styles/index.css b/src/muya/lib/assets/styles/index.css
index da269ae8..d435cbad 100644
--- a/src/muya/lib/assets/styles/index.css
+++ b/src/muya/lib/assets/styles/index.css
@@ -22,7 +22,7 @@ pre {
outline: none;
}
-div.ag-show-quick-insert-hint p.ag-paragraph.ag-active > span.ag-line:first-of-type:empty::after {
+div.ag-show-quick-insert-hint p.ag-paragraph.ag-active > span.ag-paragraph-content:first-of-type:empty::after {
content: 'Type @ to insert';
color: var(--editorColor10);
}
@@ -31,12 +31,17 @@ div.ag-show-quick-insert-hint p.ag-paragraph.ag-active > span.ag-line:first-of-t
position: relative;
}
-.ag-paragraph:empty::after,
-.ag-line:empty::after {
- content: '\200B'
+.ag-atx-line:empty::after,
+.ag-thematic-break-line:empty::after,
+.ag-code-line:empty::after,
+.ag-paragraph-content:empty::after {
+ content: '\200B';
}
-.ag-line {
+.ag-atx-line,
+.ag-thematic-break-line,
+.ag-paragraph-content,
+.ag-code-line {
display: block;
white-space: pre-wrap;
word-break: break-word;
@@ -62,11 +67,15 @@ div.ag-show-quick-insert-hint p.ag-paragraph.ag-active > span.ag-line:first-of-t
margin: 0 5px;
}
-.ag-hard-line-break::after {
+.ag-hard-line-break-space::after {
content: '↩';
opacity: .5;
}
+.ag-line-end {
+ display: block;
+}
+
*:not(.ag-hide)::selection, .ag-selection {
background: var(--selectionColor);
color: var(--editorColor);
@@ -77,8 +86,7 @@ div.ag-show-quick-insert-hint p.ag-paragraph.ag-active > span.ag-line:first-of-t
color: transparent;
}
-figure.ag-container-block pre,
-div.ag-function-html pre.ag-html-block {
+figure.ag-container-block pre {
width: 0;
height: 0;
overflow: hidden;
@@ -90,7 +98,6 @@ div.ag-function-html pre.ag-html-block {
overflow: visible;
}
-div.ag-function-html.ag-active pre.ag-html-block,
figure.ag-active.ag-container-block pre {
position: static;
width: 100%;
@@ -100,11 +107,11 @@ figure.ag-active.ag-container-block pre {
display: block;
}
-div.ag-function-html .ag-html-preview {
+figure[data-role="HTML"] .ag-html-preview {
display: block;
}
-div.ag-function-html.ag-active .ag-html-preview {
+figure[data-role="HTML"].ag-active .ag-html-preview {
display: none;
}
@@ -474,7 +481,6 @@ pre.ag-multiple-math span.ag-code-line:first-of-type:empty::after {
figure,
pre.ag-html-block,
-div.ag-function-html,
pre.ag-fence-code,
pre.ag-indent-code,
li.ag-list-item > p.ag-paragraph {
diff --git a/src/muya/lib/config/index.js b/src/muya/lib/config/index.js
index 317af0e0..402823c9 100644
--- a/src/muya/lib/config/index.js
+++ b/src/muya/lib/config/index.js
@@ -6,7 +6,7 @@ import voidHtmlTags from 'html-tags/void'
// Electron 2.0.2 not support yet! So give a default value 4
export const DEVICE_MEMORY = navigator.deviceMemory || 4 // Get the divice memory number(Chrome >= 63)
export const UNDO_DEPTH = DEVICE_MEMORY >= 4 ? 100 : 50
-export const HAS_TEXT_BLOCK_REG = /^(h\d|span|th|td|hr)/i
+export const HAS_TEXT_BLOCK_REG = /^(span|th|td)/i
export const VOID_HTML_TAGS = voidHtmlTags
export const HTML_TAGS = htmlTags
// TYPE1 ~ TYPE7 according to https://github.github.com/gfm/#html-blocks
@@ -85,6 +85,8 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_FRONT_ICON',
'AG_GRAY',
'AG_HARD_LINE_BREAK',
+ 'AG_HARD_LINE_BREAK_SPACE',
+ 'AG_LINE_END',
'AG_HEADER_TIGHT_SPACE',
'AG_HIDE',
'AG_HIGHLIGHT',
@@ -99,7 +101,6 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_INLINE_RULE',
'AG_LANGUAGE',
'AG_LANGUAGE_INPUT',
- 'AG_LINE',
'AG_LINK',
'AG_LINK_IN_BRACKET',
'AG_LIST_ITEM',
@@ -111,6 +112,7 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_RUBY_TEXT',
'AG_RUBY_RENDER',
'AG_SELECTED',
+ 'AG_SOFT_LINE_BREAK',
'AG_MATH_ERROR',
'AG_MATH_MARKER',
'AG_MATH_RENDER',
@@ -159,7 +161,18 @@ export const DEFAULT_TURNDOWN_CONFIG = {
codeBlockStyle: 'fenced', // fenced or indented
fence: '```', // ``` or ~~~
emDelimiter: '*', // _ or *
- strongDelimiter: '**' // ** or __
+ strongDelimiter: '**', // ** or __
+ blankReplacement (content, node, options) {
+ if (node && node.classList.contains('ag-soft-line-break')) {
+ return LINE_BREAK
+ } else if (node && node.classList.contains('ag-hard-line-break')) {
+ return ' ' + LINE_BREAK
+ } else if (node && node.classList.contains('ag-hard-line-break-sapce')) {
+ return ''
+ } else {
+ return node.isBlock ? '\n\n' : ''
+ }
+ }
}
export const FORMAT_MARKER_MAP = {
diff --git a/src/muya/lib/contentState/arrowCtrl.js b/src/muya/lib/contentState/arrowCtrl.js
index eecc002f..725cf05a 100644
--- a/src/muya/lib/contentState/arrowCtrl.js
+++ b/src/muya/lib/contentState/arrowCtrl.js
@@ -4,7 +4,7 @@ import selection from '../selection'
// If the next block is header, put cursor after the `#{1,6} *`
const adjustOffset = (offset, block, event) => {
- if (/^h\d$/.test(block.type) && event.key === EVENT_KEYS.ArrowDown) {
+ if (/^span$/.test(block.type) && block.functionType === 'atxLine' && event.key === EVENT_KEYS.ArrowDown) {
const match = /^\s{0,3}(?:#{1,6})(?:\s{1,}|$)/.exec(block.text)
if (match) {
return match[0].length
diff --git a/src/muya/lib/contentState/backspaceCtrl.js b/src/muya/lib/contentState/backspaceCtrl.js
index 9ee94ed9..ca45bad7 100644
--- a/src/muya/lib/contentState/backspaceCtrl.js
+++ b/src/muya/lib/contentState/backspaceCtrl.js
@@ -106,11 +106,32 @@ const backspaceCtrl = ContentState => {
if (!start || !end) {
return
}
+
const startBlock = this.getBlock(start.key)
const endBlock = this.getBlock(end.key)
const maybeLastRow = this.getParent(endBlock)
const startOutmostBlock = this.findOutMostBlock(startBlock)
const endOutmostBlock = this.findOutMostBlock(endBlock)
+ // Just for fix delete the last `#` or all the atx heading cause error @fixme
+ if (
+ start.key === end.key &&
+ startBlock.type === 'span' &&
+ startBlock.functionType === 'atxLine'
+ ) {
+ if (
+ start.offset === 0 && end.offset === startBlock.text.length ||
+ start.offset === end.offset && start.offset === 1 && startBlock.text === '#'
+ ) {
+ event.preventDefault()
+ startBlock.text = ''
+ this.cursor = {
+ start: { key: start.key, offset: 0 },
+ end: { key: end.key, offset: 0 }
+ }
+ this.updateToParagraph(this.getParent(startBlock), startBlock)
+ return this.partialRender()
+ }
+ }
// fix: #897
const { text } = startBlock
const tokens = tokenizer(text)
@@ -219,7 +240,7 @@ const backspaceCtrl = ContentState => {
) {
const preBlock = this.getParent(parent)
const pBlock = this.createBlock('p')
- const lineBlock = this.createBlock('span', block.text)
+ const lineBlock = this.createBlock('span', { text: block.text })
const key = lineBlock.key
const offset = 0
this.appendChild(pBlock, lineBlock)
@@ -235,10 +256,8 @@ const backspaceCtrl = ContentState => {
case 'mermaid':
case 'sequence':
case 'vega-lite':
- referenceBlock = this.getParent(preBlock)
- break
case 'html':
- referenceBlock = this.getParent(this.getParent(preBlock))
+ referenceBlock = this.getParent(preBlock)
break
}
this.insertBefore(pBlock, referenceBlock)
diff --git a/src/muya/lib/contentState/clickCtrl.js b/src/muya/lib/contentState/clickCtrl.js
index d11ad07d..8365d8c0 100644
--- a/src/muya/lib/contentState/clickCtrl.js
+++ b/src/muya/lib/contentState/clickCtrl.js
@@ -136,6 +136,7 @@ const clickCtrl = ContentState => {
return
}
this.cursor = cursor
+
return this.partialRender()
})
} else {
diff --git a/src/muya/lib/contentState/codeBlockCtrl.js b/src/muya/lib/contentState/codeBlockCtrl.js
index 99d08cb2..a70e98f1 100644
--- a/src/muya/lib/contentState/codeBlockCtrl.js
+++ b/src/muya/lib/contentState/codeBlockCtrl.js
@@ -72,27 +72,6 @@ const codeBlockCtrl = ContentState => {
this.partialRender()
}
- ContentState.prototype.indentCodeBlockUpdate = function (block) {
- const oldPBlock = this.getParent(block)
- const codeBlock = this.createBlock('code')
- const inputBlock = this.createBlock('span', '')
- const preBlock = this.createBlock('pre')
-
- oldPBlock.children.forEach(child => {
- child.lang = ''
- child.functionType = 'codeLine'
- child.text = child.text.replace(/^ {4}/, '')
- this.appendChild(codeBlock, child)
- })
- codeBlock.lang = preBlock.lang = ''
- inputBlock.functionType = 'languageInput'
- preBlock.functionType = 'indentcode'
- this.appendChild(preBlock, inputBlock)
- this.appendChild(preBlock, codeBlock)
- this.insertBefore(preBlock, oldPBlock)
- this.removeBlock(oldPBlock)
- }
-
/**
* [codeBlockUpdate if block updated to `pre` return true, else return false]
*/
@@ -108,9 +87,9 @@ const codeBlockCtrl = ContentState => {
const match = CODE_UPDATE_REP.exec(text)
if (match || lang) {
const codeBlock = this.createBlock('code')
- const firstLine = this.createBlock('span', code)
+ const firstLine = this.createBlock('span', { text: code })
const language = lang || (match ? match[1] : '')
- const inputBlock = this.createBlock('span', language)
+ const inputBlock = this.createBlock('span', { text: language })
loadLanguage(language)
inputBlock.functionType = 'languageInput'
block.type = 'pre'
diff --git a/src/muya/lib/contentState/containerCtrl.js b/src/muya/lib/contentState/containerCtrl.js
index e48f3faa..447b7a92 100644
--- a/src/muya/lib/contentState/containerCtrl.js
+++ b/src/muya/lib/contentState/containerCtrl.js
@@ -4,44 +4,57 @@ const FUNCTION_TYPE_LANG = {
'flowchart': 'yaml',
'mermaid': 'yaml',
'sequence': 'yaml',
- 'vega-lite': 'yaml'
+ 'vega-lite': 'yaml',
+ 'html': 'markup'
}
const containerCtrl = ContentState => {
ContentState.prototype.createContainerBlock = function (functionType, value = '') {
- const figureBlock = this.createBlock('figure')
- figureBlock.functionType = functionType
+ const figureBlock = this.createBlock('figure', {
+ functionType
+ })
+
const { preBlock, preview } = this.createPreAndPreview(functionType, value)
this.appendChild(figureBlock, preBlock)
this.appendChild(figureBlock, preview)
- this.codeBlocks.set(preBlock.key, value)
return figureBlock
}
ContentState.prototype.createPreAndPreview = function (functionType, value = '') {
- const preBlock = this.createBlock('pre')
- const codeBlock = this.createBlock('code')
- preBlock.functionType = functionType
- preBlock.lang = codeBlock.lang = FUNCTION_TYPE_LANG[functionType]
+ const lang = FUNCTION_TYPE_LANG[functionType]
+ const preBlock = this.createBlock('pre', {
+ functionType,
+ lang
+ })
+ const codeBlock = this.createBlock('code', {
+ lang
+ })
+
this.appendChild(preBlock, codeBlock)
if (typeof value === 'string' && value) {
value.replace(/^\s+/, '').split(LINE_BREAKS_REG).forEach(line => {
- const codeLine = this.createBlock('span', line)
- codeLine.functionType = 'codeLine'
- codeLine.lang = FUNCTION_TYPE_LANG[functionType]
+ const codeLine = this.createBlock('span', {
+ text: line,
+ functionType: 'codeLine',
+ lang
+ })
+
this.appendChild(codeBlock, codeLine)
})
} else {
- const emptyLine = this.createBlock('span')
- emptyLine.functionType = 'codeLine'
- emptyLine.lang = FUNCTION_TYPE_LANG[functionType]
+ const emptyLine = this.createBlock('span', {
+ functionType: 'codeLine',
+ lang
+ })
+
this.appendChild(codeBlock, emptyLine)
}
- const preview = this.createBlock('div', '', false)
- this.codeBlocks.set(preBlock.key, '')
- preview.functionType = functionType
+ const preview = this.createBlock('div', {
+ editable: false,
+ functionType
+ })
return { preBlock, preview }
}
diff --git a/src/muya/lib/contentState/copyCutCtrl.js b/src/muya/lib/contentState/copyCutCtrl.js
index 98852738..912d345b 100644
--- a/src/muya/lib/contentState/copyCutCtrl.js
+++ b/src/muya/lib/contentState/copyCutCtrl.js
@@ -101,6 +101,12 @@ const copyCutCtrl = ContentState => {
hb.replaceWith(pre)
}
+ // Just work for turndown, turndown will add `leading` and `traling` space in line-break.
+ const lineBreaks = wrapper.querySelectorAll('span.ag-soft-line-break, span.ag-hard-line-break')
+ for (const b of lineBreaks) {
+ b.innerHTML = ''
+ }
+
const mathBlock = wrapper.querySelectorAll(`figure.ag-container-block`)
for (const mb of mathBlock) {
const preElement = mb.querySelector('pre[data-role]')
@@ -128,6 +134,7 @@ const copyCutCtrl = ContentState => {
let htmlData = wrapper.innerHTML
const textData = this.htmlToMarkdown(htmlData)
+
htmlData = marked(textData)
return { html: htmlData, text: textData }
}
diff --git a/src/muya/lib/contentState/enterCtrl.js b/src/muya/lib/contentState/enterCtrl.js
index aab699ed..59b18608 100644
--- a/src/muya/lib/contentState/enterCtrl.js
+++ b/src/muya/lib/contentState/enterCtrl.js
@@ -11,6 +11,7 @@ const getIndentSpace = text => {
}
const enterCtrl = ContentState => {
+ // TODO@jocs this function need opti.
ContentState.prototype.chopBlockByCursor = function (block, key, offset) {
const newBlock = this.createBlock('p')
const { children } = block
@@ -28,7 +29,7 @@ const enterCtrl = ContentState => {
this.prependChild(newBlock, activeLine)
} else if (offset < text.length) {
activeLine.text = text.substring(0, offset)
- const newLine = this.createBlock('span', text.substring(offset))
+ const newLine = this.createBlock('span', { text: text.substring(offset) })
this.prependChild(newBlock, newLine)
}
return newBlock
@@ -197,23 +198,39 @@ const enterCtrl = ContentState => {
// handle `shift + enter` insert `soft line break` or `hard line break`
// only cursor in `line block` can create `soft line break` and `hard line break`
// handle line in code block
- if (
- (event.shiftKey && block.type === 'span') ||
- (block.type === 'span' && block.functionType === 'codeLine')
+ if (event.shiftKey && block.type === 'span' && block.functionType === 'paragraphContent') {
+ let { offset } = start
+ const { text, key } = block
+ const indent = getIndentSpace(text)
+ block.text = text.substring(0, offset) + '\n' + indent + text.substring(offset)
+
+ offset += 1 + indent.length
+ this.cursor = {
+ start: { key, offset },
+ end: { key, offset }
+ }
+ return this.partialRender()
+ } else if (
+ block.type === 'span' && block.functionType === 'codeLine'
) {
const { text } = block
const newLineText = text.substring(start.offset)
const autoIndent = checkAutoIndent(text, start.offset)
const indent = getIndentSpace(text)
block.text = text.substring(0, start.offset)
- const newLine = this.createBlock('span', `${indent}${newLineText}`)
- newLine.functionType = block.functionType
- newLine.lang = block.lang
+ const newLine = this.createBlock('span', {
+ text: `${indent}${newLineText}`,
+ functionType: block.functionType,
+ lang: block.lang
+ })
+
this.insertAfter(newLine, block)
let { key } = newLine
let offset = indent.length
if (autoIndent) {
- const emptyLine = this.createBlock('span', indent + ' '.repeat(this.tabSize))
+ const emptyLine = this.createBlock('span', {
+ text: indent + ' '.repeat(this.tabSize)
+ })
emptyLine.functionType = block.functionType
emptyLine.lang = block.lang
this.insertAfter(emptyLine, block)
@@ -221,11 +238,6 @@ const enterCtrl = ContentState => {
offset = indent.length + this.tabSize
}
- if (indent.length >= 4 && !block.preSibling) {
- this.indentCodeBlockUpdate(block)
- offset = offset - 4
- }
-
this.cursor = {
start: { key, offset },
end: { key, offset }
@@ -323,8 +335,14 @@ const enterCtrl = ContentState => {
post = `${PREFIX} ${post}`
}
block.text = pre
- newBlock = this.createBlock(type, post)
- newBlock.headingStyle = block.headingStyle
+ newBlock = this.createBlock(type, {
+ headingStyle: block.headingStyle
+ })
+ const headerContent = this.createBlock('span', {
+ text: post,
+ functionType: block.headingStyle === 'atx'? 'atxLine' : 'paragraphContent'
+ })
+ this.appendChild(newBlock, headerContent)
if (block.marker) {
newBlock.marker = block.marker
}
diff --git a/src/muya/lib/contentState/htmlBlock.js b/src/muya/lib/contentState/htmlBlock.js
index 834f0157..4121d0ff 100644
--- a/src/muya/lib/contentState/htmlBlock.js
+++ b/src/muya/lib/contentState/htmlBlock.js
@@ -2,61 +2,20 @@ import { VOID_HTML_TAGS, HTML_TAGS } from '../config'
import { inlineRules } from '../parser/rules'
const HTML_BLOCK_REG = /^<([a-zA-Z\d-]+)(?=\s|>)[^<>]*?>$/
-const LINE_BREAKS = /\n/
const htmlBlock = ContentState => {
- ContentState.prototype.createCodeInHtml = function (code) {
- const codeContainer = this.createBlock('div')
- codeContainer.functionType = 'html'
- const preview = this.createBlock('div', '', false)
- preview.functionType = 'preview'
- const preBlock = this.createBlock('pre')
- const codeBlock = this.createBlock('code')
- code.split(LINE_BREAKS).forEach(line => {
- const codeLine = this.createBlock('span', line)
- codeLine.functionType = 'codeLine'
- codeLine.lang = 'markup'
- this.appendChild(codeBlock, codeLine)
- })
- this.codeBlocks.set(preBlock.key, code)
- preBlock.lang = 'markup'
- codeBlock.lang = 'markup'
- preBlock.functionType = 'html'
- this.codeBlocks.set(preBlock.key, code)
- this.appendChild(preBlock, codeBlock)
- this.appendChild(codeContainer, preBlock)
- this.appendChild(codeContainer, preview)
- return codeContainer
- }
-
- ContentState.prototype.handleHtmlBlockClick = function (codeWrapper) {
- const id = codeWrapper.id
- const codeBlock = this.getBlock(id).children[0]
- const key = codeBlock.key
- const offset = 0
- this.cursor = {
- start: { key, offset },
- end: { key, offset }
- }
- this.partialRender()
- }
-
ContentState.prototype.createHtmlBlock = function (code) {
const block = this.createBlock('figure')
block.functionType = 'html'
- const htmlBlock = this.createCodeInHtml(code)
- this.appendChild(block, htmlBlock)
+ const { preBlock, preview } = this.createPreAndPreview('html', code)
+ this.appendChild(block, preBlock)
+ this.appendChild(block, preview)
return block
}
ContentState.prototype.initHtmlBlock = function (block) {
let htmlContent = ''
- const text = block.type === 'p'
- ? block.children.map((child => {
- return child.text
- })).join('\n').trim()
- : block.text
-
+ const text = block.children[0].text
const matches = inlineRules.html_tag.exec(text)
if (matches) {
const tag = matches[3]
@@ -83,9 +42,11 @@ const htmlBlock = ContentState => {
block.functionType = 'html'
block.text = htmlContent
block.children = []
- const codeContainer = this.createCodeInHtml(htmlContent)
- this.appendChild(block, codeContainer)
- return codeContainer.children[0] // preBlock
+ const { preBlock, preview } = this.createPreAndPreview('html', htmlContent)
+ this.appendChild(block, preBlock)
+ this.appendChild(block, preview)
+
+ return preBlock // preBlock
}
ContentState.prototype.updateHtmlBlock = function (block) {
diff --git a/src/muya/lib/contentState/index.js b/src/muya/lib/contentState/index.js
index fef8e59b..eb237fce 100644
--- a/src/muya/lib/contentState/index.js
+++ b/src/muya/lib/contentState/index.js
@@ -63,7 +63,6 @@ class ContentState {
this.exemption = new Set()
this.blocks = [ this.createBlockP() ]
this.stateRender = new StateRender(muya)
- this.codeBlocks = new Map()
this.renderRange = [ null, null ]
this.currentCursor = null
// you'll select the outmost block of current cursor when you click the front icon.
@@ -183,24 +182,32 @@ class ContentState {
* A block in Mark Text present a paragraph(block syntax in GFM) or a line in paragraph.
* a `span` block must in a `p block` or `pre block` and `p block`'s children must be `span` blocks.
*/
- createBlock (type = 'span', text = '', editable = true) {
+ createBlock (type = 'span', extras = {}) {
const key = getUniqueId()
- return {
+ const blockData = {
key,
+ text: '',
type,
- text,
- editable,
+ editable: true,
parent: null,
preSibling: null,
nextSibling: null,
children: []
}
+
+ // give span block a default functionType `paragraphContent`
+ if (type === 'span' && !extras.functionType) {
+ blockData.functionType = 'paragraphContent'
+ }
+
+ Object.assign(blockData, extras)
+ return blockData
}
createBlockP (text = '') {
const pBlock = this.createBlock('p')
- const lineBlock = this.createBlock('span', text)
- this.appendChild(pBlock, lineBlock)
+ const contentBlock = this.createBlock('span', { text })
+ this.appendChild(pBlock, contentBlock)
return pBlock
}
diff --git a/src/muya/lib/contentState/inputCtrl.js b/src/muya/lib/contentState/inputCtrl.js
index 8c0a239e..6308b1fa 100644
--- a/src/muya/lib/contentState/inputCtrl.js
+++ b/src/muya/lib/contentState/inputCtrl.js
@@ -32,7 +32,7 @@ const inputCtrl = ContentState => {
// Input @ to quick insert paragraph
ContentState.prototype.checkQuickInsert = function (block) {
const { type, text, functionType } = block
- if (type !== 'span' || functionType) return false
+ if (type !== 'span' || functionType !== 'paragraphContent') return false
return /^@\S*$/.test(text)
}
@@ -88,6 +88,7 @@ const inputCtrl = ContentState => {
const block = this.getBlock(key)
const paragraph = document.querySelector(`#${key}`)
let text = getTextContent(paragraph, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ])
+
let needRender = false
let needRenderAll = false
if (oldStart.key !== oldEnd.key) {
@@ -196,7 +197,26 @@ const inputCtrl = ContentState => {
if (this.checkNotSameToken(block.text, text)) {
needRender = true
}
- block.text = text
+ // Just work for `Shift + Enter` to create a soft and hard line break.
+ if (
+ block.text.endsWith('\n') &&
+ start.offset === text.length &&
+ event.inputType === 'insertText'
+ ) {
+ block.text += event.data
+ start.offset++
+ end.offset++
+ } else if (
+ block.text.length === oldStart.offset &&
+ block.text[oldStart.offset - 2] === '\n' &&
+ event.inputType === 'deleteContentBackward'
+ ) {
+ block.text = block.text.substring(0, oldStart.offset - 1)
+ start.offset = block.text.length
+ end.offset = block.text.length
+ } else {
+ block.text = text
+ }
if (beginRules['reference_definition'].test(text)) {
needRenderAll = true
}
@@ -226,7 +246,6 @@ const inputCtrl = ContentState => {
// Update preview content of math block
if (block && block.type === 'span' && block.functionType === 'codeLine') {
needRender = true
- this.updateCodeBlocks(block)
}
this.cursor = { start, end }
diff --git a/src/muya/lib/contentState/paragraphCtrl.js b/src/muya/lib/contentState/paragraphCtrl.js
index a69d6246..f250d833 100644
--- a/src/muya/lib/contentState/paragraphCtrl.js
+++ b/src/muya/lib/contentState/paragraphCtrl.js
@@ -68,12 +68,19 @@ const paragraphCtrl = ContentState => {
ContentState.prototype.handleFrontMatter = function () {
const firstBlock = this.blocks[0]
if (firstBlock.type === 'pre' && firstBlock.functionType === 'frontmatter') return
- const frontMatter = this.createBlock('pre')
- const codeBlock = this.createBlock('code')
- const emptyLine = this.createBlock('span')
- frontMatter.lang = codeBlock.lang = emptyLine.lang = 'yaml'
- emptyLine.functionType = 'codeLine'
- frontMatter.functionType = 'frontmatter'
+ const lang = 'yaml'
+ const frontMatter = this.createBlock('pre', {
+ functionType: 'frontmatter',
+ lang
+ })
+ const codeBlock = this.createBlock('code', {
+ lang
+ })
+ const emptyLine = this.createBlock('span', {
+ functionType: 'codeLine',
+ lang
+ })
+
this.appendChild(codeBlock, emptyLine)
this.appendChild(frontMatter, codeBlock)
this.insertBefore(frontMatter, firstBlock)
@@ -87,14 +94,13 @@ const paragraphCtrl = ContentState => {
ContentState.prototype.handleListMenu = function (paraType, insertMode) {
const { start, end, affiliation } = this.selectionChange(this.cursor)
- const { orderListMarker, bulletListMarker } = this
+ const { orderListMarker, bulletListMarker, preferLooseListItem } = this
const [blockType, listType] = paraType.split('-')
const isListed = affiliation.slice(0, 3).filter(b => /ul|ol/.test(b.type))
- const { preferLooseListItem } = this
if (isListed.length && !insertMode) {
const listBlock = isListed[0]
- if (listType === isListed[0].listType) {
+ if (listType === listBlock.listType) {
const listItems = listBlock.children
listItems.forEach(listItem => {
listItem.children.forEach(itemParagraph => {
@@ -214,41 +220,74 @@ const paragraphCtrl = ContentState => {
if (affiliation.length && affiliation[0].type === 'pre' && /code/.test(affiliation[0].functionType)) {
const preBlock = affiliation[0]
const codeLines = preBlock.children[1].children
- this.codeBlocks.delete(preBlock.key)
preBlock.type = 'p'
preBlock.children = []
+ const newParagraphBlock = this.createBlockP(codeLines.map(l => l.text).join('\n'))
+ this.insertBefore(newParagraphBlock, preBlock)
+
+ this.removeBlock(preBlock)
+ const { start, end } = this.cursor
+
+ const key = newParagraphBlock.children[0].key
+ let startOffset = 0
+ let endOffset = 0
+ let startStop = false
+ let endStop = false
for (const line of codeLines) {
- delete line.lang
- delete line.functionType
- this.appendChild(preBlock, line)
+ if (line.key !== start.key && !startStop) {
+ startOffset += line.text.length + 1
+ } else {
+ startOffset += start.offset
+ startStop = true
+ }
+ if (line.key !== end.key && !endStop) {
+ endOffset += line.text.length + 1
+ } else {
+ endOffset += end.offset
+ endStop = true
+ }
}
- delete preBlock.lang
- delete preBlock.functionType
this.cursor = {
- start: this.cursor.start,
- end: this.cursor.end
+ start: { key, offset: startOffset },
+ end: { key, offset: endOffset }
}
} else {
if (start.key === end.key) {
if (startBlock.type === 'span') {
- startBlock = this.getParent(startBlock)
- startBlock.type = 'pre'
- const codeBlock = this.createBlock('code')
- const inputBlock = this.createBlock('span', '')
- inputBlock.functionType = 'languageInput'
- startBlock.functionType = 'fencecode'
- startBlock.lang = codeBlock.lang = ''
- const codeLines = startBlock.children
- startBlock.children = []
- codeLines.forEach(line => {
- line.functionType = 'codeLine'
- line.lang = ''
- this.appendChild(codeBlock, line)
+ const anchorBlock = this.getParent(startBlock)
+ const lang = ''
+ const preBlock = this.createBlock('pre', {
+ functionType: 'fencecode',
+ lang
})
- this.appendChild(startBlock, inputBlock)
- this.appendChild(startBlock, codeBlock)
+
+ const codeBlock = this.createBlock('code', {
+ lang: ''
+ })
+
+ const inputBlock = this.createBlock('span', {
+ functionType: 'languageInput'
+ })
+
+ const codes = startBlock.text.split('\n')
+
+ for (const code of codes) {
+ const codeLine = this.createBlock('span', {
+ text: code,
+ functionType: 'codeLine',
+ lang
+ })
+ this.appendChild(codeBlock, codeLine)
+ }
+
+ this.appendChild(preBlock, inputBlock)
+ this.appendChild(preBlock, codeBlock)
+ this.insertBefore(preBlock, anchorBlock)
+
+ this.removeBlock(anchorBlock)
+
const { key } = inputBlock
const offset = 0
@@ -266,21 +305,31 @@ const paragraphCtrl = ContentState => {
const { parent, startIndex, endIndex } = this.getCommonParent()
const children = parent ? parent.children : this.blocks
const referBlock = children[endIndex]
- const preBlock = this.createBlock('pre')
- const codeBlock = this.createBlock('code')
- preBlock.functionType = 'fencecode'
- preBlock.lang = codeBlock.lang = ''
+ const lang = ''
+ const preBlock = this.createBlock('pre', {
+ functionType: 'fencecode',
+ lang
+ })
+ const codeBlock = this.createBlock('code', {
+ lang
+ })
+
const listIndentation = this.listIndentation
const markdown = new ExportMarkdown(children.slice(startIndex, endIndex + 1), listIndentation).generate()
markdown.split(LINE_BREAKS_REG).forEach(text => {
- const codeLine = this.createBlock('span', text)
- codeLine.lang = ''
- codeLine.functionType = 'codeLine'
+ const codeLine = this.createBlock('span', {
+ text,
+ lang,
+ functionType: 'codeLine'
+ })
+
this.appendChild(codeBlock, codeLine)
})
- const inputBlock = this.createBlock('span', '')
- inputBlock.functionType = 'languageInput'
+ const inputBlock = this.createBlock('span', {
+ functionType: 'languageInput'
+ })
+
this.appendChild(preBlock, inputBlock)
this.appendChild(preBlock, codeBlock)
this.insertAfter(preBlock, referBlock)
@@ -392,7 +441,7 @@ const paragraphCtrl = ContentState => {
ContentState.prototype.updateParagraph = function (paraType, insertMode = false) {
const { start, end } = this.cursor
const block = this.getBlock(start.key)
- const { type, text, functionType } = block
+ const { text } = block
switch (paraType) {
case 'front-matter': {
@@ -445,15 +494,19 @@ const paragraphCtrl = ContentState => {
case 'degrade heading':
case 'paragraph': {
if (start.key !== end.key) return
- const [, hash, partText] = /(^#*\s*)(.*)/.exec(text)
+ const headingStyle = DEFAULT_TURNDOWN_CONFIG.headingStyle
+ const parent = this.getParent(block)
+ // \u00A0 is
+ const [, hash, partText] = /(^ {0,3}#*[ \u00A0]*)([\s\S]*)/.exec(text)
let newLevel = 0 // 1, 2, 3, 4, 5, 6
let newType = 'p'
let key
+
if (/\d/.test(paraType)) {
newLevel = Number(paraType.split(/\s/)[1])
newType = `h${newLevel}`
} else if (paraType === 'upgrade heading' || paraType === 'degrade heading') {
- const currentLevel = getCurrentLevel(type)
+ const currentLevel = getCurrentLevel(parent.type)
newLevel = currentLevel
if (paraType === 'upgrade heading' && currentLevel !== 1) {
if (currentLevel === 0) newLevel = 6
@@ -475,48 +528,35 @@ const paragraphCtrl = ContentState => {
? '#'.repeat(newLevel) + `${String.fromCharCode(160)}${partText}` // code: 160
: partText
- if (block.type === 'span' && newType !== 'p') {
- const header = this.createBlock(newType, newText)
- header.headingStyle = DEFAULT_TURNDOWN_CONFIG.headingStyle
- key = header.key
- const parent = this.getParent(block)
- if (this.isOnlyChild(block)) {
- this.insertBefore(header, parent)
- this.removeBlock(parent)
- } else if (this.isFirstChild(block)) {
- this.insertBefore(header, parent)
- this.removeBlock(block)
- } else if (this.isLastChild(block)) {
- this.insertAfter(header, parent)
- this.removeBlock(block)
- } else {
- const pBlock = this.createBlock('p')
- let nextSibling = this.getNextSibling(block)
- while (nextSibling) {
- this.appendChild(pBlock, nextSibling)
- const oldNextSibling = nextSibling
- nextSibling = this.getNextSibling(nextSibling)
- this.removeBlock(oldNextSibling)
- }
- this.removeBlock(block)
- this.insertAfter(header, parent)
- this.insertAfter(pBlock, header)
- }
- } else if (/^h/.test(block.type) && newType === 'p') {
+ // No change
+ if (newType === 'p' && parent.type === newType) {
+ return
+ }
+ // No change
+ if (newType !== 'p' && parent.type === newType && parent.headingStyle === headingStyle) {
+ return
+ }
+
+ if (newType !== 'p') {
+ const header = this.createBlock(newType, {
+ headingStyle
+ })
+ const headerContent = this.createBlock('span', {
+ text: headingStyle === 'atx'? newText.replace(/\n/g, ' ') : newText,
+ functionType: headingStyle === 'atx'? 'atxLine' : 'paragraphContent'
+ })
+ this.appendChild(header, headerContent)
+ key = headerContent.key
+
+ this.insertBefore(header, parent)
+ this.removeBlock(parent)
+ } else {
const pBlock = this.createBlockP(newText)
key = pBlock.children[0].key
- this.insertAfter(pBlock, block)
- this.removeBlock(block)
- } else if (type === 'span' && !functionType && newType === 'p') {
- // The original is a paragraph, the new type is also paragraph, no need to update.
- return
- } else {
- const newHeader = this.createBlock(newType, newText)
- newHeader.headingStyle = DEFAULT_TURNDOWN_CONFIG.headingStyle
- key = newHeader.key
- this.insertAfter(newHeader, block)
- this.removeBlock(block)
+ this.insertAfter(pBlock, parent)
+ this.removeBlock(parent)
}
+
this.cursor = {
start: { key, offset: startOffset },
end: { key, offset: endOffset }
@@ -527,7 +567,11 @@ const paragraphCtrl = ContentState => {
const pBlock = this.createBlockP()
const archor = block.type === 'span' ? this.getParent(block) : block
const hrBlock = this.createBlock('hr')
- hrBlock.text = '---'
+ const thematicContent = this.createBlock('span', {
+ functionType: 'thematicBreakLine',
+ text: '---'
+ })
+ this.appendChild(hrBlock, thematicContent)
this.insertAfter(hrBlock, archor)
this.insertAfter(pBlock, hrBlock)
if (!text) {
@@ -562,42 +606,23 @@ const paragraphCtrl = ContentState => {
const { start, end } = this.cursor
// if cursor is not in one line or paragraph, can not insert paragraph
if (start.key !== end.key) return
- let block = this.getBlock(start.key)
+ const block = this.getBlock(start.key)
+ let anchor = null
if (outMost) {
- block = this.findOutMostBlock(block)
- } else if (block.type === 'span' && !block.functionType) {
- block = this.getParent(block)
- } else if (block.type === 'span' && block.functionType === 'codeLine') {
- const preBlock = this.getParent(this.getParent(block))
- switch (preBlock.functionType) {
- case 'fencecode':
- case 'indentcode':
- case 'frontmatter': {
- // You can not insert paragraph before frontmatter
- if (preBlock.functionType === 'frontmatter' && location === 'before') {
- return
- }
- block = preBlock
- break
- }
- case 'html': {
- block = this.getParent(this.getParent(preBlock))
- break
- }
- case 'multiplemath': {
- block = this.getParent(preBlock)
- break
- }
- }
- } else if (/th|td/.test(block.type)) {
- // get figure block from table cell
- block = this.getParent(this.getParent(this.getParent(this.getParent(block))))
+ anchor = this.findOutMostBlock(block)
+ } else {
+ anchor = this.getAnchor(block)
}
+ // You can not insert paragraph before frontmatter
+ if (!anchor || anchor && anchor.functionType === 'frontmatter' && location === 'before') {
+ return
+ }
+
const newBlock = this.createBlockP(text)
if (location === 'before') {
- this.insertBefore(newBlock, block)
+ this.insertBefore(newBlock, anchor)
} else {
- this.insertAfter(newBlock, block)
+ this.insertAfter(newBlock, anchor)
}
const { key } = newBlock.children[0]
const offset = text.length
@@ -634,12 +659,7 @@ const paragraphCtrl = ContentState => {
// if copied block has pre block: html, multiplemath, vega-light, mermaid, flowchart, sequence...
const copiedBlock = this.copyBlock(startOutmostBlock)
this.insertAfter(copiedBlock, startOutmostBlock)
- if (copiedBlock.type === 'figure' && copiedBlock.functionType) {
- const preBlock = this.getPreBlock(copiedBlock)
- if (preBlock) {
- this.updateCodeBlocks(preBlock.children[0].children[0])
- }
- }
+
const cursorBlock = this.firstInDescendant(copiedBlock)
// set cursor at the end of the first descendant of the duplicated block.
const { key, text } = cursorBlock
diff --git a/src/muya/lib/contentState/pasteCtrl.js b/src/muya/lib/contentState/pasteCtrl.js
index 0aa59770..005c1d67 100644
--- a/src/muya/lib/contentState/pasteCtrl.js
+++ b/src/muya/lib/contentState/pasteCtrl.js
@@ -8,21 +8,33 @@ const pasteCtrl = ContentState => {
// check paste type: `MERGE` or `NEWLINE`
ContentState.prototype.checkPasteType = function (start, fragment) {
const fragmentType = fragment.type
- if (start.type === 'span') {
- start = this.getParent(start)
- }
- if (fragmentType === 'p') return 'MERGE'
- if (fragmentType === 'blockquote') return 'NEWLINE'
- let parent = this.getParent(start)
- if (parent && parent.type === 'li') parent = this.getParent(parent)
- let startType = start.type
- if (start.type === 'p') {
- startType = parent ? parent.type : startType
- }
- if (LIST_REG.test(fragmentType) && LIST_REG.test(startType)) {
+ const parent = this.getParent(start)
+
+ if (fragmentType === 'p') {
return 'MERGE'
+ } else if (/^h\d/.test(fragmentType)) {
+ if (start.text) {
+ return 'MERGE'
+ } else {
+ return 'NEWLINE'
+ }
+ } else if (LIST_REG.test(fragmentType)) {
+ const listItem = this.getParent(parent)
+ const list = listItem && listItem.type === 'li' ? this.getParent(listItem) : null
+ if (list) {
+ if (
+ list.listType === fragment.listType &&
+ listItem.bulletMarkerOrDelimiter === fragment.children[0].bulletMarkerOrDelimiter
+ ) {
+ return 'MERGE'
+ } else {
+ return 'NEWLINE'
+ }
+ } else {
+ return 'NEWLINE'
+ }
} else {
- return startType === fragmentType ? 'MERGE' : 'NEWLINE'
+ return 'NEWLINE'
}
}
@@ -137,7 +149,7 @@ const pasteCtrl = ContentState => {
startBlock.text = prePartText + line
} else {
line = i === textList.length - 1 ? line + postPartText : line
- const lineBlock = this.createBlock('span', line)
+ const lineBlock = this.createBlock('span', { text: line })
lineBlock.functionType = startBlock.functionType
lineBlock.lang = startBlock.lang
this.insertAfter(lineBlock, referenceBlock)
@@ -161,7 +173,6 @@ const pasteCtrl = ContentState => {
end: { key, offset }
}
}
- this.updateCodeBlocks(startBlock)
return this.partialRender()
}
@@ -179,41 +190,12 @@ const pasteCtrl = ContentState => {
// handle copyAsHtml
if (copyType === 'copyAsHtml') {
- // already handle code block above
- if (startBlock.type === 'span' && startBlock.nextSibling) {
- const afterParagraph = this.createBlock('p')
- let temp = startBlock
- const removeCache = []
- while (temp.nextSibling) {
- temp = this.getBlock(temp.nextSibling)
- this.appendChild(afterParagraph, temp)
- removeCache.push(temp)
- }
- removeCache.forEach(b => this.removeBlock(b))
- this.insertAfter(afterParagraph, parent)
- startBlock.nextSibling = null
- }
switch (type) {
case 'normal': {
- const htmlBlock = this.createBlock('p')
- const lines = text.trim().split(LINE_BREAKS_REG).map(line => this.createBlock('span', line))
- for (const line of lines) {
- this.appendChild(htmlBlock, line)
- }
- if (startBlock.type === 'span') {
- this.insertAfter(htmlBlock, parent)
- } else {
- this.insertAfter(htmlBlock, startBlock)
- }
- if (
- startBlock.type === 'span' && startBlock.text.length === 0 && this.isOnlyChild(startBlock)
- ) {
- this.removeBlock(parent)
- }
+ const htmlBlock = this.createBlockP(text.trim())
+ this.insertAfter(htmlBlock, parent)
+ this.removeBlock(parent)
// handler heading
- if (startBlock.text.length === 0 && startBlock.type !== 'span') {
- this.removeBlock(startBlock)
- }
this.insertHtmlBlock(htmlBlock)
break
}
@@ -222,24 +204,16 @@ const pasteCtrl = ContentState => {
let htmlBlock = null
if (!startBlock.text || lines.length > 1) {
- htmlBlock = this.createBlock('p')
- ;(startBlock.text ? lines.slice(1) : lines).map(line => this.createBlock('span', line))
- .forEach(l => {
- this.appendChild(htmlBlock, l)
- })
+ htmlBlock = this.createBlockP((startBlock.text ? lines.slice(1) : lines).join('\n'))
}
if (htmlBlock) {
- if (startBlock.type === 'span') {
- this.insertAfter(htmlBlock, parent)
- } else {
- this.insertAfter(htmlBlock, startBlock)
- }
+ this.insertAfter(htmlBlock, parent)
this.insertHtmlBlock(htmlBlock)
}
if (startBlock.text) {
appendHtml(lines[0])
} else {
- this.removeBlock(startBlock.type === 'span' ? parent : startBlock)
+ this.removeBlock(parent)
}
break
}
@@ -300,7 +274,6 @@ const pasteCtrl = ContentState => {
if (liChildren[0].type === 'p') {
// TODO @JOCS
startBlock.text += liChildren[0].children[0].text
- liChildren[0].children.slice(1).forEach(c => this.appendChild(parent, c))
const tail = liChildren.slice(1)
if (tail.length) {
tail.forEach(t => {
@@ -323,33 +296,21 @@ const pasteCtrl = ContentState => {
this.insertAfter(block, target)
target = block
})
- } else {
- if (firstFragment.type === 'p') {
- if (/^h\d$/.test(startBlock.type)) {
- // handle paste into header
- startBlock.text += firstFragment.children[0].text
- if (firstFragment.children.length > 1) {
- const newParagraph = this.createBlock('p')
- firstFragment.children.slice(1).forEach(line => {
- this.appendChild(newParagraph, line)
- })
- this.insertAfter(newParagraph, startBlock)
- }
- } else {
- startBlock.text += firstFragment.children[0].text
- firstFragment.children.slice(1).forEach(line => {
- if (startBlock.functionType) line.functionType = startBlock.functionType
- if (startBlock.lang) line.lang = startBlock.lang
- this.appendChild(parent, line)
- })
+ } else if (firstFragment.type === 'p' || /^h\d/.test(firstFragment.type)) {
+ const text = firstFragment.children[0].text
+ const lines = text.split('\n')
+ let target = parent
+ if (parent.headingStyle === 'atx') {
+ startBlock.text += lines[0]
+ if (lines.length > 1) {
+ const pBlock = this.createBlockP(lines.slice(1).join('\n'))
+ this.insertAfter(parent, pBlock)
+ target = pBlock
}
- } else if (/^h\d$/.test(firstFragment.type)) {
- startBlock.text += firstFragment.text.split(/\s+/)[1]
} else {
- startBlock.text += firstFragment.text
+ startBlock.text += text
}
-
- let target = /^h\d$/.test(startBlock.type) ? startBlock : parent
+
tailFragments.forEach(block => {
this.insertAfter(block, target)
target = block
@@ -358,14 +319,13 @@ const pasteCtrl = ContentState => {
break
}
case 'NEWLINE': {
- let target = startBlock.type === 'span' ? parent : startBlock
+ let target = parent
stateFragments.forEach(block => {
this.insertAfter(block, target)
target = block
})
if (startBlock.text.length === 0) {
- this.removeBlock(startBlock)
- if (this.isOnlyChild(startBlock) && startBlock.type === 'span') this.removeBlock(parent)
+ this.removeBlock(parent)
}
break
}
@@ -380,10 +340,7 @@ const pasteCtrl = ContentState => {
offset = startBlock.text.length - cacheText.length
cursorBlock = startBlock
}
- // TODO @Jocs duplicate with codes in updateCtrl.js
- if (cursorBlock && cursorBlock.type === 'span' && cursorBlock.functionType === 'codeLine') {
- this.updateCodeBlocks(cursorBlock)
- }
+
this.cursor = {
start: {
key, offset
diff --git a/src/muya/lib/contentState/tableBlockCtrl.js b/src/muya/lib/contentState/tableBlockCtrl.js
index 9fb2f255..76860b71 100644
--- a/src/muya/lib/contentState/tableBlockCtrl.js
+++ b/src/muya/lib/contentState/tableBlockCtrl.js
@@ -18,7 +18,9 @@ const tableBlockCtrl = ContentState => {
const rowBlock = this.createBlock('tr')
i === 0 ? this.appendChild(tHead, rowBlock) : this.appendChild(tBody, rowBlock)
for (j = 0; j < columns; j++) {
- const cell = this.createBlock(i === 0 ? 'th' : 'td', headerTexts && i === 0 ? headerTexts[j] : '')
+ const cell = this.createBlock(i === 0 ? 'th' : 'td', {
+ text: headerTexts && i === 0 ? headerTexts[j] : ''
+ })
this.appendChild(rowBlock, cell)
cell.align = ''
cell.column = j
@@ -41,24 +43,18 @@ const tableBlockCtrl = ContentState => {
ContentState.prototype.getAnchor = function (block) {
const { type, functionType } = block
- switch (true) {
- case /^span$/.test(type): {
- if (!functionType) {
- return this.closest(block, 'p')
- } else if (functionType === 'codeLine') {
+ switch (type) {
+ case 'span':
+ if (functionType === 'codeLine') {
return this.closest(block, 'figure') || this.closest(block, 'pre')
+ } else {
+ return this.getParent(block)
}
- return null
- }
- case /^(th|td)$/.test(type): {
+
+ case 'th':
+ case 'td':
return this.closest(block, 'figure')
- }
- case /^h\d$/.test(type): {
- return block
- }
- case /hr/.test(type): {
- return block
- }
+
default:
return null
}
@@ -74,7 +70,7 @@ const tableBlockCtrl = ContentState => {
if (!anchor) return
this.insertAfter(figureBlock, anchor)
- if (anchor.type === 'p' && !endBlock.text) {
+ if (/p|h\d/.test(anchor.type) && !endBlock.text) {
this.removeBlock(anchor)
}
this.appendChild(figureBlock, table)
diff --git a/src/muya/lib/contentState/updateCtrl.js b/src/muya/lib/contentState/updateCtrl.js
index f3b69e9a..a5b21a45 100644
--- a/src/muya/lib/contentState/updateCtrl.js
+++ b/src/muya/lib/contentState/updateCtrl.js
@@ -3,13 +3,14 @@ import { conflict } from '../utils'
import { CLASS_OR_ID } from '../config'
const INLINE_UPDATE_FRAGMENTS = [
- '^([*+-]\\s)', // Bullet list
- '^(\\[[x\\s]{1}\\]\\s)', // Task 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
- '^\\s{0,3}((?:\\*\\s*\\*\\s*\\*|-\\s*-\\s*-|_\\s*_\\s*_)[\\s\\*\\-\\_]*)$' // Thematic break
+ '(?:^|\n) {0,3}([*+-] {1,4})', // Bullet list
+ '(?:^|\n)(\\[[x ]{1}\\] {1,4})', // Task list
+ '(?:^|\n) {0,3}(\\d{1,9}(?:\\.|\\)) {1,4})', // Order list
+ '(?:^|\n) {0,3}(#{1,6})(?=\\s{1,}|$)', // ATX headings
+ '^(?:[\\s\\S]+?)\\n {0,3}(\\={3,}|\\-{3,})(?= {1,}|$)', // Setext headings **match from beginning**
+ '(?:^|\n) {0,3}(>).+', // Block quote
+ '^( {4,})', // Indent code **match from beginning**
+ '(?:^|\n) {0,3}((?:\\* *\\* *\\*|- *- *-|_ *_ *_)[ \\*\\-\\_]*)$' // Thematic break
]
const INLINE_UPDATE_REG = new RegExp(INLINE_UPDATE_FRAGMENTS.join('|'), 'i')
@@ -21,11 +22,6 @@ const updateCtrl = ContentState => {
const block = this.getBlock(id)
block.checked = checked
checkbox.classList.toggle(CLASS_OR_ID['AG_CHECKBOX_CHECKED'])
- // this.render()
- }
-
- ContentState.prototype.checkSameLooseType = function (list, isLooseType) {
- return list.children[0].isLooseListItem === isLooseType
}
ContentState.prototype.checkSameMarkerOrDelimiter = function (list, markerOrDelimiter) {
@@ -40,9 +36,10 @@ const updateCtrl = ContentState => {
const endBlock = this.getBlock(cEnd ? cEnd.key : focus.key)
const startOffset = cStart ? cStart.offset : anchor.offset
const endOffset = cEnd ? cEnd.offset : focus.offset
+ const NO_NEED_TOKEN_REG = /text|hard_line_break|soft_line_break/
for (const token of tokenizer(startBlock.text, undefined, undefined, labels)) {
- if (token.type === 'text') continue
+ if (NO_NEED_TOKEN_REG.test(token.type)) continue
const { start, end } = token.range
const textLen = startBlock.text.length
if (
@@ -52,7 +49,7 @@ const updateCtrl = ContentState => {
}
}
for (const token of tokenizer(endBlock.text, undefined, undefined, labels)) {
- if (token.type === 'text') continue
+ if (NO_NEED_TOKEN_REG.test(token.type)) continue
const { start, end } = token.range
const textLen = endBlock.text.length
if (
@@ -65,34 +62,35 @@ const updateCtrl = ContentState => {
return false
}
+ /**
+ * block must be span block.
+ */
ContentState.prototype.checkInlineUpdate = function (block) {
// table cell can not have blocks in it
if (/th|td|figure/.test(block.type)) return false
if (/codeLine|languageInput/.test(block.functionType)) return false
- // line in paragraph can also update to other block. So comment bellow code.
- // if (block.type === 'span' && block.preSibling) return false
- const hasPreLine = !!(block.type === 'span' && block.preSibling)
+
let line = null
const { text } = block
if (block.type === 'span') {
line = block
block = this.getParent(block)
}
- const parent = this.getParent(block)
- const [match, bullet, tasklist, order, atxHeader, setextHeader, blockquote, hr] = text.match(INLINE_UPDATE_REG) || []
+ const listItem = this.getParent(block)
+ const [
+ match, bullet, tasklist, order, atxHeader,
+ setextHeader, blockquote, indentCode, hr
+ ] = text.match(INLINE_UPDATE_REG) || []
switch (true) {
- case (
- (!!hr && new Set(hr.split('').filter(i => /\S/.test(i))).size === 1) ||
- (!!setextHeader && !hasPreLine)
- ):
- return this.updateHr(block, hr || setextHeader)
+ case (!!hr && new Set(hr.split('').filter(i => /\S/.test(i))).size === 1):
+ return this.updateHr(block, hr, line)
case !!bullet:
return this.updateList(block, 'bullet', bullet, line)
// only `bullet` list item can be update to `task` list item
- case !!tasklist && parent && parent.listItemType === 'bullet':
+ case !!tasklist && listItem && listItem.listItemType === 'bullet':
return this.updateTaskListItem(block, 'tasklist', tasklist)
case !!order:
@@ -101,75 +99,112 @@ const updateCtrl = ContentState => {
case !!atxHeader:
return this.updateAtxHeader(block, atxHeader, line)
- case !!setextHeader && hasPreLine:
+ case !!setextHeader:
return this.updateSetextHeader(block, setextHeader, line)
case !!blockquote:
return this.updateBlockQuote(block, line)
+ case !!indentCode:
+ return this.updateIndentCode(block, line)
+
case !match:
default:
- return this.updateToParagraph(block)
+ return this.updateToParagraph(block, line)
}
}
- // thematic break
- ContentState.prototype.updateHr = function (block, marker) {
- if (block.type !== 'hr') {
- block.type = 'hr'
- block.text = marker
- block.children.length = 0
- const { key } = block
- this.cursor.start.key = this.cursor.end.key = key
- return block
+ // Thematic break
+ ContentState.prototype.updateHr = function (block, marker, line) {
+ // If the block is already thematic break, no need to update.
+ if (block.type === 'hr') return null
+ const text = line.text
+ const lines = text.split('\n')
+ const preParagraphLines = []
+ let thematicLine = ''
+ const postParagraphLines = []
+ let thematicLineHasPushed = false
+
+ for (const l of lines) {
+ if (/ {0,3}(?:\\* *\\* *\\*|- *- *-|_ *_ *_)[ \\*\\-\\_]*$/.test(l) && !thematicLineHasPushed) {
+ thematicLine = l
+ thematicLineHasPushed = true
+ } else if (!thematicLineHasPushed) {
+ preParagraphLines.push(l)
+ } else {
+ postParagraphLines.push(l)
+ }
}
- return null
+
+ const thematicBlock = this.createBlock('hr')
+ const thematicLineBlock = this.createBlock('span', {
+ text: thematicLine,
+ functionType: 'thematicBreakLine'
+ })
+ this.appendChild(thematicBlock, thematicLineBlock)
+ this.insertBefore(thematicBlock, block)
+ if (preParagraphLines.length) {
+ const preBlock = this.createBlockP(preParagraphLines.join('\n'))
+ this.insertBefore(preBlock, thematicBlock)
+ }
+ if (postParagraphLines.length) {
+ const postBlock = this.createBlockP(postParagraphLines.join('\n'))
+ this.insertAfter(postBlock, thematicBlock)
+ }
+
+ this.removeBlock(block)
+ const { start, end } = this.cursor
+ const key = thematicBlock.children[0].key
+ this.cursor = {
+ start: { key, offset: start.offset },
+ end: { key, offset: end.offset }
+ }
+ return thematicBlock
}
ContentState.prototype.updateList = function (block, type, marker = '', line) {
- 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`
const { start, end } = this.cursor
const startOffset = start.offset
const endOffset = end.offset
- const newBlock = this.createBlock('li')
+ const newListItemBlock = this.createBlock('li')
+ const LIST_ITEM_REG = /^ {0,3}(?:[*+-]|\d{1,9}(?:\.|\))) {0,4}/
+ const text = line.text
+ const lines = text.split('\n')
- if (/^h\d$/.test(block.type)) {
- delete block.marker
- delete block.headingStyle
- block.type = 'p'
- block.children = []
- const line = this.createBlock('span', block.text.substring(marker.length))
- block.text = ''
- this.appendChild(block, line)
- } else {
- line.text = line.text.substring(marker.length)
- const paragraphBefore = this.createBlock('p')
- const index = block.children.indexOf(line)
- if (index !== 0) {
- const removeCache = []
- for (const child of block.children) {
- if (child === line) break
- removeCache.push(child)
- }
- removeCache.forEach(c => {
- this.removeBlock(c)
- this.appendChild(paragraphBefore, c)
- })
- this.insertBefore(paragraphBefore, block)
+ const preParagraphLines = []
+ const listItemLines = []
+ let isPushedListItemLine = false
+ for (const l of lines) {
+ if (LIST_ITEM_REG.test(l) && !isPushedListItemLine) {
+ listItemLines.push(l.replace(LIST_ITEM_REG, ''))
+ isPushedListItemLine = true
+ } else if (!isPushedListItemLine) {
+ preParagraphLines.push(l)
+ } else {
+ listItemLines.push(l)
}
}
+ const pBlock = this.createBlockP(listItemLines.join('\n'))
+ this.insertBefore(pBlock, block)
+
+ if (preParagraphLines.length > 0) {
+ const preParagraphBlock = this.createBlockP(preParagraphLines.join('\n'))
+ this.insertBefore(preParagraphBlock, pBlock)
+ }
+
+ this.removeBlock(block)
+
+ // important!
+ block = pBlock
+
const preSibling = this.getPreSibling(block)
const nextSibling = this.getNextSibling(block)
- newBlock.listItemType = type
- newBlock.isLooseListItem = preferLooseListItem
+ newListItemBlock.listItemType = type
+ newListItemBlock.isLooseListItem = preferLooseListItem
let bulletMarkerOrDelimiter
if (type === 'order') {
@@ -178,7 +213,7 @@ const updateCtrl = ContentState => {
const { bulletListMarker } = this
bulletMarkerOrDelimiter = marker ? marker.charAt(0) : bulletListMarker
}
- newBlock.bulletMarkerOrDelimiter = bulletMarkerOrDelimiter
+ newListItemBlock.bulletMarkerOrDelimiter = bulletMarkerOrDelimiter
// Special cases for CommonMark 264 and 265: Changing the bullet or ordered list delimiter starts a new list.
// Same list type or new list
@@ -188,7 +223,7 @@ const updateCtrl = ContentState => {
nextSibling &&
this.checkSameMarkerOrDelimiter(nextSibling, bulletMarkerOrDelimiter)
) {
- this.appendChild(preSibling, newBlock)
+ this.appendChild(preSibling, newListItemBlock)
const partChildren = nextSibling.children.splice(0)
partChildren.forEach(b => this.appendChild(preSibling, b))
this.removeBlock(nextSibling)
@@ -199,7 +234,7 @@ const updateCtrl = ContentState => {
preSibling &&
this.checkSameMarkerOrDelimiter(preSibling, bulletMarkerOrDelimiter)
) {
- this.appendChild(preSibling, newBlock)
+ this.appendChild(preSibling, newListItemBlock)
this.removeBlock(block)
const isLooseListItem = preSibling.children.some(c => c.isLooseListItem)
preSibling.children.forEach(c => c.isLooseListItem = isLooseListItem)
@@ -207,67 +242,59 @@ const updateCtrl = ContentState => {
nextSibling &&
this.checkSameMarkerOrDelimiter(nextSibling, bulletMarkerOrDelimiter)
) {
- this.insertBefore(newBlock, nextSibling.children[0])
+ this.insertBefore(newListItemBlock, nextSibling.children[0])
this.removeBlock(block)
const isLooseListItem = nextSibling.children.some(c => c.isLooseListItem)
nextSibling.children.forEach(c => c.isLooseListItem = isLooseListItem)
- } else if (
- // todo@jocs remove this if in 0.15.xx
- parent &&
- parent.listType === type &&
- this.checkSameLooseType(parent, preferLooseListItem)
- ) {
- this.insertBefore(newBlock, block)
- this.removeBlock(block)
} else {
// Create a new list when changing list type, bullet or list delimiter
- const listBlock = this.createBlock(wrapperTag)
- listBlock.listType = type
+ const listBlock = this.createBlock(wrapperTag, {
+ 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.appendChild(listBlock, newListItemBlock)
this.insertBefore(listBlock, block)
this.removeBlock(block)
}
// key point
- this.appendChild(newBlock, block)
- const TASK_LIST_REG = /^\[[x ]\] /i
+ this.appendChild(newListItemBlock, block)
+ const TASK_LIST_REG = /^\[[x ]\] {1,4}/i
const listItemText = block.children[0].text
+ const { key } = block.children[0]
+ const delta = marker.length + preParagraphLines.join('\n').length + 1
+ this.cursor = {
+ start: {
+ key,
+ offset: Math.max(0, startOffset - delta)
+ },
+ end: {
+ key,
+ offset: Math.max(0, endOffset - delta)
+ }
+ }
if (TASK_LIST_REG.test(listItemText)) {
const [,,tasklist,,,,] = listItemText.match(INLINE_UPDATE_REG) || []
return this.updateTaskListItem(block, 'tasklist', tasklist)
} else {
- const { key } = block.children[0]
- this.cursor = {
- start: {
- key,
- offset: Math.max(0, startOffset - marker.length)
- },
- end: {
- key,
- offset: Math.max(0, endOffset - marker.length)
- }
- }
return block
}
}
ContentState.prototype.updateTaskListItem = function (block, type, marker = '') {
- if (block.type === 'span') {
- block = this.getParent(block)
- }
-
const { preferLooseListItem } = this
const parent = this.getParent(block)
const grandpa = this.getParent(parent)
const checked = /\[x\]\s/i.test(marker) // use `i` flag to ignore upper case or lower case
- const checkbox = this.createBlock('input')
+ const checkbox = this.createBlock('input', {
+ checked
+ })
const { start, end } = this.cursor
- checkbox.checked = checked
this.insertBefore(checkbox, block)
block.children[0].text = block.children[0].text.substring(marker.length)
parent.listItemType = 'task'
@@ -277,16 +304,21 @@ const updateCtrl = ContentState => {
if (this.isOnlyChild(parent)) {
grandpa.listType = 'task'
} else if (this.isFirstChild(parent) || this.isLastChild(parent)) {
- taskListWrapper = this.createBlock('ul')
- taskListWrapper.listType = 'task'
+ taskListWrapper = this.createBlock('ul', {
+ listType: 'task'
+ })
+
this.isFirstChild(parent) ? this.insertBefore(taskListWrapper, grandpa) : this.insertAfter(taskListWrapper, grandpa)
this.removeBlock(parent)
this.appendChild(taskListWrapper, parent)
} else {
- taskListWrapper = this.createBlock('ul')
- taskListWrapper.listType = 'task'
- const bulletListWrapper = this.createBlock('ul')
- bulletListWrapper.listType = 'bullet'
+ taskListWrapper = this.createBlock('ul', {
+ listType: 'task'
+ })
+
+ const bulletListWrapper = this.createBlock('ul', {
+ listType: 'bullet'
+ })
let preSibling = this.getPreSibling(parent)
while (preSibling) {
@@ -319,145 +351,246 @@ const updateCtrl = ContentState => {
return taskListWrapper || grandpa
}
+ // ATX heading doesn't support soft line break and hard line break.
ContentState.prototype.updateAtxHeader = function (block, header, line) {
const newType = `h${header.length}`
- const text = line ? line.text : block.text
- if (line) {
- const index = block.children.indexOf(line)
- const header = this.createBlock(newType, text)
- header.headingStyle = 'atx'
- this.insertBefore(header, block)
- const paragraphBefore = this.createBlock('p')
- const paragraphAfter = this.createBlock('p')
- let i = 0
- const len = block.children.length
- for (i; i < len; i++) {
- const child = block.children[i]
- if (i < index) {
- this.appendChild(paragraphBefore, child)
- } else if (i > index) {
- this.appendChild(paragraphAfter, child)
- }
- }
- if (paragraphBefore.children.length) {
- this.insertBefore(paragraphBefore, header)
- }
- if (paragraphAfter.children.length) {
- this.insertAfter(paragraphAfter, header)
- }
- this.removeBlock(block)
- this.cursor.start.key = this.cursor.end.key = header.key
- return header
- } else {
- if (block.type === newType && block.headingStyle === 'atx') {
- return null
- }
- block.headingStyle = 'atx'
- block.type = newType
- block.text = text
- block.children.length = 0
- this.cursor.start.key = this.cursor.end.key = block.key
- return block
+ const headingStyle = 'atx'
+ if (block.type === newType && block.headingStyle === headingStyle) {
+ return null
}
+ const text = line.text
+ const lines = text.split('\n')
+ const preParagraphLines = []
+ let atxLine = ''
+ const postParagraphLines = []
+ let atxLineHasPushed = false
+
+ for (const l of lines) {
+ if (/^ {0,3}#{1,6}(?=\s{1,}|$)/.test(l) && !atxLineHasPushed) {
+ atxLine = l
+ atxLineHasPushed = true
+ } else if (!atxLineHasPushed) {
+ preParagraphLines.push(l)
+ } else {
+ postParagraphLines.push(l)
+ }
+ }
+
+ const atxBlock = this.createBlock(newType, {
+ headingStyle
+ })
+ const atxLineBlock = this.createBlock('span', {
+ text: atxLine,
+ functionType: 'atxLine'
+ })
+ this.appendChild(atxBlock, atxLineBlock)
+ this.insertBefore(atxBlock, block)
+ if (preParagraphLines.length) {
+ const preBlock = this.createBlockP(preParagraphLines.join('\n'))
+ this.insertBefore(preBlock, atxBlock)
+ }
+ if (postParagraphLines.length) {
+ const postBlock = this.createBlockP(postParagraphLines.join('\n'))
+ this.insertAfter(postBlock, atxBlock)
+ }
+
+ this.removeBlock(block)
+
+ const { start, end } = this.cursor
+ const key = atxBlock.children[0].key
+ this.cursor = {
+ start: { key, offset: start.offset },
+ end: { key, offset: end.offset }
+ }
+ return atxBlock
}
ContentState.prototype.updateSetextHeader = function (block, marker, line) {
const newType = /=/.test(marker) ? 'h1' : 'h2'
- const header = this.createBlock(newType)
- header.headingStyle = 'setext'
- header.marker = marker
- const index = block.children.indexOf(line)
- let i = 0
- let text = ''
- for (i; i < index; i++) {
- text += `${block.children[i].text}\n`
+ const headingStyle = 'setext'
+ if (block.type === newType && block.headingStyle === headingStyle) {
+ return null
}
- header.text = text.trimRight()
- this.insertBefore(header, block)
- if (line.nextSibling) {
- const removedCache = []
- for (const child of block.children) {
- removedCache.push(child)
- if (child === line) {
- break
- }
+
+ const text = line.text
+ const lines = text.split('\n')
+ let setextLines = []
+ const postParagraphLines = []
+ let setextLineHasPushed = false
+
+ for (const l of lines) {
+ if (/^ {0,3}(?:={3,}|-{3,})(?= {1,}|$)/.test(l) && !setextLineHasPushed) {
+ setextLineHasPushed = true
+ } else if (!setextLineHasPushed) {
+ setextLines.push(l)
+ } else {
+ postParagraphLines.push(l)
}
- removedCache.forEach(child => this.removeBlock(child))
- } else {
- this.removeBlock(block)
}
- this.cursor.start.key = this.cursor.end.key = header.key
- this.cursor.start.offset = this.cursor.end.offset = header.text.length
- return header
+
+ const setextBlock = this.createBlock(newType, {
+ headingStyle,
+ marker
+ })
+ const setextLineBlock = this.createBlock('span', {
+ text: setextLines.join('\n'),
+ functionType: 'paragraphContent'
+ })
+ this.appendChild(setextBlock, setextLineBlock)
+ this.insertBefore(setextBlock, block)
+
+ if (postParagraphLines.length) {
+ const postBlock = this.createBlockP(postParagraphLines.join('\n'))
+ this.insertAfter(postBlock, setextBlock)
+ }
+
+ this.removeBlock(block)
+
+ const key = setextBlock.children[0].key
+ const offset = setextBlock.children[0].text.length
+
+ this.cursor = {
+ start: { key, offset },
+ end: { key, offset }
+ }
+
+ return setextBlock
}
ContentState.prototype.updateBlockQuote = function (block, line) {
- if (line && !this.isFirstChild(line)) {
- const paragraphBefore = this.createBlock('p')
- const removeCache = []
- for (const child of block.children) {
- if (child === line) break
- removeCache.push(child)
- }
- removeCache.forEach(c => {
- this.removeBlock(c)
- this.appendChild(paragraphBefore, c)
- })
- this.insertBefore(paragraphBefore, block)
- }
- if (!line && /^h\d/.test(block.type)) {
- block.text = block.text.substring(1).trim()
- delete block.headingStyle
- delete block.marker
- block.type = 'p'
- block.children = []
- const line = this.createBlock('span', block.text.substring(1))
- block.text = ''
- this.appendChild(block, line)
- } else {
- line.text = line.text.substring(1).trim()
- }
- const quoteBlock = this.createBlock('blockquote')
- this.insertBefore(quoteBlock, block)
- this.removeBlock(block)
- this.appendChild(quoteBlock, block)
+ const text = line.text
+ const lines = text.split('\n')
+ const preParagraphLines = []
+ let quoteLines = []
+ let quoteLinesHasPushed = false
+ for (const l of lines) {
+ if (/^ {0,3}>/.test(l) && !quoteLinesHasPushed) {
+ quoteLinesHasPushed = true
+ quoteLines.push(l.trimStart().substring(1).trimStart())
+ } else if (!quoteLinesHasPushed) {
+ preParagraphLines.push(l)
+ } else {
+ quoteLines.push(l)
+ }
+ }
+ let quoteParagraphBlock
+ if (/^h\d/.test(block.type)) {
+ quoteParagraphBlock = this.createBlock(block.type, {
+ headingStyle: block.headingStyle
+ })
+ if (block.headingStyle === 'setext') {
+ quoteParagraphBlock.marker = block.marker
+ }
+ const headerContent = this.createBlock('span', {
+ text: quoteLines.join('\n'),
+ functionType: block.headingStyle === 'setext'? 'paragraphContent' : 'atxLine'
+ })
+ this.appendChild(quoteParagraphBlock, headerContent)
+ } else {
+ quoteParagraphBlock = this.createBlockP(quoteLines.join('\n'))
+ }
+
+ const quoteBlock = this.createBlock('blockquote')
+ this.appendChild(quoteBlock, quoteParagraphBlock)
+ this.insertBefore(quoteBlock, block)
+
+ if (preParagraphLines.length) {
+ const preParagraphBlock = this.createBlockP(preParagraphLines.join('\n'))
+ this.insertBefore(preParagraphBlock, quoteBlock)
+ }
+
+ this.removeBlock(block)
+
+ const key = quoteParagraphBlock.children[0].key
const { start, end } = this.cursor
this.cursor = {
- start: {
- key: start.key,
- offset: start.offset - 1
- },
- end: {
- key: end.key,
- offset: end.offset - 1
- }
+ start: { key, offset: start.offset - 1 },
+ end: { key, offset: end.offset - 1 }
}
+
return quoteBlock
}
- ContentState.prototype.updateToParagraph = function (block) {
+ ContentState.prototype.updateIndentCode = function (block, line) {
+ const codeBlock = this.createBlock('code', {
+ lang: ''
+ })
+ const inputBlock = this.createBlock('span', {
+ functionType: 'languageInput'
+ })
+ const preBlock = this.createBlock('pre', {
+ functionType: 'indentcode',
+ lang: ''
+ })
+
+ const text = line ? line.text : block.text
+
+ const lines = text.split('\n')
+ const codeLines = []
+ const paragraphLines = []
+ let canBeCodeLine = true
+
+ for (const l of lines) {
+ if (/^ {4,}/.test(l) && canBeCodeLine) {
+ codeLines.push(l.replace(/^ {4}/, ''))
+ } else {
+ canBeCodeLine = false
+ paragraphLines.push(l)
+ }
+ }
+ codeLines.forEach(text => {
+ const codeLine = this.createBlock('span', {
+ text,
+ functionType: 'codeLine',
+ lang: ''
+ })
+ this.appendChild(codeBlock, codeLine)
+ })
+
+ this.appendChild(preBlock, inputBlock)
+ this.appendChild(preBlock, codeBlock)
+ this.insertBefore(preBlock, block)
+
+ if (paragraphLines.length > 0 && line) {
+ const newLine = this.createBlock('span', {
+ text: paragraphLines.join('\n')
+ })
+ this.insertBefore(newLine, line)
+ this.removeBlock(line)
+ } else {
+ this.removeBlock(block)
+ }
+
+ const key = codeBlock.children[0].key
+ const { start, end } = this.cursor
+ this.cursor = {
+ start: { key, offset: start.offset - 4 },
+ end: { key, offset: end.offset - 4 }
+ }
+ return preBlock
+ }
+
+ ContentState.prototype.updateToParagraph = function (block, line) {
if (/^h\d$/.test(block.type) && block.headingStyle === 'setext') {
return null
}
+
const newType = 'p'
if (block.type !== newType) {
- block.type = newType // updateP
- const newLine = this.createBlock('span', block.text)
- this.appendChild(block, newLine)
- block.text = ''
- this.cursor.start.key = this.cursor.end.key = newLine.key
+ const newBlock = this.createBlockP(line.text)
+ this.insertBefore(newBlock, block)
+ this.removeBlock(block)
+ const { start, end } = this.cursor
+ const key = newBlock.children[0].key
+ this.cursor = {
+ start: { key, offset: start.offset },
+ end: { key, offset: end.offset }
+ }
return block
}
return null
}
-
- ContentState.prototype.updateCodeBlocks = function (block) {
- const codeBlock = this.getParent(block)
- const preBlock = this.getParent(codeBlock)
- const code = codeBlock.children.map(line => line.text).join('\n')
- this.codeBlocks.set(preBlock.key, code)
- }
}
export default updateCtrl
diff --git a/src/muya/lib/eventHandler/clickEvent.js b/src/muya/lib/eventHandler/clickEvent.js
index 846d7a6b..7b5e0f2b 100644
--- a/src/muya/lib/eventHandler/clickEvent.js
+++ b/src/muya/lib/eventHandler/clickEvent.js
@@ -73,15 +73,12 @@ class ClickEvent {
if (target.closest('div.ag-container-preview') || target.closest('div.ag-html-preview')) {
return event.stopPropagation()
}
- // handler html preview click
+ // handler container preview click
const editIcon = target.closest(`.ag-container-icon`)
if (editIcon) {
event.preventDefault()
event.stopPropagation()
- const nextElement = editIcon.nextElementSibling
- if (nextElement && nextElement.classList.contains('ag-function-html')) {
- contentState.handleHtmlBlockClick(nextElement)
- } else if (editIcon.parentNode.classList.contains('ag-container-block')) {
+ if (editIcon.parentNode.classList.contains('ag-container-block')) {
contentState.handleContainerBlockClick(editIcon.parentNode)
}
}
diff --git a/src/muya/lib/eventHandler/keyboard.js b/src/muya/lib/eventHandler/keyboard.js
index a1ab9b6c..a2d50cb1 100644
--- a/src/muya/lib/eventHandler/keyboard.js
+++ b/src/muya/lib/eventHandler/keyboard.js
@@ -66,6 +66,9 @@ class Keyboard {
if (event.target.closest('[contenteditable=false]')) {
return
}
+
+ // We need check cursor is null, because we may copy the html preview content,
+ // and no need to dispatch change.
const { start, end } = selection.getCursorRange()
if (!start || !end) {
return
diff --git a/src/muya/lib/parser/index.js b/src/muya/lib/parser/index.js
index 3792ab59..66e18ba0 100644
--- a/src/muya/lib/parser/index.js
+++ b/src/muya/lib/parser/index.js
@@ -378,15 +378,37 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
pos = pos + autoLTo[0].length
continue
}
+ // soft line break
+ const softTo = inlineRules['soft_line_break'].exec(src)
+ if (softTo) {
+ const len = softTo[0].length
+ pushPending()
+ tokens.push({
+ type: 'soft_line_break',
+ raw: softTo[0],
+ lineBreak: softTo[1],
+ isAtEnd: softTo.input.length === softTo[0].length,
+ parent: tokens,
+ range: {
+ start: pos,
+ end: pos + len
+ }
+ })
+ src = src.substring(len)
+ pos += len
+ continue
+ }
// hard line break
const hardTo = inlineRules['hard_line_break'].exec(src)
- if (hardTo && top) {
+ if (hardTo) {
const len = hardTo[0].length
pushPending()
tokens.push({
type: 'hard_line_break',
raw: hardTo[0],
spaces: hardTo[1],
+ lineBreak: hardTo[2],
+ isAtEnd: hardTo.input.length === hardTo[0].length,
parent: tokens,
range: {
start: pos,
diff --git a/src/muya/lib/parser/marked/lexer.js b/src/muya/lib/parser/marked/lexer.js
index e635cf85..9580c395 100644
--- a/src/muya/lib/parser/marked/lexer.js
+++ b/src/muya/lib/parser/marked/lexer.js
@@ -185,9 +185,11 @@ Lexer.prototype.token = function (src, top) {
// hr
cap = this.rules.hr.exec(src)
if (cap) {
+ const marker = cap[0].replace(/\n*$/, '')
src = src.substring(cap[0].length)
this.tokens.push({
- type: 'hr'
+ type: 'hr',
+ marker
})
continue
}
diff --git a/src/muya/lib/parser/render/index.js b/src/muya/lib/parser/render/index.js
index a09d726a..67dd9e43 100644
--- a/src/muya/lib/parser/render/index.js
+++ b/src/muya/lib/parser/render/index.js
@@ -3,7 +3,7 @@ import flowchart from 'flowchart.js'
import Diagram from './sequence'
import vegaEmbed from 'vega-embed'
import { CLASS_OR_ID } from '../../config'
-import { conflict, mixins } from '../../utils'
+import { conflict, mixins, camelToSnake } from '../../utils'
import { patch, toVNode, toHTML, h } from './snabbdom'
import { beginRules } from '../rules'
import renderInlines from './renderInlines'
@@ -13,6 +13,7 @@ class StateRender {
constructor (muya) {
this.muya = muya
this.eventCenter = muya.eventCenter
+ this.codeCache = new Map()
this.loadImageMap = new Map()
this.loadMathMap = new Map()
this.mermaidCache = new Set()
@@ -85,7 +86,7 @@ class StateRender {
selector += `.${CLASS_OR_ID['AG_ACTIVE']}`
}
if (type === 'span') {
- selector += `.${CLASS_OR_ID['AG_LINE']}`
+ selector += `.ag-${camelToSnake(block.functionType)}`
}
if (!block.parent && selectedBlock && block.key === selectedBlock.key) {
selector += `.${CLASS_OR_ID['AG_SELECTED']}`
@@ -155,6 +156,7 @@ class StateRender {
patch(oldVdom, newVdom)
this.renderMermaid()
this.renderDiagram()
+ this.codeCache.clear()
}
// Only render the blocks which you updated
@@ -198,6 +200,7 @@ class StateRender {
this.renderMermaid()
this.renderDiagram()
+ this.codeCache.clear()
}
}
diff --git a/src/muya/lib/parser/render/renderBlock/renderBlock.js b/src/muya/lib/parser/render/renderBlock/renderBlock.js
index 66dda8e8..3ecd4c4c 100644
--- a/src/muya/lib/parser/render/renderBlock/renderBlock.js
+++ b/src/muya/lib/parser/render/renderBlock/renderBlock.js
@@ -2,7 +2,7 @@
* [renderBlock render one block, no matter it is a container block or text block]
*/
export default function renderBlock (block, cursor, activeBlocks, selectedBlock, matches, useCache = false) {
- const method = block.children.length > 0
+ const method = Array.isArray(block.children) && block.children.length > 0
? 'renderContainerBlock'
: 'renderLeafBlock'
diff --git a/src/muya/lib/parser/render/renderBlock/renderContainerBlock.js b/src/muya/lib/parser/render/renderBlock/renderContainerBlock.js
index f7335725..916895a2 100644
--- a/src/muya/lib/parser/render/renderBlock/renderContainerBlock.js
+++ b/src/muya/lib/parser/render/renderBlock/renderContainerBlock.js
@@ -17,28 +17,47 @@ const PRE_BLOCK_HASH = {
export default function renderContainerBlock (block, cursor, activeBlocks, selectedBlock, matches, useCache = false) {
let selector = this.getSelector(block, cursor, activeBlocks, selectedBlock)
+ const {
+ type,
+ headingStyle,
+ editable,
+ functionType,
+ listType,
+ listItemType,
+ bulletMarkerOrDelimiter,
+ isLooseListItem,
+ lang
+ } = block
const children = block.children.map(child => this.renderBlock(child, cursor, activeBlocks, selectedBlock, matches, useCache))
const data = {
attrs: {},
dataset: {}
}
- // handle `div` block
- if (/div/.test(block.type)) {
- if (block.toolBarType) {
- selector += `.${'ag-tool-' + block.toolBarType}.${CLASS_OR_ID['AG_TOOL_BAR']}`
- }
- if (block.functionType) {
- selector += `.${'ag-function-' + block.functionType}`
- }
- if (block.editable !== undefined && !block.editable) {
- Object.assign(data.attrs, { contenteditable: 'false' })
- }
+
+ if (editable === false) {
+ Object.assign(data.attrs, { contenteditable: 'false' })
}
- // handle `figure` block
- if (block.type === 'figure') {
- if (block.functionType) {
- Object.assign(data.dataset, { role: block.functionType.toUpperCase() })
- if (block.functionType === 'table') {
+
+ if (/code|pre/.test(type) && typeof lang === 'string' && !!lang) {
+ selector += `.language-${lang}`
+ }
+
+ if (/^h/.test(type)) {
+ if (/^h\d$/.test(type)) {
+ // TODO: This should be the best place to create and update the TOC.
+ // Cache `block.key` and title and update only if necessary.
+ Object.assign(data.dataset, {
+ head: type
+ })
+ selector += `.${headingStyle}`
+ }
+ Object.assign(data.dataset, {
+ role: type
+ })
+ } else if (type === 'figure') {
+ if (functionType) {
+ Object.assign(data.dataset, { role: functionType.toUpperCase() })
+ if (functionType === 'table') {
children.unshift(renderTableTools(activeBlocks))
} else {
children.unshift(renderEditIcon())
@@ -46,61 +65,29 @@ export default function renderContainerBlock (block, cursor, activeBlocks, selec
}
if (
- /multiplemath|flowchart|mermaid|sequence|vega-lite/.test(block.functionType)
+ /html|multiplemath|flowchart|mermaid|sequence|vega-lite/.test(functionType)
) {
selector += `.${CLASS_OR_ID['AG_CONTAINER_BLOCK']}`
}
- }
- // hanle list block
- if (/ul|ol/.test(block.type) && block.listType) {
- switch (block.listType) {
- case 'order':
- selector += `.${CLASS_OR_ID['AG_ORDER_LIST']}`
- break
- case 'bullet':
- selector += `.${CLASS_OR_ID['AG_BULLET_LIST']}`
- break
- case 'task':
- selector += `.${CLASS_OR_ID['AG_TASK_LIST']}`
- break
- default:
- break
+ } else if (/ul|ol/.test(type) && listType) {
+ selector += `.ag-${listType}-list`
+ if (type === 'ol') {
+ Object.assign(data.attrs, { start: block.start })
}
- }
- if (block.type === 'li' && block.listItemType) {
+ } else if (type === 'li' && listItemType) {
+ Object.assign(data.dataset, { marker: bulletMarkerOrDelimiter })
selector += `.${CLASS_OR_ID['AG_LIST_ITEM']}`
- switch (block.listItemType) {
- case 'order':
- selector += `.${CLASS_OR_ID['AG_ORDER_LIST_ITEM']}`
- break
- case 'bullet':
- selector += `.${CLASS_OR_ID['AG_BULLET_LIST_ITEM']}`
- break
- case 'task':
- selector += `.${CLASS_OR_ID['AG_TASK_LIST_ITEM']}`
- break
- default:
- break
- }
- Object.assign(data.dataset, { marker: block.bulletMarkerOrDelimiter })
- selector += 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 })
- }
- if (block.type === 'code') {
- const { lang } = block
- if (lang) {
- selector += `.language-${lang}`
- }
- }
- if (block.type === 'pre') {
- const { lang, functionType } = block
- if (lang) {
- selector += `.language-${lang}`
- }
+ selector += `.ag-${listItemType}-list-item`
+ selector += isLooseListItem ? `.${CLASS_OR_ID['AG_LOOSE_LIST_ITEM']}` : `.${CLASS_OR_ID['AG_TIGHT_LIST_ITEM']}`
+ } else if (type === 'pre') {
Object.assign(data.dataset, { role: functionType })
selector += PRE_BLOCK_HASH[block.functionType]
+
+ if (/html|multiplemath|mermaid|flowchart|wega-lite|sequence/.test(functionType)) {
+ const codeBlock = block.children[0]
+ const code = codeBlock.children.map(line => line.text).join('\n')
+ this.codeCache.set(block.key, code)
+ }
}
if (!block.parent) {
diff --git a/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js b/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js
index 9bceb0e3..b63851a2 100644
--- a/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js
+++ b/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js
@@ -57,7 +57,6 @@ export default function renderLeafBlock (block, cursor, activeBlocks, selectedBl
const {
text,
type,
- headingStyle,
align,
checked,
key,
@@ -65,13 +64,16 @@ export default function renderLeafBlock (block, cursor, activeBlocks, selectedBl
functionType,
editable
} = block
+
const data = {
props: {},
attrs: {},
dataset: {},
style: {}
}
+
let children = ''
+
if (text) {
let tokens = []
if (highlights.length === 0 && this.tokenCache.has(text)) {
@@ -81,7 +83,7 @@ export default function renderLeafBlock (block, cursor, activeBlocks, selectedBl
functionType !== 'codeLine' &&
functionType !== 'languageInput'
) {
- const hasBeginRules = /^(h\d|span|hr)/.test(type)
+ const hasBeginRules = type === 'span'
tokens = tokenizer(text, highlights, hasBeginRules, this.labels)
const hasReferenceTokens = hasReferenceToken(tokens)
if (highlights.length === 0 && useCache && DEVICE_MEMORY >= 4 && !hasReferenceTokens) {
@@ -103,9 +105,9 @@ export default function renderLeafBlock (block, cursor, activeBlocks, selectedBl
style: `text-align:${align}`
})
} else if (type === 'div') {
- const code = this.muya.contentState.codeBlocks.get(block.preSibling)
+ const code = this.codeCache.get(block.preSibling)
switch (functionType) {
- case 'preview': {
+ case 'html': {
selector += `.${CLASS_OR_ID['AG_HTML_PREVIEW']}`
const htmlContent = sanitize(code, PREVIEW_DOMPURIFY_CONFIG)
// handle empty html bock
@@ -168,7 +170,6 @@ export default function renderLeafBlock (block, cursor, activeBlocks, selectedBl
case 'flowchart':
case 'sequence':
case 'vega-lite': {
- const code = this.muya.contentState.codeBlocks.get(block.preSibling)
selector += `.${CLASS_OR_ID['AG_CONTAINER_PREVIEW']}`
if (code === '') {
children = '< Empty Diagram Block >'
@@ -183,18 +184,6 @@ export default function renderLeafBlock (block, cursor, activeBlocks, selectedBl
break
}
}
- } else if (/^h/.test(type)) {
- if (/^h\d$/.test(type)) {
- // TODO: This should be the best place to create and update the TOC.
- // Cache `block.key` and title and update only if necessary.
- Object.assign(data.dataset, {
- head: type
- })
- selector += `.${headingStyle}`
- }
- Object.assign(data.dataset, {
- role: type
- })
} else if (type === 'input') {
Object.assign(data.attrs, {
type: 'checkbox'
@@ -214,8 +203,6 @@ export default function renderLeafBlock (block, cursor, activeBlocks, selectedBl
.replace(new RegExp(MARKER_HASK['"'], 'g'), '"')
.replace(new RegExp(MARKER_HASK["'"], 'g'), "'")
- selector += `.${CLASS_OR_ID['AG_CODE_LINE']}`
-
if (lang && /\S/.test(code) && loadedCache.has(lang)) {
const wrapper = document.createElement('div')
wrapper.classList.add(`language-${lang}`)
@@ -230,7 +217,6 @@ export default function renderLeafBlock (block, cursor, activeBlocks, selectedBl
}
} else if (type === 'span' && functionType === 'languageInput') {
const html = getHighlightHtml(text, highlights)
- selector += `.${CLASS_OR_ID['AG_LANGUAGE_INPUT']}`
children = htmlToVNode(html)
}
if (!block.parent) {
diff --git a/src/muya/lib/parser/render/renderInlines/hardLineBreak.js b/src/muya/lib/parser/render/renderInlines/hardLineBreak.js
index cbeffa7f..606a3cf5 100644
--- a/src/muya/lib/parser/render/renderInlines/hardLineBreak.js
+++ b/src/muya/lib/parser/render/renderInlines/hardLineBreak.js
@@ -1,13 +1,17 @@
import { CLASS_OR_ID } from '../../../config'
-export default function hardLineBreak (h, cursor, block, token, outerClass) {
+export default function softLineBreak (h, cursor, block, token, outerClass) {
+ const { spaces, lineBreak, isAtEnd } = token
const className = CLASS_OR_ID['AG_HARD_LINE_BREAK']
- const content = [token.spaces]
- if (block.type === 'span' && block.nextSibling) {
+ const spaceClass = CLASS_OR_ID['AG_HARD_LINE_BREAK_SPACE']
+ if (isAtEnd) {
return [
- h(`span.${className}`, content)
+ h(`span.${className}`, h(`span.${spaceClass}`, spaces)),
+ h(`span.${CLASS_OR_ID['AG_LINE_END']}`, lineBreak)
]
} else {
- return content
+ return [
+ h(`span.${className}`, [ h(`span.${spaceClass}`, spaces), lineBreak ])
+ ]
}
}
diff --git a/src/muya/lib/parser/render/renderInlines/index.js b/src/muya/lib/parser/render/renderInlines/index.js
index 4c5053f8..3ddb0764 100644
--- a/src/muya/lib/parser/render/renderInlines/index.js
+++ b/src/muya/lib/parser/render/renderInlines/index.js
@@ -7,6 +7,7 @@ import htmlTag from './htmlTag'
import hr from './hr'
import tailHeader from './tailHeader'
import hardLineBreak from './hardLineBreak'
+import softLineBreak from './softLineBreak'
import codeFense from './codeFense'
import inlineMath from './inlineMath'
import autoLink from './autoLink'
@@ -37,6 +38,7 @@ export default {
hr,
tailHeader,
hardLineBreak,
+ softLineBreak,
codeFense,
inlineMath,
autoLink,
diff --git a/src/muya/lib/parser/render/renderInlines/softLineBreak.js b/src/muya/lib/parser/render/renderInlines/softLineBreak.js
new file mode 100644
index 00000000..15bdc0a5
--- /dev/null
+++ b/src/muya/lib/parser/render/renderInlines/softLineBreak.js
@@ -0,0 +1,13 @@
+import { CLASS_OR_ID } from '../../../config'
+
+export default function hardLineBreak (h, cursor, block, token, outerClass) {
+ const { lineBreak, isAtEnd } = token
+ let selector = `span.${CLASS_OR_ID['AG_SOFT_LINE_BREAK']}`
+ if (isAtEnd) {
+ selector += `.${CLASS_OR_ID['AG_LINE_END']}`
+ }
+
+ return [
+ h(selector, lineBreak)
+ ]
+}
diff --git a/src/muya/lib/parser/rules.js b/src/muya/lib/parser/rules.js
index eb090b7b..9129fa00 100644
--- a/src/muya/lib/parser/rules.js
+++ b/src/muya/lib/parser/rules.js
@@ -25,7 +25,8 @@ export const inlineRules = {
'tail_header': /^(\s{1,}#{1,})(\s*)$/,
'html_tag': /^(|(<([a-zA-Z]{1}[a-zA-Z\d-]*) *[_\.\-/:a-zA-Z\d='";\? *]* *(?:\/)?>)(?:([\s\S]*?)(<\/\3 *>))?)/, // row html
'html_escape': new RegExp(`^(${escapeCharacters.join('|')})`, 'i'),
- 'hard_line_break': /^(\s{2,})$/,
+ 'soft_line_break': /^(\n)(?!\n)/,
+ 'hard_line_break': /^( {2,})(\n)(?!\n)/,
// patched math marker `$`
'backlash': /^(\\)([\\`*{}\[\]()#+\-.!_>~:\|\<\>$]{1})/,
diff --git a/src/muya/lib/prism/index.js b/src/muya/lib/prism/index.js
index 6f341610..858c6d54 100644
--- a/src/muya/lib/prism/index.js
+++ b/src/muya/lib/prism/index.js
@@ -1,11 +1,11 @@
-import Prism from 'prismjs'
+import Prism from 'prismjs2'
import { filter } from 'fuzzaldrin'
import initLoadLanguage, { loadedCache } from './loadLanguage'
import languages from './languages'
const prism = Prism
window.Prism = Prism
-import('prismjs/plugins/keep-markup/prism-keep-markup')
+import('prismjs2/plugins/keep-markup/prism-keep-markup')
const langs = Object.keys(languages).map(name => (languages[name]))
const loadLanguage = initLoadLanguage(Prism)
diff --git a/src/muya/lib/prism/loadLanguage.js b/src/muya/lib/prism/loadLanguage.js
index 7b77095d..50390f49 100644
--- a/src/muya/lib/prism/loadLanguage.js
+++ b/src/muya/lib/prism/loadLanguage.js
@@ -74,7 +74,7 @@ function initLoadLanguage (Prism) {
}
delete Prism.languages[language]
- await import('prismjs/components/prism-' + language)
+ await import('prismjs2/components/prism-' + language)
loadedCache.add(language)
promises.push(Promise.resolve({
status: 'loaded',
diff --git a/src/muya/lib/selection/dom.js b/src/muya/lib/selection/dom.js
index 13c7d6b5..9e702899 100644
--- a/src/muya/lib/selection/dom.js
+++ b/src/muya/lib/selection/dom.js
@@ -4,7 +4,12 @@ import {
const CHOP_TEXT_REG = /(\*{1,3})([^*]+)(\1)/g
export const getTextContent = (node, blackList) => {
- if (!blackList) return node.textContent
+ if (node.nodeType === 3) {
+ return node.textContent
+ } else if (!blackList) {
+ return node.textContent
+ }
+
let text = ''
if (blackList.some(className => node.classList && node.classList.contains(className))) {
return text
diff --git a/src/muya/lib/selection/index.js b/src/muya/lib/selection/index.js
index 70aa3dfe..258aa8bc 100644
--- a/src/muya/lib/selection/index.js
+++ b/src/muya/lib/selection/index.js
@@ -436,6 +436,7 @@ class Selection {
let { node: anchorNode, offset: anchorOffset } = getNodeAndOffset(anchorParagraph, anchor.offset)
let { node: focusNode, offset: focusOffset } = getNodeAndOffset(focusParagraph, focus.offset)
+
anchorOffset = Math.min(anchorOffset, anchorNode.textContent.length)
focusOffset = Math.min(focusOffset, focusNode.textContent.length)
// First set the anchor node and anchor offeet, make it collapsed
@@ -449,15 +450,14 @@ class Selection {
if (node.nodeType === 3) {
node = node.parentNode
}
- return node.closest('p[data-role=hr]') ||
- node.closest('span.ag-paragraph.ag-line') ||
+ return node.closest('span.ag-paragraph') ||
node.closest('th.ag-paragraph') ||
- node.closest('td.ag-paragraph') ||
- node.closest('.ag-paragraph[data-head]')
+ node.closest('td.ag-paragraph')
}
getCursorRange () {
let { anchorNode, anchorOffset, focusNode, focusOffset } = this.doc.getSelection()
+
const isAnchorValid = this.isValidCursorNode(anchorNode)
const isFocusValid = this.isValidCursorNode(focusNode)
let needFix = false
@@ -480,6 +480,16 @@ class Selection {
})
}
+ // fix bug click empty line, the cursor will jump to the end of pre line.
+ if (
+ anchorNode === focusNode &&
+ anchorOffset === focusOffset &&
+ anchorNode.textContent === '\n' &&
+ focusOffset === 0
+ ) {
+ focusOffset = anchorOffset = 1
+ }
+
const anchorParagraph = findNearestParagraph(anchorNode)
const focusParagraph = findNearestParagraph(focusNode)
@@ -502,6 +512,7 @@ class Selection {
const aOffset = getOffsetOfParagraph(anchorNode, anchorParagraph) + anchorOffset
const fOffset = getOffsetOfParagraph(focusNode, focusParagraph) + focusOffset
+
const anchor = { key: anchorParagraph.id, offset: aOffset }
const focus = { key: focusParagraph.id, offset: fOffset }
const result = new Cursor({ anchor, focus })
diff --git a/src/muya/lib/utils/exportHtml.js b/src/muya/lib/utils/exportHtml.js
index 05490373..5b1ba8cf 100644
--- a/src/muya/lib/utils/exportHtml.js
+++ b/src/muya/lib/utils/exportHtml.js
@@ -1,12 +1,12 @@
import marked from '../parser/marked'
-import Prism from 'prismjs'
+import Prism from 'prismjs2'
import katex from 'katex'
import mermaid from 'mermaid'
import flowchart from 'flowchart.js'
import Diagram from '../parser/render/sequence'
import vegaEmbed from 'vega-embed'
import githubMarkdownCss from 'github-markdown-css/github-markdown.css'
-import highlightCss from 'prismjs/themes/prism.css'
+import highlightCss from 'prismjs2/themes/prism.css'
import katexCss from 'katex/dist/katex.css'
import { EXPORT_DOMPURIFY_CONFIG } from '../config'
import { sanitize, unescapeHtml } from '../utils'
diff --git a/src/muya/lib/utils/exportMarkdown.js b/src/muya/lib/utils/exportMarkdown.js
index a145df52..6ad3c244 100644
--- a/src/muya/lib/utils/exportMarkdown.js
+++ b/src/muya/lib/utils/exportMarkdown.js
@@ -42,7 +42,8 @@ class ExportMarkdown {
}
switch (block.type) {
- case 'p': {
+ case 'p':
+ case 'hr': {
this.insertLineBreak(result, indent)
result.push(this.translateBlocks2Markdown(block.children, indent))
break
@@ -51,11 +52,6 @@ class ExportMarkdown {
result.push(this.normalizeParagraphText(block, indent))
break
}
- case 'hr': {
- this.insertLineBreak(result, indent)
- result.push(this.normalizeParagraphText(block, indent))
- break
- }
case 'h1':
case 'h2':
case 'h3':
@@ -171,17 +167,21 @@ class ExportMarkdown {
}
normalizeParagraphText (block, indent) {
- return `${indent}${block.text}\n`
+ const { text } = block
+ const lines = text.split('\n')
+ return lines.map(line => `${indent}${line}`).join('\n') + '\n'
}
normalizeHeaderText (block, indent) {
const { headingStyle, marker } = block
+ const { text } = block.children[0]
if (headingStyle === 'atx') {
- const match = block.text.match(/(#{1,6})(.*)/)
- const text = `${match[1]} ${match[2].trim()}`
- return `${indent}${text}\n`
+ const match = text.match(/(#{1,6})(.*)/)
+ const atxHeadingText = `${match[1]} ${match[2].trim()}`
+ return `${indent}${atxHeadingText}\n`
} else if (headingStyle === 'setext') {
- return `${indent}${block.text}\n${indent}${marker.trim()}\n`
+ const lines = text.trim().split('\n')
+ return lines.map(line => `${indent}${line}`).join('\n') + `\n${indent}${marker.trim()}\n`
}
}
@@ -244,7 +244,7 @@ class ExportMarkdown {
normalizeHTML (block, indent) { // figure
const result = []
- const codeLines = block.children[0].children[0].children[0].children
+ const codeLines = block.children[0].children[0].children
for (const line of codeLines) {
result.push(`${indent}${line.text}\n`)
}
diff --git a/src/muya/lib/utils/importMarkdown.js b/src/muya/lib/utils/importMarkdown.js
index e608abda..776f7a67 100644
--- a/src/muya/lib/utils/importMarkdown.js
+++ b/src/muya/lib/utils/importMarkdown.js
@@ -13,6 +13,56 @@ import { CURSOR_DNA } from '../config'
const LINE_BREAKS_REG = /\n/
+// Just because turndown change `\n`(soft line break) to space, So we add `span.ag-soft-line-break` to workaround.
+const turnSoftBreakToSpan = html => {
+ const parser = new DOMParser()
+ const doc = parser.parseFromString(
+ `${html}`,
+ 'text/html'
+ )
+ const root = doc.querySelector(`#turn-root`)
+ const travel = childNodes => {
+ for (const node of childNodes) {
+ if (node.nodeType === 3) {
+ let startLen = 0
+ let endLen = 0
+ const text = node.nodeValue.replace(/^(\n+)/, (_, p) => {
+ startLen = p.length
+ return ''
+ }).replace(/(\n+)$/, (_, p) => {
+ endLen = p.length
+ return ''
+ })
+ if (/\n/.test(text)) {
+ const tokens = text.split('\n')
+ const params = []
+ let i = 0
+ const len = tokens.length
+ for (; i< len; i++) {
+ let text = tokens[i]
+ if (i === 0 && startLen !== 0) {
+ text = '\n'.repeat(startLen) + text
+ } else if (i === len - 1 && endLen !== 0) {
+ text = text + '\n'.repeat(endLen)
+ }
+ params.push(document.createTextNode(text))
+ if (i !== len - 1) {
+ const softBreak = document.createElement('span')
+ softBreak.classList.add('ag-soft-line-break')
+ params.push(softBreak)
+ }
+ }
+ node.replaceWith(...params)
+ }
+ } else if (node.nodeType === 1) {
+ travel(node.childNodes)
+ }
+ }
+ }
+ travel(root.childNodes)
+ return root.innerHTML.trim()
+}
+
const importRegister = ContentState => {
// turn markdown to blocks
ContentState.prototype.markdownToState = function (markdown) {
@@ -37,37 +87,57 @@ const importRegister = ContentState => {
while ((token = tokens.shift())) {
switch (token.type) {
case 'frontmatter': {
+ const lang = 'yaml'
value = token.text
- block = this.createBlock('pre')
- const codeBlock = this.createBlock('code')
+ block = this.createBlock('pre', {
+ functionType: token.type,
+ lang
+ })
+ const codeBlock = this.createBlock('code', {
+ lang
+ })
value
.replace(/^\s+/, '')
.replace(/\s$/, '')
.split(LINE_BREAKS_REG).forEach(line => {
- const codeLine = this.createBlock('span', line)
- codeLine.functionType = 'codeLine'
- codeLine.lang = 'yaml'
+ const codeLine = this.createBlock('span', {
+ text: line,
+ lang,
+ functionType: 'codeLine'
+ })
+
this.appendChild(codeBlock, codeLine)
})
- block.functionType = token.type
- block.lang = codeBlock.lang = 'yaml'
- this.codeBlocks.set(block.key, value)
this.appendChild(block, codeBlock)
this.appendChild(parentList[0], block)
break
}
case 'hr': {
- value = '---'
- block = this.createBlock('hr', value)
+ value = token.marker
+ block = this.createBlock('hr')
+ const thematicBreakContent = this.createBlock('span', {
+ text: value,
+ functionType: 'thematicBreakLine'
+ })
+ this.appendChild(block, thematicBreakContent)
this.appendChild(parentList[0], block)
break
}
case 'heading': {
const { headingStyle, depth, text, marker } = token
value = headingStyle === 'atx' ? '#'.repeat(+depth) + ` ${text}` : text
- block = this.createBlock(`h${depth}`, value)
- block.headingStyle = headingStyle
+ block = this.createBlock(`h${depth}`, {
+ headingStyle
+ })
+
+ const headingContent = this.createBlock('span', {
+ text: value,
+ functionType: headingStyle === 'atx'? 'atxLine' : 'paragraphContent'
+ })
+
+ this.appendChild(block, headingContent)
+
if (marker) {
block.marker = marker
}
@@ -95,15 +165,25 @@ const importRegister = ContentState => {
block = this.createContainerBlock(lang, value)
this.appendChild(parentList[0], block)
} else {
- block = this.createBlock('pre')
- const codeBlock = this.createBlock('code')
+ block = this.createBlock('pre', {
+ functionType: codeBlockStyle === 'fenced' ? 'fencecode' : 'indentcode',
+ lang
+ })
+ const codeBlock = this.createBlock('code', {
+ lang
+ })
value.split(LINE_BREAKS_REG).forEach(line => {
- const codeLine = this.createBlock('span', line)
+ const codeLine = this.createBlock('span', {
+ text: line
+ })
codeLine.lang = lang
codeLine.functionType = 'codeLine'
this.appendChild(codeBlock, codeLine)
})
- const inputBlock = this.createBlock('span', lang)
+ const inputBlock = this.createBlock('span', {
+ text: lang,
+ functionType: 'languageInput'
+ })
if (lang && !languageLoaded.has(lang)) {
languageLoaded.add(lang)
loadLanguage(lang)
@@ -121,10 +201,7 @@ const importRegister = ContentState => {
console.warn(err)
})
}
- inputBlock.functionType = 'languageInput'
- this.codeBlocks.set(block.key, value)
- block.functionType = codeBlockStyle === 'fenced' ? 'fencecode' : 'indentcode'
- block.lang = codeBlock.lang = lang
+
this.appendChild(block, inputBlock)
this.appendChild(block, codeBlock)
this.appendChild(parentList[0], block)
@@ -144,7 +221,9 @@ const importRegister = ContentState => {
}
for (const headText of header) {
const i = header.indexOf(headText)
- const th = this.createBlock('th', restoreTableEscapeCharacters(headText))
+ const th = this.createBlock('th', {
+ text: restoreTableEscapeCharacters(headText)
+ })
Object.assign(th, { align: align[i] || '', column: i })
this.appendChild(theadRow, th)
}
@@ -152,7 +231,9 @@ const importRegister = ContentState => {
const rowBlock = this.createBlock('tr')
for (const cell of row) {
const i = row.indexOf(cell)
- const td = this.createBlock('td', restoreTableEscapeCharacters(cell))
+ const td = this.createBlock('td', {
+ text: restoreTableEscapeCharacters(cell)
+ })
Object.assign(td, { align: align[i] || '', column: i })
this.appendChild(rowBlock, td)
}
@@ -181,20 +262,20 @@ const importRegister = ContentState => {
value += `\n${token.text}`
}
block = this.createBlock('p')
- const lines = value.split(LINE_BREAKS_REG).map(line => this.createBlock('span', line))
- for (const line of lines) {
- this.appendChild(block, line)
- }
+ const contentBlock = this.createBlock('span', {
+ text: value
+ })
+ this.appendChild(block, contentBlock)
this.appendChild(parentList[0], block)
break
}
case 'paragraph': {
value = token.text
block = this.createBlock('p')
- const lines = value.split(LINE_BREAKS_REG).map(line => this.createBlock('span', line))
- for (const line of lines) {
- this.appendChild(block, line)
- }
+ const contentBlock = this.createBlock('span', {
+ text: value
+ })
+ this.appendChild(block, contentBlock)
this.appendChild(parentList[0], block)
break
}
@@ -226,13 +307,17 @@ const importRegister = ContentState => {
case 'loose_item_start':
case 'list_item_start': {
const { listItemType, bulletMarkerOrDelimiter, checked, type } = token
- block = this.createBlock('li')
- block.listItemType = checked !== undefined ? 'task' : listItemType
- block.bulletMarkerOrDelimiter = bulletMarkerOrDelimiter
- block.isLooseListItem = type === 'loose_item_start'
+ block = this.createBlock('li', {
+ listItemType: checked !== undefined ? 'task' : listItemType,
+ bulletMarkerOrDelimiter,
+ isLooseListItem: type === 'loose_item_start'
+ })
+
if (checked !== undefined) {
- const input = this.createBlock('input')
- input.checked = checked
+ const input = this.createBlock('input', {
+ checked
+ })
+
this.appendChild(block, input)
}
this.appendChild(parentList[0], block)
@@ -260,10 +345,10 @@ const importRegister = ContentState => {
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
// fix #752, but I don't know why the vanlished.
html = html.replace(/ /g, ' ')
- const markdown = turndownService.turndown(html) // .replace(/(\\)\\/g, '$1')
+ html = turnSoftBreakToSpan(html)
+ const markdown = turndownService.turndown(html)
return markdown
}
@@ -308,7 +393,7 @@ const importRegister = ContentState => {
// set cursor
const travel = blocks => {
for (const block of blocks) {
- const { key, text, children, editable, type, functionType } = block
+ const { key, text, children, editable } = block
if (text) {
const offset = text.indexOf(CURSOR_DNA)
if (offset > -1) {
@@ -318,17 +403,6 @@ const importRegister = ContentState => {
start: { key, offset },
end: { key, offset }
}
- // handle cursor in Math block, need to remove `CURSOR_DNA` in preview block
- if (type === 'span' && functionType === 'codeLine') {
- const preBlock = this.getParent(this.getParent(block))
- const code = this.codeBlocks.get(preBlock.key)
- if (!code) return
- const offset = code.indexOf(CURSOR_DNA)
- if (offset > -1) {
- const newCode = code.substring(0, offset) + code.substring(offset + CURSOR_DNA.length)
- this.codeBlocks.set(preBlock.key, newCode)
- }
- }
return
}
}
@@ -351,7 +425,6 @@ const importRegister = ContentState => {
}
ContentState.prototype.importMarkdown = function (markdown) {
- this.codeBlocks = new Map()
this.blocks = this.markdownToState(markdown)
}
}
diff --git a/src/muya/lib/utils/index.js b/src/muya/lib/utils/index.js
index be2879ad..604d9f11 100644
--- a/src/muya/lib/utils/index.js
+++ b/src/muya/lib/utils/index.js
@@ -23,6 +23,8 @@ export const isEven = number => Math.abs(number) % 2 === 0
export const isLengthEven = (str = '') => str.length % 2 === 0
export const snakeToCamel = name => name.replace(/_([a-z])/g, (p0, p1) => p1.toUpperCase())
+
+export const camelToSnake = name => name.replace(/([A-Z])/g, (_, p) => `-${p.toLowerCase()}`)
/**
* Are two arrays have intersection
*/
diff --git a/src/muya/lib/utils/turndownService.js b/src/muya/lib/utils/turndownService.js
index f0609807..51a0cef4 100644
--- a/src/muya/lib/utils/turndownService.js
+++ b/src/muya/lib/utils/turndownService.js
@@ -26,11 +26,13 @@ export const usePluginAddRules = turndownService => {
}
})
- // 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', {
+ // handle `line break` in code block
+ // add `LINE_BREAK` to the end of every code line but not the last line.
+ turndownService.addRule('codeLineBreak', {
filter (node, options) {
- return node.nodeName === 'SPAN' && node.classList.contains(CLASS_OR_ID['AG_LINE']) && node.nextElementSibling
+ return (
+ node.nodeName === 'SPAN' && node.classList.contains(CLASS_OR_ID['AG_CODE_LINE']) && node.nextElementSibling
+ )
},
replacement (content, node, options) {
return content + LINE_BREAK
diff --git a/src/muya/themes/default.css b/src/muya/themes/default.css
index 450b5835..dbb05e6e 100644
--- a/src/muya/themes/default.css
+++ b/src/muya/themes/default.css
@@ -141,12 +141,6 @@ kbd {
text-decoration: none;
}
- h1 .ag-gray,
- h2 .ag-gray,
- h3 .ag-gray {
- font-size: .6em;
- }
-
.ag-header-tight-space {
margin-left: -.3em;
}
@@ -227,29 +221,48 @@ kbd {
}
h1 {
- font-size: 40px;
+ font-size: 30px;
}
h2 {
- font-size: 32px;
- }
-
- h3 {
- font-size: 28px;
- }
-
- h4 {
font-size: 24px;
}
- h5 {
+ h3 {
+ font-size: 22px;
+ }
+
+ h4 {
font-size: 20px;
}
+ h5 {
+ font-size: 18px;
+ }
+
h6 {
font-size: 16px;
}
+ h1 .ag-gray {
+ font-size: 21px;
+ }
+ h2 .ag-gray {
+ font-size: 20px;
+ }
+ h3 .ag-gray {
+ font-size: 19px;
+ }
+ h4 .ag-gray {
+ font-size: 18px;
+ }
+ h5 .ag-gray {
+ font-size: 17px;
+ }
+ h6 .ag-gray {
+ font-size: 16px;
+ }
+
p,
blockquote,
ul,
diff --git a/test/unit/data/common/BasicTextFormatting.md b/test/unit/data/common/BasicTextFormatting.md
index 4e604031..0c63416b 100644
--- a/test/unit/data/common/BasicTextFormatting.md
+++ b/test/unit/data/common/BasicTextFormatting.md
@@ -1,4 +1,10 @@
-# Basic Text Formatting
+## Basic Text Formatting
+
+**Strong** text __Also Strong__
+
+~~strike~~ and `inline code`
+
+under line and 43 H2O
*this is in italic* and _so is this_
@@ -12,17 +18,19 @@
this is strike through text
+So _a_ single _word_ followed _b_y _a_nother
+
+So __a__ single __word__ followed __b__y __a__nother
+
+## Some markdown extentions
+
+This is emoji :man:
+
+This is inline math $a \ne b$
+
## Paragraph
A two trailing spaces and a new line
makes a line break.
Two new lines make a new paragraph.
-
-## Failing Tests
-
-```
-So _a_ single _word_ followed _b_y _a_nother
-
-So __a__ single __word__ followed __b__y __a__nother
-```
diff --git a/test/unit/data/common/CodeBlocks.md b/test/unit/data/common/CodeBlocks.md
index 091087d3..6e270159 100644
--- a/test/unit/data/common/CodeBlocks.md
+++ b/test/unit/data/common/CodeBlocks.md
@@ -1,5 +1,7 @@
# Code Blocks
+## Indent Code Block
+
This line won't *have any markdown* formatting applied.
I can even write HTML and it will show up as text.
This is great for showing program source code, or HTML or even
@@ -9,6 +11,8 @@
Within a paragraph, you can use backquotes to do the same thing.
`This won't be *italic* or **bold** at all.`
+## Fence Code Block
+
```cpp
#include
diff --git a/test/unit/data/common/Headings.md b/test/unit/data/common/Headings.md
index 3fee4dc0..248fa527 100644
--- a/test/unit/data/common/Headings.md
+++ b/test/unit/data/common/Headings.md
@@ -1,5 +1,7 @@
# Headings
+## Setext heading
+
This is a huge header
===
@@ -9,6 +11,14 @@ this is a smaller header
header
---
+This is a huge header
+==================
+
+this is a smaller header
+------------------
+
+## Atx heading
+
## ATX Headings
# foo
@@ -53,20 +63,6 @@ foo
bar
-## Failing Tests
-
-Headings and horizontal rules are shrinked to three characters because this simplify the parsing - maybe we should change this.
-
-```
-This is a huge header
-==================
-
-this is a smaller header
-------------------
-```
-
-```
## Horizontal Rule
-----------------
-```
+ - - - - -- --- --- ----
diff --git a/yarn.lock b/yarn.lock
index 9e58352f..4e3d181b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8682,10 +8682,10 @@ pretty-error@^2.0.2:
renderkid "^2.0.1"
utila "~0.4"
-prismjs@^1.16.0:
- version "1.16.0"
- resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.16.0.tgz#406eb2c8aacb0f5f0f1167930cb83835d10a4308"
- integrity sha512-OA4MKxjFZHSvZcisLGe14THYsug/nF6O1f0pAJc0KN0wTyAcLqmsbE+lTGKSpyh+9pEW57+k6pg2AfYR+coyHA==
+prismjs2@^1.15.1:
+ version "1.15.1"
+ resolved "https://registry.yarnpkg.com/prismjs2/-/prismjs2-1.15.1.tgz#6dda1b9aa7e8ecddf55b145f2189b605f89e2738"
+ integrity sha512-tDYrcjuYxi5VceNCniF7YjxFTHJv7unA5KbN9EVZh0hnKmEaxdSSe43Gagobvue5UnbnUSB0y+l5b8Y3C1cXkA==
optionalDependencies:
clipboard "^2.0.0"