mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 00:51:26 +08:00
container block preview and inline syntax error (#992)
* opti: container block preview * remove unused codes * rewrite createBlock method * remove ag-line classname * just push codes * hand enter + shift in paragraph * update import markdown and export markdown * update part updateCtrl * update indent code block * auto indent when press shift + enter * update thematic break * update inline syntax update reg * update list and task list * update atx heading and setext heading * update paragraph * update block quote * adjust cursor in heading * update codes * paragraph turn into feature check * check copy paste * update turn into * fix: delete last # error * fix: turn setext heading to atx heading error * fix: delete thematic break error * paste and copy * workarond turndown to support soft line break * fix: unable create table * modify export markdown * modify test markdown * fix: cursor error when update blockquote * readd cursor check when dispatch changes * fix: inline math create a lot extra char * add code cache clear after each render * fallback to prismjs2
This commit is contained in:
parent
77ff23c2c8
commit
230c90c920
161
doc/Block addition properties and its value.md
Normal file
161
doc/Block addition properties and its value.md
Normal file
@ -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
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
@ -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 {
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -136,6 +136,7 @@ const clickCtrl = ContentState => {
|
||||
return
|
||||
}
|
||||
this.cursor = cursor
|
||||
|
||||
return this.partialRender()
|
||||
})
|
||||
} else {
|
||||
|
@ -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'
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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 ])
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
13
src/muya/lib/parser/render/renderInlines/softLineBreak.js
Normal file
13
src/muya/lib/parser/render/renderInlines/softLineBreak.js
Normal file
@ -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)
|
||||
]
|
||||
}
|
@ -25,7 +25,8 @@ export const inlineRules = {
|
||||
'tail_header': /^(\s{1,}#{1,})(\s*)$/,
|
||||
'html_tag': /^(<!--[\s\S]*?-->|(<([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})/,
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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 })
|
||||
|
@ -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'
|
||||
|
@ -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`)
|
||||
}
|
||||
|
@ -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(
|
||||
`<x-mt id="turn-root">${html}</x-mt>`,
|
||||
'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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -1,4 +1,10 @@
|
||||
# Basic Text Formatting
|
||||
## Basic Text Formatting
|
||||
|
||||
**Strong** text __Also Strong__
|
||||
|
||||
~~strike~~ and `inline code`
|
||||
|
||||
<u>under line</u> and 4<sup>3</sup> H<sub>2</sub>O
|
||||
|
||||
*this is in italic* and _so is this_
|
||||
|
||||
@ -12,17 +18,19 @@
|
||||
|
||||
<s>this is strike through text</s>
|
||||
|
||||
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
|
||||
```
|
||||
|
@ -1,5 +1,7 @@
|
||||
# Code Blocks
|
||||
|
||||
## Indent Code Block
|
||||
|
||||
This line won't *have any markdown* formatting applied.
|
||||
I can even write <b>HTML</b> 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 <iostream>
|
||||
|
||||
|
@ -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
|
||||
|
||||
----------------
|
||||
```
|
||||
- - - - -- --- --- ----
|
||||
|
@ -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"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user