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:
Ran Luo 2019-05-04 23:41:46 +08:00 committed by GitHub
parent 77ff23c2c8
commit 230c90c920
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1252 additions and 806 deletions

View 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

View File

@ -189,7 +189,7 @@
"keyboard-layout": "^2.0.15", "keyboard-layout": "^2.0.15",
"mermaid": "^8.0.0", "mermaid": "^8.0.0",
"popper.js": "^1.15.0", "popper.js": "^1.15.0",
"prismjs": "^1.16.0", "prismjs2": "^1.15.1",
"snabbdom": "^0.7.3", "snabbdom": "^0.7.3",
"snabbdom-to-html": "^5.1.1", "snabbdom-to-html": "^5.1.1",
"snapsvg": "^0.5.1", "snapsvg": "^0.5.1",

View File

@ -35,11 +35,11 @@ const setParagraphMenuItemStatus = bool => {
.forEach(item => (item.enabled = bool)) .forEach(item => (item.enabled = bool))
} }
const disableNoMultiple = disableLabels => { const setMultipleStatus = (list, status) => {
const paragraphMenuItem = getMenuItemById('paragraphMenuEntry') const paragraphMenuItem = getMenuItemById('paragraphMenuEntry')
paragraphMenuItem.submenu.items paragraphMenuItem.submenu.items
.filter(item => item.id && disableLabels.includes(item.id)) .filter(item => item.id && list.includes(item.id))
.forEach(item => (item.enabled = false)) .forEach(item => (item.enabled = status))
} }
const setCheckedMenuItem = affiliation => { const setCheckedMenuItem = affiliation => {
@ -105,15 +105,17 @@ ipcMain.on('AGANI::selection-change', (e, { start, end, affiliation }) => {
(end.type === 'span' && end.block.functionType === 'codeLine') (end.type === 'span' && end.block.functionType === 'codeLine')
) { ) {
setParagraphMenuItemStatus(false) setParagraphMenuItemStatus(false)
if (start.block.functionType === 'codeLine' || end.block.functionType === 'codeLine') { if (start.block.functionType === 'codeLine' || end.block.functionType === 'codeLine') {
setMultipleStatus(['codeFencesMenuItem'], true)
formatMenuItem.submenu.items.forEach(item => (item.enabled = false)) formatMenuItem.submenu.items.forEach(item => (item.enabled = false))
} }
} else if (start.key !== end.key) { } else if (start.key !== end.key) {
formatMenuItem.submenu.items formatMenuItem.submenu.items
.filter(item => item.id && DISABLE_LABELS.includes(item.id)) .filter(item => item.id && DISABLE_LABELS.includes(item.id))
.forEach(item => (item.enabled = false)) .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))) { } else if (!affiliation.slice(0, 3).some(p => /ul|ol/.test(p.type))) {
disableNoMultiple(['looseListItemMenuItem']) setMultipleStatus(['looseListItemMenuItem'], false)
} }
}) })

View File

@ -22,7 +22,7 @@ pre {
outline: none; 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'; content: 'Type @ to insert';
color: var(--editorColor10); 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; position: relative;
} }
.ag-paragraph:empty::after, .ag-atx-line:empty::after,
.ag-line:empty::after { .ag-thematic-break-line:empty::after,
content: '\200B' .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; display: block;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; 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; margin: 0 5px;
} }
.ag-hard-line-break::after { .ag-hard-line-break-space::after {
content: '↩'; content: '↩';
opacity: .5; opacity: .5;
} }
.ag-line-end {
display: block;
}
*:not(.ag-hide)::selection, .ag-selection { *:not(.ag-hide)::selection, .ag-selection {
background: var(--selectionColor); background: var(--selectionColor);
color: var(--editorColor); 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; color: transparent;
} }
figure.ag-container-block pre, figure.ag-container-block pre {
div.ag-function-html pre.ag-html-block {
width: 0; width: 0;
height: 0; height: 0;
overflow: hidden; overflow: hidden;
@ -90,7 +98,6 @@ div.ag-function-html pre.ag-html-block {
overflow: visible; overflow: visible;
} }
div.ag-function-html.ag-active pre.ag-html-block,
figure.ag-active.ag-container-block pre { figure.ag-active.ag-container-block pre {
position: static; position: static;
width: 100%; width: 100%;
@ -100,11 +107,11 @@ figure.ag-active.ag-container-block pre {
display: block; display: block;
} }
div.ag-function-html .ag-html-preview { figure[data-role="HTML"] .ag-html-preview {
display: block; display: block;
} }
div.ag-function-html.ag-active .ag-html-preview { figure[data-role="HTML"].ag-active .ag-html-preview {
display: none; display: none;
} }
@ -474,7 +481,6 @@ pre.ag-multiple-math span.ag-code-line:first-of-type:empty::after {
figure, figure,
pre.ag-html-block, pre.ag-html-block,
div.ag-function-html,
pre.ag-fence-code, pre.ag-fence-code,
pre.ag-indent-code, pre.ag-indent-code,
li.ag-list-item > p.ag-paragraph { li.ag-list-item > p.ag-paragraph {

View File

@ -6,7 +6,7 @@ import voidHtmlTags from 'html-tags/void'
// Electron 2.0.2 not support yet! So give a default value 4 // 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 DEVICE_MEMORY = navigator.deviceMemory || 4 // Get the divice memory number(Chrome >= 63)
export const UNDO_DEPTH = DEVICE_MEMORY >= 4 ? 100 : 50 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 VOID_HTML_TAGS = voidHtmlTags
export const HTML_TAGS = htmlTags export const HTML_TAGS = htmlTags
// TYPE1 ~ TYPE7 according to https://github.github.com/gfm/#html-blocks // 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_FRONT_ICON',
'AG_GRAY', 'AG_GRAY',
'AG_HARD_LINE_BREAK', 'AG_HARD_LINE_BREAK',
'AG_HARD_LINE_BREAK_SPACE',
'AG_LINE_END',
'AG_HEADER_TIGHT_SPACE', 'AG_HEADER_TIGHT_SPACE',
'AG_HIDE', 'AG_HIDE',
'AG_HIGHLIGHT', 'AG_HIGHLIGHT',
@ -99,7 +101,6 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_INLINE_RULE', 'AG_INLINE_RULE',
'AG_LANGUAGE', 'AG_LANGUAGE',
'AG_LANGUAGE_INPUT', 'AG_LANGUAGE_INPUT',
'AG_LINE',
'AG_LINK', 'AG_LINK',
'AG_LINK_IN_BRACKET', 'AG_LINK_IN_BRACKET',
'AG_LIST_ITEM', 'AG_LIST_ITEM',
@ -111,6 +112,7 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_RUBY_TEXT', 'AG_RUBY_TEXT',
'AG_RUBY_RENDER', 'AG_RUBY_RENDER',
'AG_SELECTED', 'AG_SELECTED',
'AG_SOFT_LINE_BREAK',
'AG_MATH_ERROR', 'AG_MATH_ERROR',
'AG_MATH_MARKER', 'AG_MATH_MARKER',
'AG_MATH_RENDER', 'AG_MATH_RENDER',
@ -159,7 +161,18 @@ export const DEFAULT_TURNDOWN_CONFIG = {
codeBlockStyle: 'fenced', // fenced or indented codeBlockStyle: 'fenced', // fenced or indented
fence: '```', // ``` or ~~~ fence: '```', // ``` or ~~~
emDelimiter: '*', // _ 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 = { export const FORMAT_MARKER_MAP = {

View File

@ -4,7 +4,7 @@ import selection from '../selection'
// If the next block is header, put cursor after the `#{1,6} *` // If the next block is header, put cursor after the `#{1,6} *`
const adjustOffset = (offset, block, event) => { 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) const match = /^\s{0,3}(?:#{1,6})(?:\s{1,}|$)/.exec(block.text)
if (match) { if (match) {
return match[0].length return match[0].length

View File

@ -106,11 +106,32 @@ const backspaceCtrl = ContentState => {
if (!start || !end) { if (!start || !end) {
return return
} }
const startBlock = this.getBlock(start.key) const startBlock = this.getBlock(start.key)
const endBlock = this.getBlock(end.key) const endBlock = this.getBlock(end.key)
const maybeLastRow = this.getParent(endBlock) const maybeLastRow = this.getParent(endBlock)
const startOutmostBlock = this.findOutMostBlock(startBlock) const startOutmostBlock = this.findOutMostBlock(startBlock)
const endOutmostBlock = this.findOutMostBlock(endBlock) 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 // fix: #897
const { text } = startBlock const { text } = startBlock
const tokens = tokenizer(text) const tokens = tokenizer(text)
@ -219,7 +240,7 @@ const backspaceCtrl = ContentState => {
) { ) {
const preBlock = this.getParent(parent) const preBlock = this.getParent(parent)
const pBlock = this.createBlock('p') 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 key = lineBlock.key
const offset = 0 const offset = 0
this.appendChild(pBlock, lineBlock) this.appendChild(pBlock, lineBlock)
@ -235,10 +256,8 @@ const backspaceCtrl = ContentState => {
case 'mermaid': case 'mermaid':
case 'sequence': case 'sequence':
case 'vega-lite': case 'vega-lite':
referenceBlock = this.getParent(preBlock)
break
case 'html': case 'html':
referenceBlock = this.getParent(this.getParent(preBlock)) referenceBlock = this.getParent(preBlock)
break break
} }
this.insertBefore(pBlock, referenceBlock) this.insertBefore(pBlock, referenceBlock)

View File

@ -136,6 +136,7 @@ const clickCtrl = ContentState => {
return return
} }
this.cursor = cursor this.cursor = cursor
return this.partialRender() return this.partialRender()
}) })
} else { } else {

View File

@ -72,27 +72,6 @@ const codeBlockCtrl = ContentState => {
this.partialRender() 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] * [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) const match = CODE_UPDATE_REP.exec(text)
if (match || lang) { if (match || lang) {
const codeBlock = this.createBlock('code') 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 language = lang || (match ? match[1] : '')
const inputBlock = this.createBlock('span', language) const inputBlock = this.createBlock('span', { text: language })
loadLanguage(language) loadLanguage(language)
inputBlock.functionType = 'languageInput' inputBlock.functionType = 'languageInput'
block.type = 'pre' block.type = 'pre'

View File

@ -4,44 +4,57 @@ const FUNCTION_TYPE_LANG = {
'flowchart': 'yaml', 'flowchart': 'yaml',
'mermaid': 'yaml', 'mermaid': 'yaml',
'sequence': 'yaml', 'sequence': 'yaml',
'vega-lite': 'yaml' 'vega-lite': 'yaml',
'html': 'markup'
} }
const containerCtrl = ContentState => { const containerCtrl = ContentState => {
ContentState.prototype.createContainerBlock = function (functionType, value = '') { ContentState.prototype.createContainerBlock = function (functionType, value = '') {
const figureBlock = this.createBlock('figure') const figureBlock = this.createBlock('figure', {
figureBlock.functionType = functionType functionType
})
const { preBlock, preview } = this.createPreAndPreview(functionType, value) const { preBlock, preview } = this.createPreAndPreview(functionType, value)
this.appendChild(figureBlock, preBlock) this.appendChild(figureBlock, preBlock)
this.appendChild(figureBlock, preview) this.appendChild(figureBlock, preview)
this.codeBlocks.set(preBlock.key, value)
return figureBlock return figureBlock
} }
ContentState.prototype.createPreAndPreview = function (functionType, value = '') { ContentState.prototype.createPreAndPreview = function (functionType, value = '') {
const preBlock = this.createBlock('pre') const lang = FUNCTION_TYPE_LANG[functionType]
const codeBlock = this.createBlock('code') const preBlock = this.createBlock('pre', {
preBlock.functionType = functionType functionType,
preBlock.lang = codeBlock.lang = FUNCTION_TYPE_LANG[functionType] lang
})
const codeBlock = this.createBlock('code', {
lang
})
this.appendChild(preBlock, codeBlock) this.appendChild(preBlock, codeBlock)
if (typeof value === 'string' && value) { if (typeof value === 'string' && value) {
value.replace(/^\s+/, '').split(LINE_BREAKS_REG).forEach(line => { value.replace(/^\s+/, '').split(LINE_BREAKS_REG).forEach(line => {
const codeLine = this.createBlock('span', line) const codeLine = this.createBlock('span', {
codeLine.functionType = 'codeLine' text: line,
codeLine.lang = FUNCTION_TYPE_LANG[functionType] functionType: 'codeLine',
lang
})
this.appendChild(codeBlock, codeLine) this.appendChild(codeBlock, codeLine)
}) })
} else { } else {
const emptyLine = this.createBlock('span') const emptyLine = this.createBlock('span', {
emptyLine.functionType = 'codeLine' functionType: 'codeLine',
emptyLine.lang = FUNCTION_TYPE_LANG[functionType] lang
})
this.appendChild(codeBlock, emptyLine) this.appendChild(codeBlock, emptyLine)
} }
const preview = this.createBlock('div', '', false) const preview = this.createBlock('div', {
this.codeBlocks.set(preBlock.key, '') editable: false,
preview.functionType = functionType functionType
})
return { preBlock, preview } return { preBlock, preview }
} }

View File

@ -101,6 +101,12 @@ const copyCutCtrl = ContentState => {
hb.replaceWith(pre) 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`) const mathBlock = wrapper.querySelectorAll(`figure.ag-container-block`)
for (const mb of mathBlock) { for (const mb of mathBlock) {
const preElement = mb.querySelector('pre[data-role]') const preElement = mb.querySelector('pre[data-role]')
@ -128,6 +134,7 @@ const copyCutCtrl = ContentState => {
let htmlData = wrapper.innerHTML let htmlData = wrapper.innerHTML
const textData = this.htmlToMarkdown(htmlData) const textData = this.htmlToMarkdown(htmlData)
htmlData = marked(textData) htmlData = marked(textData)
return { html: htmlData, text: textData } return { html: htmlData, text: textData }
} }

View File

@ -11,6 +11,7 @@ const getIndentSpace = text => {
} }
const enterCtrl = ContentState => { const enterCtrl = ContentState => {
// TODO@jocs this function need opti.
ContentState.prototype.chopBlockByCursor = function (block, key, offset) { ContentState.prototype.chopBlockByCursor = function (block, key, offset) {
const newBlock = this.createBlock('p') const newBlock = this.createBlock('p')
const { children } = block const { children } = block
@ -28,7 +29,7 @@ const enterCtrl = ContentState => {
this.prependChild(newBlock, activeLine) this.prependChild(newBlock, activeLine)
} else if (offset < text.length) { } else if (offset < text.length) {
activeLine.text = text.substring(0, offset) 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) this.prependChild(newBlock, newLine)
} }
return newBlock return newBlock
@ -197,23 +198,39 @@ const enterCtrl = ContentState => {
// handle `shift + enter` insert `soft line break` or `hard line break` // 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` // only cursor in `line block` can create `soft line break` and `hard line break`
// handle line in code block // handle line in code block
if ( if (event.shiftKey && block.type === 'span' && block.functionType === 'paragraphContent') {
(event.shiftKey && block.type === 'span') || let { offset } = start
(block.type === 'span' && block.functionType === 'codeLine') 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 { text } = block
const newLineText = text.substring(start.offset) const newLineText = text.substring(start.offset)
const autoIndent = checkAutoIndent(text, start.offset) const autoIndent = checkAutoIndent(text, start.offset)
const indent = getIndentSpace(text) const indent = getIndentSpace(text)
block.text = text.substring(0, start.offset) block.text = text.substring(0, start.offset)
const newLine = this.createBlock('span', `${indent}${newLineText}`) const newLine = this.createBlock('span', {
newLine.functionType = block.functionType text: `${indent}${newLineText}`,
newLine.lang = block.lang functionType: block.functionType,
lang: block.lang
})
this.insertAfter(newLine, block) this.insertAfter(newLine, block)
let { key } = newLine let { key } = newLine
let offset = indent.length let offset = indent.length
if (autoIndent) { 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.functionType = block.functionType
emptyLine.lang = block.lang emptyLine.lang = block.lang
this.insertAfter(emptyLine, block) this.insertAfter(emptyLine, block)
@ -221,11 +238,6 @@ const enterCtrl = ContentState => {
offset = indent.length + this.tabSize offset = indent.length + this.tabSize
} }
if (indent.length >= 4 && !block.preSibling) {
this.indentCodeBlockUpdate(block)
offset = offset - 4
}
this.cursor = { this.cursor = {
start: { key, offset }, start: { key, offset },
end: { key, offset } end: { key, offset }
@ -323,8 +335,14 @@ const enterCtrl = ContentState => {
post = `${PREFIX} ${post}` post = `${PREFIX} ${post}`
} }
block.text = pre block.text = pre
newBlock = this.createBlock(type, post) newBlock = this.createBlock(type, {
newBlock.headingStyle = block.headingStyle headingStyle: block.headingStyle
})
const headerContent = this.createBlock('span', {
text: post,
functionType: block.headingStyle === 'atx'? 'atxLine' : 'paragraphContent'
})
this.appendChild(newBlock, headerContent)
if (block.marker) { if (block.marker) {
newBlock.marker = block.marker newBlock.marker = block.marker
} }

View File

@ -2,61 +2,20 @@ import { VOID_HTML_TAGS, HTML_TAGS } from '../config'
import { inlineRules } from '../parser/rules' import { inlineRules } from '../parser/rules'
const HTML_BLOCK_REG = /^<([a-zA-Z\d-]+)(?=\s|>)[^<>]*?>$/ const HTML_BLOCK_REG = /^<([a-zA-Z\d-]+)(?=\s|>)[^<>]*?>$/
const LINE_BREAKS = /\n/
const htmlBlock = ContentState => { 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) { ContentState.prototype.createHtmlBlock = function (code) {
const block = this.createBlock('figure') const block = this.createBlock('figure')
block.functionType = 'html' block.functionType = 'html'
const htmlBlock = this.createCodeInHtml(code) const { preBlock, preview } = this.createPreAndPreview('html', code)
this.appendChild(block, htmlBlock) this.appendChild(block, preBlock)
this.appendChild(block, preview)
return block return block
} }
ContentState.prototype.initHtmlBlock = function (block) { ContentState.prototype.initHtmlBlock = function (block) {
let htmlContent = '' let htmlContent = ''
const text = block.type === 'p' const text = block.children[0].text
? block.children.map((child => {
return child.text
})).join('\n').trim()
: block.text
const matches = inlineRules.html_tag.exec(text) const matches = inlineRules.html_tag.exec(text)
if (matches) { if (matches) {
const tag = matches[3] const tag = matches[3]
@ -83,9 +42,11 @@ const htmlBlock = ContentState => {
block.functionType = 'html' block.functionType = 'html'
block.text = htmlContent block.text = htmlContent
block.children = [] block.children = []
const codeContainer = this.createCodeInHtml(htmlContent) const { preBlock, preview } = this.createPreAndPreview('html', htmlContent)
this.appendChild(block, codeContainer) this.appendChild(block, preBlock)
return codeContainer.children[0] // preBlock this.appendChild(block, preview)
return preBlock // preBlock
} }
ContentState.prototype.updateHtmlBlock = function (block) { ContentState.prototype.updateHtmlBlock = function (block) {

View File

@ -63,7 +63,6 @@ class ContentState {
this.exemption = new Set() this.exemption = new Set()
this.blocks = [ this.createBlockP() ] this.blocks = [ this.createBlockP() ]
this.stateRender = new StateRender(muya) this.stateRender = new StateRender(muya)
this.codeBlocks = new Map()
this.renderRange = [ null, null ] this.renderRange = [ null, null ]
this.currentCursor = null this.currentCursor = null
// you'll select the outmost block of current cursor when you click the front icon. // 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 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. * 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() const key = getUniqueId()
return { const blockData = {
key, key,
text: '',
type, type,
text, editable: true,
editable,
parent: null, parent: null,
preSibling: null, preSibling: null,
nextSibling: null, nextSibling: null,
children: [] children: []
} }
// give span block a default functionType `paragraphContent`
if (type === 'span' && !extras.functionType) {
blockData.functionType = 'paragraphContent'
}
Object.assign(blockData, extras)
return blockData
} }
createBlockP (text = '') { createBlockP (text = '') {
const pBlock = this.createBlock('p') const pBlock = this.createBlock('p')
const lineBlock = this.createBlock('span', text) const contentBlock = this.createBlock('span', { text })
this.appendChild(pBlock, lineBlock) this.appendChild(pBlock, contentBlock)
return pBlock return pBlock
} }

View File

@ -32,7 +32,7 @@ const inputCtrl = ContentState => {
// Input @ to quick insert paragraph // Input @ to quick insert paragraph
ContentState.prototype.checkQuickInsert = function (block) { ContentState.prototype.checkQuickInsert = function (block) {
const { type, text, functionType } = block const { type, text, functionType } = block
if (type !== 'span' || functionType) return false if (type !== 'span' || functionType !== 'paragraphContent') return false
return /^@\S*$/.test(text) return /^@\S*$/.test(text)
} }
@ -88,6 +88,7 @@ const inputCtrl = ContentState => {
const block = this.getBlock(key) const block = this.getBlock(key)
const paragraph = document.querySelector(`#${key}`) const paragraph = document.querySelector(`#${key}`)
let text = getTextContent(paragraph, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ]) let text = getTextContent(paragraph, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ])
let needRender = false let needRender = false
let needRenderAll = false let needRenderAll = false
if (oldStart.key !== oldEnd.key) { if (oldStart.key !== oldEnd.key) {
@ -196,7 +197,26 @@ const inputCtrl = ContentState => {
if (this.checkNotSameToken(block.text, text)) { if (this.checkNotSameToken(block.text, text)) {
needRender = true needRender = true
} }
// 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 block.text = text
}
if (beginRules['reference_definition'].test(text)) { if (beginRules['reference_definition'].test(text)) {
needRenderAll = true needRenderAll = true
} }
@ -226,7 +246,6 @@ const inputCtrl = ContentState => {
// Update preview content of math block // Update preview content of math block
if (block && block.type === 'span' && block.functionType === 'codeLine') { if (block && block.type === 'span' && block.functionType === 'codeLine') {
needRender = true needRender = true
this.updateCodeBlocks(block)
} }
this.cursor = { start, end } this.cursor = { start, end }

View File

@ -68,12 +68,19 @@ const paragraphCtrl = ContentState => {
ContentState.prototype.handleFrontMatter = function () { ContentState.prototype.handleFrontMatter = function () {
const firstBlock = this.blocks[0] const firstBlock = this.blocks[0]
if (firstBlock.type === 'pre' && firstBlock.functionType === 'frontmatter') return if (firstBlock.type === 'pre' && firstBlock.functionType === 'frontmatter') return
const frontMatter = this.createBlock('pre') const lang = 'yaml'
const codeBlock = this.createBlock('code') const frontMatter = this.createBlock('pre', {
const emptyLine = this.createBlock('span') functionType: 'frontmatter',
frontMatter.lang = codeBlock.lang = emptyLine.lang = 'yaml' lang
emptyLine.functionType = 'codeLine' })
frontMatter.functionType = 'frontmatter' const codeBlock = this.createBlock('code', {
lang
})
const emptyLine = this.createBlock('span', {
functionType: 'codeLine',
lang
})
this.appendChild(codeBlock, emptyLine) this.appendChild(codeBlock, emptyLine)
this.appendChild(frontMatter, codeBlock) this.appendChild(frontMatter, codeBlock)
this.insertBefore(frontMatter, firstBlock) this.insertBefore(frontMatter, firstBlock)
@ -87,14 +94,13 @@ const paragraphCtrl = ContentState => {
ContentState.prototype.handleListMenu = function (paraType, insertMode) { ContentState.prototype.handleListMenu = function (paraType, insertMode) {
const { start, end, affiliation } = this.selectionChange(this.cursor) const { start, end, affiliation } = this.selectionChange(this.cursor)
const { orderListMarker, bulletListMarker } = this const { orderListMarker, bulletListMarker, preferLooseListItem } = this
const [blockType, listType] = paraType.split('-') const [blockType, listType] = paraType.split('-')
const isListed = affiliation.slice(0, 3).filter(b => /ul|ol/.test(b.type)) const isListed = affiliation.slice(0, 3).filter(b => /ul|ol/.test(b.type))
const { preferLooseListItem } = this
if (isListed.length && !insertMode) { if (isListed.length && !insertMode) {
const listBlock = isListed[0] const listBlock = isListed[0]
if (listType === isListed[0].listType) { if (listType === listBlock.listType) {
const listItems = listBlock.children const listItems = listBlock.children
listItems.forEach(listItem => { listItems.forEach(listItem => {
listItem.children.forEach(itemParagraph => { listItem.children.forEach(itemParagraph => {
@ -214,41 +220,74 @@ const paragraphCtrl = ContentState => {
if (affiliation.length && affiliation[0].type === 'pre' && /code/.test(affiliation[0].functionType)) { if (affiliation.length && affiliation[0].type === 'pre' && /code/.test(affiliation[0].functionType)) {
const preBlock = affiliation[0] const preBlock = affiliation[0]
const codeLines = preBlock.children[1].children const codeLines = preBlock.children[1].children
this.codeBlocks.delete(preBlock.key)
preBlock.type = 'p' preBlock.type = 'p'
preBlock.children = [] 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) { for (const line of codeLines) {
delete line.lang if (line.key !== start.key && !startStop) {
delete line.functionType startOffset += line.text.length + 1
this.appendChild(preBlock, line) } 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 = { this.cursor = {
start: this.cursor.start, start: { key, offset: startOffset },
end: this.cursor.end end: { key, offset: endOffset }
} }
} else { } else {
if (start.key === end.key) { if (start.key === end.key) {
if (startBlock.type === 'span') { if (startBlock.type === 'span') {
startBlock = this.getParent(startBlock) const anchorBlock = this.getParent(startBlock)
startBlock.type = 'pre' const lang = ''
const codeBlock = this.createBlock('code') const preBlock = this.createBlock('pre', {
const inputBlock = this.createBlock('span', '') functionType: 'fencecode',
inputBlock.functionType = 'languageInput' lang
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)
}) })
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 { key } = inputBlock
const offset = 0 const offset = 0
@ -266,21 +305,31 @@ const paragraphCtrl = ContentState => {
const { parent, startIndex, endIndex } = this.getCommonParent() const { parent, startIndex, endIndex } = this.getCommonParent()
const children = parent ? parent.children : this.blocks const children = parent ? parent.children : this.blocks
const referBlock = children[endIndex] const referBlock = children[endIndex]
const preBlock = this.createBlock('pre') const lang = ''
const codeBlock = this.createBlock('code') const preBlock = this.createBlock('pre', {
preBlock.functionType = 'fencecode' functionType: 'fencecode',
preBlock.lang = codeBlock.lang = '' lang
})
const codeBlock = this.createBlock('code', {
lang
})
const listIndentation = this.listIndentation const listIndentation = this.listIndentation
const markdown = new ExportMarkdown(children.slice(startIndex, endIndex + 1), listIndentation).generate() const markdown = new ExportMarkdown(children.slice(startIndex, endIndex + 1), listIndentation).generate()
markdown.split(LINE_BREAKS_REG).forEach(text => { markdown.split(LINE_BREAKS_REG).forEach(text => {
const codeLine = this.createBlock('span', text) const codeLine = this.createBlock('span', {
codeLine.lang = '' text,
codeLine.functionType = 'codeLine' lang,
functionType: 'codeLine'
})
this.appendChild(codeBlock, codeLine) this.appendChild(codeBlock, codeLine)
}) })
const inputBlock = this.createBlock('span', '') const inputBlock = this.createBlock('span', {
inputBlock.functionType = 'languageInput' functionType: 'languageInput'
})
this.appendChild(preBlock, inputBlock) this.appendChild(preBlock, inputBlock)
this.appendChild(preBlock, codeBlock) this.appendChild(preBlock, codeBlock)
this.insertAfter(preBlock, referBlock) this.insertAfter(preBlock, referBlock)
@ -392,7 +441,7 @@ const paragraphCtrl = ContentState => {
ContentState.prototype.updateParagraph = function (paraType, insertMode = false) { ContentState.prototype.updateParagraph = function (paraType, insertMode = false) {
const { start, end } = this.cursor const { start, end } = this.cursor
const block = this.getBlock(start.key) const block = this.getBlock(start.key)
const { type, text, functionType } = block const { text } = block
switch (paraType) { switch (paraType) {
case 'front-matter': { case 'front-matter': {
@ -445,15 +494,19 @@ const paragraphCtrl = ContentState => {
case 'degrade heading': case 'degrade heading':
case 'paragraph': { case 'paragraph': {
if (start.key !== end.key) return 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 &nbsp;
const [, hash, partText] = /(^ {0,3}#*[ \u00A0]*)([\s\S]*)/.exec(text)
let newLevel = 0 // 1, 2, 3, 4, 5, 6 let newLevel = 0 // 1, 2, 3, 4, 5, 6
let newType = 'p' let newType = 'p'
let key let key
if (/\d/.test(paraType)) { if (/\d/.test(paraType)) {
newLevel = Number(paraType.split(/\s/)[1]) newLevel = Number(paraType.split(/\s/)[1])
newType = `h${newLevel}` newType = `h${newLevel}`
} else if (paraType === 'upgrade heading' || paraType === 'degrade heading') { } else if (paraType === 'upgrade heading' || paraType === 'degrade heading') {
const currentLevel = getCurrentLevel(type) const currentLevel = getCurrentLevel(parent.type)
newLevel = currentLevel newLevel = currentLevel
if (paraType === 'upgrade heading' && currentLevel !== 1) { if (paraType === 'upgrade heading' && currentLevel !== 1) {
if (currentLevel === 0) newLevel = 6 if (currentLevel === 0) newLevel = 6
@ -475,48 +528,35 @@ const paragraphCtrl = ContentState => {
? '#'.repeat(newLevel) + `${String.fromCharCode(160)}${partText}` // &nbsp; code: 160 ? '#'.repeat(newLevel) + `${String.fromCharCode(160)}${partText}` // &nbsp; code: 160
: partText : partText
if (block.type === 'span' && newType !== 'p') { // No change
const header = this.createBlock(newType, newText) if (newType === 'p' && parent.type === newType) {
header.headingStyle = DEFAULT_TURNDOWN_CONFIG.headingStyle return
key = header.key }
const parent = this.getParent(block) // No change
if (this.isOnlyChild(block)) { 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.insertBefore(header, parent)
this.removeBlock(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 { } 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') {
const pBlock = this.createBlockP(newText) const pBlock = this.createBlockP(newText)
key = pBlock.children[0].key key = pBlock.children[0].key
this.insertAfter(pBlock, block) this.insertAfter(pBlock, parent)
this.removeBlock(block) this.removeBlock(parent)
} 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.cursor = { this.cursor = {
start: { key, offset: startOffset }, start: { key, offset: startOffset },
end: { key, offset: endOffset } end: { key, offset: endOffset }
@ -527,7 +567,11 @@ const paragraphCtrl = ContentState => {
const pBlock = this.createBlockP() const pBlock = this.createBlockP()
const archor = block.type === 'span' ? this.getParent(block) : block const archor = block.type === 'span' ? this.getParent(block) : block
const hrBlock = this.createBlock('hr') 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(hrBlock, archor)
this.insertAfter(pBlock, hrBlock) this.insertAfter(pBlock, hrBlock)
if (!text) { if (!text) {
@ -562,42 +606,23 @@ const paragraphCtrl = ContentState => {
const { start, end } = this.cursor const { start, end } = this.cursor
// if cursor is not in one line or paragraph, can not insert paragraph // if cursor is not in one line or paragraph, can not insert paragraph
if (start.key !== end.key) return if (start.key !== end.key) return
let block = this.getBlock(start.key) const block = this.getBlock(start.key)
let anchor = null
if (outMost) { if (outMost) {
block = this.findOutMostBlock(block) anchor = this.findOutMostBlock(block)
} else if (block.type === 'span' && !block.functionType) { } else {
block = this.getParent(block) anchor = this.getAnchor(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 // You can not insert paragraph before frontmatter
if (preBlock.functionType === 'frontmatter' && location === 'before') { if (!anchor || anchor && anchor.functionType === 'frontmatter' && location === 'before') {
return 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))))
}
const newBlock = this.createBlockP(text) const newBlock = this.createBlockP(text)
if (location === 'before') { if (location === 'before') {
this.insertBefore(newBlock, block) this.insertBefore(newBlock, anchor)
} else { } else {
this.insertAfter(newBlock, block) this.insertAfter(newBlock, anchor)
} }
const { key } = newBlock.children[0] const { key } = newBlock.children[0]
const offset = text.length const offset = text.length
@ -634,12 +659,7 @@ const paragraphCtrl = ContentState => {
// if copied block has pre block: html, multiplemath, vega-light, mermaid, flowchart, sequence... // if copied block has pre block: html, multiplemath, vega-light, mermaid, flowchart, sequence...
const copiedBlock = this.copyBlock(startOutmostBlock) const copiedBlock = this.copyBlock(startOutmostBlock)
this.insertAfter(copiedBlock, 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) const cursorBlock = this.firstInDescendant(copiedBlock)
// set cursor at the end of the first descendant of the duplicated block. // set cursor at the end of the first descendant of the duplicated block.
const { key, text } = cursorBlock const { key, text } = cursorBlock

View File

@ -8,21 +8,33 @@ const pasteCtrl = ContentState => {
// check paste type: `MERGE` or `NEWLINE` // check paste type: `MERGE` or `NEWLINE`
ContentState.prototype.checkPasteType = function (start, fragment) { ContentState.prototype.checkPasteType = function (start, fragment) {
const fragmentType = fragment.type const fragmentType = fragment.type
if (start.type === 'span') { const parent = this.getParent(start)
start = this.getParent(start)
} if (fragmentType === 'p') {
if (fragmentType === 'p') return 'MERGE' return 'MERGE'
if (fragmentType === 'blockquote') return 'NEWLINE' } else if (/^h\d/.test(fragmentType)) {
let parent = this.getParent(start) if (start.text) {
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)) {
return 'MERGE' return 'MERGE'
} else { } else {
return startType === fragmentType ? 'MERGE' : 'NEWLINE' 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 'NEWLINE'
} }
} }
@ -137,7 +149,7 @@ const pasteCtrl = ContentState => {
startBlock.text = prePartText + line startBlock.text = prePartText + line
} else { } else {
line = i === textList.length - 1 ? line + postPartText : line 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.functionType = startBlock.functionType
lineBlock.lang = startBlock.lang lineBlock.lang = startBlock.lang
this.insertAfter(lineBlock, referenceBlock) this.insertAfter(lineBlock, referenceBlock)
@ -161,7 +173,6 @@ const pasteCtrl = ContentState => {
end: { key, offset } end: { key, offset }
} }
} }
this.updateCodeBlocks(startBlock)
return this.partialRender() return this.partialRender()
} }
@ -179,41 +190,12 @@ const pasteCtrl = ContentState => {
// handle copyAsHtml // handle copyAsHtml
if (copyType === '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) { switch (type) {
case 'normal': { case 'normal': {
const htmlBlock = this.createBlock('p') const htmlBlock = this.createBlockP(text.trim())
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) this.insertAfter(htmlBlock, parent)
} else {
this.insertAfter(htmlBlock, startBlock)
}
if (
startBlock.type === 'span' && startBlock.text.length === 0 && this.isOnlyChild(startBlock)
) {
this.removeBlock(parent) this.removeBlock(parent)
}
// handler heading // handler heading
if (startBlock.text.length === 0 && startBlock.type !== 'span') {
this.removeBlock(startBlock)
}
this.insertHtmlBlock(htmlBlock) this.insertHtmlBlock(htmlBlock)
break break
} }
@ -222,24 +204,16 @@ const pasteCtrl = ContentState => {
let htmlBlock = null let htmlBlock = null
if (!startBlock.text || lines.length > 1) { if (!startBlock.text || lines.length > 1) {
htmlBlock = this.createBlock('p') htmlBlock = this.createBlockP((startBlock.text ? lines.slice(1) : lines).join('\n'))
;(startBlock.text ? lines.slice(1) : lines).map(line => this.createBlock('span', line))
.forEach(l => {
this.appendChild(htmlBlock, l)
})
} }
if (htmlBlock) { if (htmlBlock) {
if (startBlock.type === 'span') {
this.insertAfter(htmlBlock, parent) this.insertAfter(htmlBlock, parent)
} else {
this.insertAfter(htmlBlock, startBlock)
}
this.insertHtmlBlock(htmlBlock) this.insertHtmlBlock(htmlBlock)
} }
if (startBlock.text) { if (startBlock.text) {
appendHtml(lines[0]) appendHtml(lines[0])
} else { } else {
this.removeBlock(startBlock.type === 'span' ? parent : startBlock) this.removeBlock(parent)
} }
break break
} }
@ -300,7 +274,6 @@ const pasteCtrl = ContentState => {
if (liChildren[0].type === 'p') { if (liChildren[0].type === 'p') {
// TODO @JOCS // TODO @JOCS
startBlock.text += liChildren[0].children[0].text startBlock.text += liChildren[0].children[0].text
liChildren[0].children.slice(1).forEach(c => this.appendChild(parent, c))
const tail = liChildren.slice(1) const tail = liChildren.slice(1)
if (tail.length) { if (tail.length) {
tail.forEach(t => { tail.forEach(t => {
@ -323,33 +296,21 @@ const pasteCtrl = ContentState => {
this.insertAfter(block, target) this.insertAfter(block, target)
target = block target = block
}) })
} else { } else if (firstFragment.type === 'p' || /^h\d/.test(firstFragment.type)) {
if (firstFragment.type === 'p') { const text = firstFragment.children[0].text
if (/^h\d$/.test(startBlock.type)) { const lines = text.split('\n')
// handle paste into header let target = parent
startBlock.text += firstFragment.children[0].text if (parent.headingStyle === 'atx') {
if (firstFragment.children.length > 1) { startBlock.text += lines[0]
const newParagraph = this.createBlock('p') if (lines.length > 1) {
firstFragment.children.slice(1).forEach(line => { const pBlock = this.createBlockP(lines.slice(1).join('\n'))
this.appendChild(newParagraph, line) this.insertAfter(parent, pBlock)
}) target = pBlock
this.insertAfter(newParagraph, startBlock)
} }
} else { } else {
startBlock.text += firstFragment.children[0].text startBlock.text += 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 (/^h\d$/.test(firstFragment.type)) {
startBlock.text += firstFragment.text.split(/\s+/)[1]
} else {
startBlock.text += firstFragment.text
} }
let target = /^h\d$/.test(startBlock.type) ? startBlock : parent
tailFragments.forEach(block => { tailFragments.forEach(block => {
this.insertAfter(block, target) this.insertAfter(block, target)
target = block target = block
@ -358,14 +319,13 @@ const pasteCtrl = ContentState => {
break break
} }
case 'NEWLINE': { case 'NEWLINE': {
let target = startBlock.type === 'span' ? parent : startBlock let target = parent
stateFragments.forEach(block => { stateFragments.forEach(block => {
this.insertAfter(block, target) this.insertAfter(block, target)
target = block target = block
}) })
if (startBlock.text.length === 0) { if (startBlock.text.length === 0) {
this.removeBlock(startBlock) this.removeBlock(parent)
if (this.isOnlyChild(startBlock) && startBlock.type === 'span') this.removeBlock(parent)
} }
break break
} }
@ -380,10 +340,7 @@ const pasteCtrl = ContentState => {
offset = startBlock.text.length - cacheText.length offset = startBlock.text.length - cacheText.length
cursorBlock = startBlock cursorBlock = startBlock
} }
// TODO @Jocs duplicate with codes in updateCtrl.js
if (cursorBlock && cursorBlock.type === 'span' && cursorBlock.functionType === 'codeLine') {
this.updateCodeBlocks(cursorBlock)
}
this.cursor = { this.cursor = {
start: { start: {
key, offset key, offset

View File

@ -18,7 +18,9 @@ const tableBlockCtrl = ContentState => {
const rowBlock = this.createBlock('tr') const rowBlock = this.createBlock('tr')
i === 0 ? this.appendChild(tHead, rowBlock) : this.appendChild(tBody, rowBlock) i === 0 ? this.appendChild(tHead, rowBlock) : this.appendChild(tBody, rowBlock)
for (j = 0; j < columns; j++) { 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) this.appendChild(rowBlock, cell)
cell.align = '' cell.align = ''
cell.column = j cell.column = j
@ -41,24 +43,18 @@ const tableBlockCtrl = ContentState => {
ContentState.prototype.getAnchor = function (block) { ContentState.prototype.getAnchor = function (block) {
const { type, functionType } = block const { type, functionType } = block
switch (true) { switch (type) {
case /^span$/.test(type): { case 'span':
if (!functionType) { if (functionType === 'codeLine') {
return this.closest(block, 'p')
} else if (functionType === 'codeLine') {
return this.closest(block, 'figure') || this.closest(block, 'pre') return this.closest(block, 'figure') || this.closest(block, 'pre')
} else {
return this.getParent(block)
} }
return null
} case 'th':
case /^(th|td)$/.test(type): { case 'td':
return this.closest(block, 'figure') return this.closest(block, 'figure')
}
case /^h\d$/.test(type): {
return block
}
case /hr/.test(type): {
return block
}
default: default:
return null return null
} }
@ -74,7 +70,7 @@ const tableBlockCtrl = ContentState => {
if (!anchor) return if (!anchor) return
this.insertAfter(figureBlock, anchor) this.insertAfter(figureBlock, anchor)
if (anchor.type === 'p' && !endBlock.text) { if (/p|h\d/.test(anchor.type) && !endBlock.text) {
this.removeBlock(anchor) this.removeBlock(anchor)
} }
this.appendChild(figureBlock, table) this.appendChild(figureBlock, table)

View File

@ -3,13 +3,14 @@ import { conflict } from '../utils'
import { CLASS_OR_ID } from '../config' import { CLASS_OR_ID } from '../config'
const INLINE_UPDATE_FRAGMENTS = [ const INLINE_UPDATE_FRAGMENTS = [
'^([*+-]\\s)', // Bullet list '(?:^|\n) {0,3}([*+-] {1,4})', // Bullet list
'^(\\[[x\\s]{1}\\]\\s)', // Task list '(?:^|\n)(\\[[x ]{1}\\] {1,4})', // Task list
'^(\\d{1,9}(?:\\.|\\))\\s)', // Order list '(?:^|\n) {0,3}(\\d{1,9}(?:\\.|\\)) {1,4})', // Order list
'^\\s{0,3}(#{1,6})(?=\\s{1,}|$)', // ATX headings '(?:^|\n) {0,3}(#{1,6})(?=\\s{1,}|$)', // ATX headings
'^\\s{0,3}(\\={3,}|\\-{3,})(?=\\s{1,}|$)', // Setext headings '^(?:[\\s\\S]+?)\\n {0,3}(\\={3,}|\\-{3,})(?= {1,}|$)', // Setext headings **match from beginning**
'^(>).+', // Block quote '(?:^|\n) {0,3}(>).+', // Block quote
'^\\s{0,3}((?:\\*\\s*\\*\\s*\\*|-\\s*-\\s*-|_\\s*_\\s*_)[\\s\\*\\-\\_]*)$' // Thematic break '^( {4,})', // Indent code **match from beginning**
'(?:^|\n) {0,3}((?:\\* *\\* *\\*|- *- *-|_ *_ *_)[ \\*\\-\\_]*)$' // Thematic break
] ]
const INLINE_UPDATE_REG = new RegExp(INLINE_UPDATE_FRAGMENTS.join('|'), 'i') const INLINE_UPDATE_REG = new RegExp(INLINE_UPDATE_FRAGMENTS.join('|'), 'i')
@ -21,11 +22,6 @@ const updateCtrl = ContentState => {
const block = this.getBlock(id) const block = this.getBlock(id)
block.checked = checked block.checked = checked
checkbox.classList.toggle(CLASS_OR_ID['AG_CHECKBOX_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) { ContentState.prototype.checkSameMarkerOrDelimiter = function (list, markerOrDelimiter) {
@ -40,9 +36,10 @@ const updateCtrl = ContentState => {
const endBlock = this.getBlock(cEnd ? cEnd.key : focus.key) const endBlock = this.getBlock(cEnd ? cEnd.key : focus.key)
const startOffset = cStart ? cStart.offset : anchor.offset const startOffset = cStart ? cStart.offset : anchor.offset
const endOffset = cEnd ? cEnd.offset : focus.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)) { 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 { start, end } = token.range
const textLen = startBlock.text.length const textLen = startBlock.text.length
if ( if (
@ -52,7 +49,7 @@ const updateCtrl = ContentState => {
} }
} }
for (const token of tokenizer(endBlock.text, undefined, undefined, labels)) { 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 { start, end } = token.range
const textLen = endBlock.text.length const textLen = endBlock.text.length
if ( if (
@ -65,34 +62,35 @@ const updateCtrl = ContentState => {
return false return false
} }
/**
* block must be span block.
*/
ContentState.prototype.checkInlineUpdate = function (block) { ContentState.prototype.checkInlineUpdate = function (block) {
// table cell can not have blocks in it // table cell can not have blocks in it
if (/th|td|figure/.test(block.type)) return false if (/th|td|figure/.test(block.type)) return false
if (/codeLine|languageInput/.test(block.functionType)) 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 let line = null
const { text } = block const { text } = block
if (block.type === 'span') { if (block.type === 'span') {
line = block line = block
block = this.getParent(block) block = this.getParent(block)
} }
const parent = this.getParent(block) const listItem = this.getParent(block)
const [match, bullet, tasklist, order, atxHeader, setextHeader, blockquote, hr] = text.match(INLINE_UPDATE_REG) || [] const [
match, bullet, tasklist, order, atxHeader,
setextHeader, blockquote, indentCode, hr
] = text.match(INLINE_UPDATE_REG) || []
switch (true) { switch (true) {
case ( case (!!hr && new Set(hr.split('').filter(i => /\S/.test(i))).size === 1):
(!!hr && new Set(hr.split('').filter(i => /\S/.test(i))).size === 1) || return this.updateHr(block, hr, line)
(!!setextHeader && !hasPreLine)
):
return this.updateHr(block, hr || setextHeader)
case !!bullet: case !!bullet:
return this.updateList(block, 'bullet', bullet, line) return this.updateList(block, 'bullet', bullet, line)
// only `bullet` list item can be update to `task` list item // 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) return this.updateTaskListItem(block, 'tasklist', tasklist)
case !!order: case !!order:
@ -101,75 +99,112 @@ const updateCtrl = ContentState => {
case !!atxHeader: case !!atxHeader:
return this.updateAtxHeader(block, atxHeader, line) return this.updateAtxHeader(block, atxHeader, line)
case !!setextHeader && hasPreLine: case !!setextHeader:
return this.updateSetextHeader(block, setextHeader, line) return this.updateSetextHeader(block, setextHeader, line)
case !!blockquote: case !!blockquote:
return this.updateBlockQuote(block, line) return this.updateBlockQuote(block, line)
case !!indentCode:
return this.updateIndentCode(block, line)
case !match: case !match:
default: default:
return this.updateToParagraph(block) return this.updateToParagraph(block, line)
} }
} }
// thematic break // Thematic break
ContentState.prototype.updateHr = function (block, marker) { ContentState.prototype.updateHr = function (block, marker, line) {
if (block.type !== 'hr') { // If the block is already thematic break, no need to update.
block.type = 'hr' if (block.type === 'hr') return null
block.text = marker const text = line.text
block.children.length = 0 const lines = text.split('\n')
const { key } = block const preParagraphLines = []
this.cursor.start.key = this.cursor.end.key = key let thematicLine = ''
return block 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) { ContentState.prototype.updateList = function (block, type, marker = '', line) {
if (block.type === 'span') {
block = this.getParent(block)
}
const cleanMarker = marker ? marker.trim() : null const cleanMarker = marker ? marker.trim() : null
const { preferLooseListItem } = this const { preferLooseListItem } = this
const parent = this.getParent(block)
const wrapperTag = type === 'order' ? 'ol' : 'ul' // `bullet` => `ul` and `order` => `ol` const wrapperTag = type === 'order' ? 'ol' : 'ul' // `bullet` => `ul` and `order` => `ol`
const { start, end } = this.cursor const { start, end } = this.cursor
const startOffset = start.offset const startOffset = start.offset
const endOffset = end.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)) { const preParagraphLines = []
delete block.marker const listItemLines = []
delete block.headingStyle let isPushedListItemLine = false
block.type = 'p' for (const l of lines) {
block.children = [] if (LIST_ITEM_REG.test(l) && !isPushedListItemLine) {
const line = this.createBlock('span', block.text.substring(marker.length)) listItemLines.push(l.replace(LIST_ITEM_REG, ''))
block.text = '' isPushedListItemLine = true
this.appendChild(block, line) } else if (!isPushedListItemLine) {
preParagraphLines.push(l)
} else { } else {
line.text = line.text.substring(marker.length) listItemLines.push(l)
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 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 preSibling = this.getPreSibling(block)
const nextSibling = this.getNextSibling(block) const nextSibling = this.getNextSibling(block)
newBlock.listItemType = type newListItemBlock.listItemType = type
newBlock.isLooseListItem = preferLooseListItem newListItemBlock.isLooseListItem = preferLooseListItem
let bulletMarkerOrDelimiter let bulletMarkerOrDelimiter
if (type === 'order') { if (type === 'order') {
@ -178,7 +213,7 @@ const updateCtrl = ContentState => {
const { bulletListMarker } = this const { bulletListMarker } = this
bulletMarkerOrDelimiter = marker ? marker.charAt(0) : bulletListMarker 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. // Special cases for CommonMark 264 and 265: Changing the bullet or ordered list delimiter starts a new list.
// Same list type or new list // Same list type or new list
@ -188,7 +223,7 @@ const updateCtrl = ContentState => {
nextSibling && nextSibling &&
this.checkSameMarkerOrDelimiter(nextSibling, bulletMarkerOrDelimiter) this.checkSameMarkerOrDelimiter(nextSibling, bulletMarkerOrDelimiter)
) { ) {
this.appendChild(preSibling, newBlock) this.appendChild(preSibling, newListItemBlock)
const partChildren = nextSibling.children.splice(0) const partChildren = nextSibling.children.splice(0)
partChildren.forEach(b => this.appendChild(preSibling, b)) partChildren.forEach(b => this.appendChild(preSibling, b))
this.removeBlock(nextSibling) this.removeBlock(nextSibling)
@ -199,7 +234,7 @@ const updateCtrl = ContentState => {
preSibling && preSibling &&
this.checkSameMarkerOrDelimiter(preSibling, bulletMarkerOrDelimiter) this.checkSameMarkerOrDelimiter(preSibling, bulletMarkerOrDelimiter)
) { ) {
this.appendChild(preSibling, newBlock) this.appendChild(preSibling, newListItemBlock)
this.removeBlock(block) this.removeBlock(block)
const isLooseListItem = preSibling.children.some(c => c.isLooseListItem) const isLooseListItem = preSibling.children.some(c => c.isLooseListItem)
preSibling.children.forEach(c => c.isLooseListItem = isLooseListItem) preSibling.children.forEach(c => c.isLooseListItem = isLooseListItem)
@ -207,67 +242,59 @@ const updateCtrl = ContentState => {
nextSibling && nextSibling &&
this.checkSameMarkerOrDelimiter(nextSibling, bulletMarkerOrDelimiter) this.checkSameMarkerOrDelimiter(nextSibling, bulletMarkerOrDelimiter)
) { ) {
this.insertBefore(newBlock, nextSibling.children[0]) this.insertBefore(newListItemBlock, nextSibling.children[0])
this.removeBlock(block) this.removeBlock(block)
const isLooseListItem = nextSibling.children.some(c => c.isLooseListItem) const isLooseListItem = nextSibling.children.some(c => c.isLooseListItem)
nextSibling.children.forEach(c => c.isLooseListItem = 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 { } else {
// Create a new list when changing list type, bullet or list delimiter // Create a new list when changing list type, bullet or list delimiter
const listBlock = this.createBlock(wrapperTag) const listBlock = this.createBlock(wrapperTag, {
listBlock.listType = type listType: type
})
if (wrapperTag === 'ol') { if (wrapperTag === 'ol') {
const start = cleanMarker ? cleanMarker.slice(0, -1) : 1 const start = cleanMarker ? cleanMarker.slice(0, -1) : 1
listBlock.start = /^\d+$/.test(start) ? start : 1 listBlock.start = /^\d+$/.test(start) ? start : 1
} }
this.appendChild(listBlock, newBlock) this.appendChild(listBlock, newListItemBlock)
this.insertBefore(listBlock, block) this.insertBefore(listBlock, block)
this.removeBlock(block) this.removeBlock(block)
} }
// key point // key point
this.appendChild(newBlock, block) this.appendChild(newListItemBlock, block)
const TASK_LIST_REG = /^\[[x ]\] /i const TASK_LIST_REG = /^\[[x ]\] {1,4}/i
const listItemText = block.children[0].text 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)) { if (TASK_LIST_REG.test(listItemText)) {
const [,,tasklist,,,,] = listItemText.match(INLINE_UPDATE_REG) || [] const [,,tasklist,,,,] = listItemText.match(INLINE_UPDATE_REG) || []
return this.updateTaskListItem(block, 'tasklist', tasklist) return this.updateTaskListItem(block, 'tasklist', tasklist)
} else { } 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 return block
} }
} }
ContentState.prototype.updateTaskListItem = function (block, type, marker = '') { ContentState.prototype.updateTaskListItem = function (block, type, marker = '') {
if (block.type === 'span') {
block = this.getParent(block)
}
const { preferLooseListItem } = this const { preferLooseListItem } = this
const parent = this.getParent(block) const parent = this.getParent(block)
const grandpa = this.getParent(parent) const grandpa = this.getParent(parent)
const checked = /\[x\]\s/i.test(marker) // use `i` flag to ignore upper case or lower case 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 const { start, end } = this.cursor
checkbox.checked = checked
this.insertBefore(checkbox, block) this.insertBefore(checkbox, block)
block.children[0].text = block.children[0].text.substring(marker.length) block.children[0].text = block.children[0].text.substring(marker.length)
parent.listItemType = 'task' parent.listItemType = 'task'
@ -277,16 +304,21 @@ const updateCtrl = ContentState => {
if (this.isOnlyChild(parent)) { if (this.isOnlyChild(parent)) {
grandpa.listType = 'task' grandpa.listType = 'task'
} else if (this.isFirstChild(parent) || this.isLastChild(parent)) { } else if (this.isFirstChild(parent) || this.isLastChild(parent)) {
taskListWrapper = this.createBlock('ul') taskListWrapper = this.createBlock('ul', {
taskListWrapper.listType = 'task' listType: 'task'
})
this.isFirstChild(parent) ? this.insertBefore(taskListWrapper, grandpa) : this.insertAfter(taskListWrapper, grandpa) this.isFirstChild(parent) ? this.insertBefore(taskListWrapper, grandpa) : this.insertAfter(taskListWrapper, grandpa)
this.removeBlock(parent) this.removeBlock(parent)
this.appendChild(taskListWrapper, parent) this.appendChild(taskListWrapper, parent)
} else { } else {
taskListWrapper = this.createBlock('ul') taskListWrapper = this.createBlock('ul', {
taskListWrapper.listType = 'task' listType: 'task'
const bulletListWrapper = this.createBlock('ul') })
bulletListWrapper.listType = 'bullet'
const bulletListWrapper = this.createBlock('ul', {
listType: 'bullet'
})
let preSibling = this.getPreSibling(parent) let preSibling = this.getPreSibling(parent)
while (preSibling) { while (preSibling) {
@ -319,145 +351,246 @@ const updateCtrl = ContentState => {
return taskListWrapper || grandpa return taskListWrapper || grandpa
} }
// ATX heading doesn't support soft line break and hard line break.
ContentState.prototype.updateAtxHeader = function (block, header, line) { ContentState.prototype.updateAtxHeader = function (block, header, line) {
const newType = `h${header.length}` const newType = `h${header.length}`
const text = line ? line.text : block.text const headingStyle = 'atx'
if (line) { if (block.type === newType && block.headingStyle === headingStyle) {
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 return null
} }
block.headingStyle = 'atx' const text = line.text
block.type = newType const lines = text.split('\n')
block.text = text const preParagraphLines = []
block.children.length = 0 let atxLine = ''
this.cursor.start.key = this.cursor.end.key = block.key const postParagraphLines = []
return block 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) { ContentState.prototype.updateSetextHeader = function (block, marker, line) {
const newType = /=/.test(marker) ? 'h1' : 'h2' const newType = /=/.test(marker) ? 'h1' : 'h2'
const header = this.createBlock(newType) const headingStyle = 'setext'
header.headingStyle = 'setext' if (block.type === newType && block.headingStyle === headingStyle) {
header.marker = marker return null
const index = block.children.indexOf(line)
let i = 0
let text = ''
for (i; i < index; i++) {
text += `${block.children[i].text}\n`
} }
header.text = text.trimRight()
this.insertBefore(header, block) const text = line.text
if (line.nextSibling) { const lines = text.split('\n')
const removedCache = [] let setextLines = []
for (const child of block.children) { const postParagraphLines = []
removedCache.push(child) let setextLineHasPushed = false
if (child === line) {
break for (const l of lines) {
} if (/^ {0,3}(?:={3,}|-{3,})(?= {1,}|$)/.test(l) && !setextLineHasPushed) {
} setextLineHasPushed = true
removedCache.forEach(child => this.removeBlock(child)) } else if (!setextLineHasPushed) {
setextLines.push(l)
} else { } else {
this.removeBlock(block) postParagraphLines.push(l)
} }
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) { ContentState.prototype.updateBlockQuote = function (block, line) {
if (line && !this.isFirstChild(line)) { const text = line.text
const paragraphBefore = this.createBlock('p') const lines = text.split('\n')
const removeCache = [] const preParagraphLines = []
for (const child of block.children) { let quoteLines = []
if (child === line) break let quoteLinesHasPushed = false
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)
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 const { start, end } = this.cursor
this.cursor = { this.cursor = {
start: { start: { key, offset: start.offset - 1 },
key: start.key, end: { key, offset: end.offset - 1 }
offset: start.offset - 1
},
end: {
key: end.key,
offset: end.offset - 1
}
} }
return quoteBlock 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') { if (/^h\d$/.test(block.type) && block.headingStyle === 'setext') {
return null return null
} }
const newType = 'p' const newType = 'p'
if (block.type !== newType) { if (block.type !== newType) {
block.type = newType // updateP const newBlock = this.createBlockP(line.text)
const newLine = this.createBlock('span', block.text) this.insertBefore(newBlock, block)
this.appendChild(block, newLine) this.removeBlock(block)
block.text = '' const { start, end } = this.cursor
this.cursor.start.key = this.cursor.end.key = newLine.key const key = newBlock.children[0].key
this.cursor = {
start: { key, offset: start.offset },
end: { key, offset: end.offset }
}
return block return block
} }
return null 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 export default updateCtrl

View File

@ -73,15 +73,12 @@ class ClickEvent {
if (target.closest('div.ag-container-preview') || target.closest('div.ag-html-preview')) { if (target.closest('div.ag-container-preview') || target.closest('div.ag-html-preview')) {
return event.stopPropagation() return event.stopPropagation()
} }
// handler html preview click // handler container preview click
const editIcon = target.closest(`.ag-container-icon`) const editIcon = target.closest(`.ag-container-icon`)
if (editIcon) { if (editIcon) {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
const nextElement = editIcon.nextElementSibling if (editIcon.parentNode.classList.contains('ag-container-block')) {
if (nextElement && nextElement.classList.contains('ag-function-html')) {
contentState.handleHtmlBlockClick(nextElement)
} else if (editIcon.parentNode.classList.contains('ag-container-block')) {
contentState.handleContainerBlockClick(editIcon.parentNode) contentState.handleContainerBlockClick(editIcon.parentNode)
} }
} }

View File

@ -66,6 +66,9 @@ class Keyboard {
if (event.target.closest('[contenteditable=false]')) { if (event.target.closest('[contenteditable=false]')) {
return 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() const { start, end } = selection.getCursorRange()
if (!start || !end) { if (!start || !end) {
return return

View File

@ -378,15 +378,37 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
pos = pos + autoLTo[0].length pos = pos + autoLTo[0].length
continue 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 // hard line break
const hardTo = inlineRules['hard_line_break'].exec(src) const hardTo = inlineRules['hard_line_break'].exec(src)
if (hardTo && top) { if (hardTo) {
const len = hardTo[0].length const len = hardTo[0].length
pushPending() pushPending()
tokens.push({ tokens.push({
type: 'hard_line_break', type: 'hard_line_break',
raw: hardTo[0], raw: hardTo[0],
spaces: hardTo[1], spaces: hardTo[1],
lineBreak: hardTo[2],
isAtEnd: hardTo.input.length === hardTo[0].length,
parent: tokens, parent: tokens,
range: { range: {
start: pos, start: pos,

View File

@ -185,9 +185,11 @@ Lexer.prototype.token = function (src, top) {
// hr // hr
cap = this.rules.hr.exec(src) cap = this.rules.hr.exec(src)
if (cap) { if (cap) {
const marker = cap[0].replace(/\n*$/, '')
src = src.substring(cap[0].length) src = src.substring(cap[0].length)
this.tokens.push({ this.tokens.push({
type: 'hr' type: 'hr',
marker
}) })
continue continue
} }

View File

@ -3,7 +3,7 @@ import flowchart from 'flowchart.js'
import Diagram from './sequence' import Diagram from './sequence'
import vegaEmbed from 'vega-embed' import vegaEmbed from 'vega-embed'
import { CLASS_OR_ID } from '../../config' 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 { patch, toVNode, toHTML, h } from './snabbdom'
import { beginRules } from '../rules' import { beginRules } from '../rules'
import renderInlines from './renderInlines' import renderInlines from './renderInlines'
@ -13,6 +13,7 @@ class StateRender {
constructor (muya) { constructor (muya) {
this.muya = muya this.muya = muya
this.eventCenter = muya.eventCenter this.eventCenter = muya.eventCenter
this.codeCache = new Map()
this.loadImageMap = new Map() this.loadImageMap = new Map()
this.loadMathMap = new Map() this.loadMathMap = new Map()
this.mermaidCache = new Set() this.mermaidCache = new Set()
@ -85,7 +86,7 @@ class StateRender {
selector += `.${CLASS_OR_ID['AG_ACTIVE']}` selector += `.${CLASS_OR_ID['AG_ACTIVE']}`
} }
if (type === 'span') { if (type === 'span') {
selector += `.${CLASS_OR_ID['AG_LINE']}` selector += `.ag-${camelToSnake(block.functionType)}`
} }
if (!block.parent && selectedBlock && block.key === selectedBlock.key) { if (!block.parent && selectedBlock && block.key === selectedBlock.key) {
selector += `.${CLASS_OR_ID['AG_SELECTED']}` selector += `.${CLASS_OR_ID['AG_SELECTED']}`
@ -155,6 +156,7 @@ class StateRender {
patch(oldVdom, newVdom) patch(oldVdom, newVdom)
this.renderMermaid() this.renderMermaid()
this.renderDiagram() this.renderDiagram()
this.codeCache.clear()
} }
// Only render the blocks which you updated // Only render the blocks which you updated
@ -198,6 +200,7 @@ class StateRender {
this.renderMermaid() this.renderMermaid()
this.renderDiagram() this.renderDiagram()
this.codeCache.clear()
} }
} }

View File

@ -2,7 +2,7 @@
* [renderBlock render one block, no matter it is a container block or text block] * [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) { 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' ? 'renderContainerBlock'
: 'renderLeafBlock' : 'renderLeafBlock'

View File

@ -17,28 +17,47 @@ const PRE_BLOCK_HASH = {
export default function renderContainerBlock (block, cursor, activeBlocks, selectedBlock, matches, useCache = false) { export default function renderContainerBlock (block, cursor, activeBlocks, selectedBlock, matches, useCache = false) {
let selector = this.getSelector(block, cursor, activeBlocks, selectedBlock) 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 children = block.children.map(child => this.renderBlock(child, cursor, activeBlocks, selectedBlock, matches, useCache))
const data = { const data = {
attrs: {}, attrs: {},
dataset: {} dataset: {}
} }
// handle `div` block
if (/div/.test(block.type)) { if (editable === false) {
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' }) Object.assign(data.attrs, { contenteditable: 'false' })
} }
if (/code|pre/.test(type) && typeof lang === 'string' && !!lang) {
selector += `.language-${lang}`
} }
// handle `figure` block
if (block.type === 'figure') { if (/^h/.test(type)) {
if (block.functionType) { if (/^h\d$/.test(type)) {
Object.assign(data.dataset, { role: block.functionType.toUpperCase() }) // TODO: This should be the best place to create and update the TOC.
if (block.functionType === 'table') { // 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)) children.unshift(renderTableTools(activeBlocks))
} else { } else {
children.unshift(renderEditIcon()) children.unshift(renderEditIcon())
@ -46,61 +65,29 @@ export default function renderContainerBlock (block, cursor, activeBlocks, selec
} }
if ( 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']}` selector += `.${CLASS_OR_ID['AG_CONTAINER_BLOCK']}`
} }
} } else if (/ul|ol/.test(type) && listType) {
// hanle list block selector += `.ag-${listType}-list`
if (/ul|ol/.test(block.type) && block.listType) { if (type === 'ol') {
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
}
}
if (block.type === 'li' && block.listItemType) {
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 }) Object.assign(data.attrs, { start: block.start })
} }
if (block.type === 'code') { } else if (type === 'li' && listItemType) {
const { lang } = block Object.assign(data.dataset, { marker: bulletMarkerOrDelimiter })
if (lang) { selector += `.${CLASS_OR_ID['AG_LIST_ITEM']}`
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') {
if (block.type === 'pre') {
const { lang, functionType } = block
if (lang) {
selector += `.language-${lang}`
}
Object.assign(data.dataset, { role: functionType }) Object.assign(data.dataset, { role: functionType })
selector += PRE_BLOCK_HASH[block.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) { if (!block.parent) {

View File

@ -57,7 +57,6 @@ export default function renderLeafBlock (block, cursor, activeBlocks, selectedBl
const { const {
text, text,
type, type,
headingStyle,
align, align,
checked, checked,
key, key,
@ -65,13 +64,16 @@ export default function renderLeafBlock (block, cursor, activeBlocks, selectedBl
functionType, functionType,
editable editable
} = block } = block
const data = { const data = {
props: {}, props: {},
attrs: {}, attrs: {},
dataset: {}, dataset: {},
style: {} style: {}
} }
let children = '' let children = ''
if (text) { if (text) {
let tokens = [] let tokens = []
if (highlights.length === 0 && this.tokenCache.has(text)) { if (highlights.length === 0 && this.tokenCache.has(text)) {
@ -81,7 +83,7 @@ export default function renderLeafBlock (block, cursor, activeBlocks, selectedBl
functionType !== 'codeLine' && functionType !== 'codeLine' &&
functionType !== 'languageInput' functionType !== 'languageInput'
) { ) {
const hasBeginRules = /^(h\d|span|hr)/.test(type) const hasBeginRules = type === 'span'
tokens = tokenizer(text, highlights, hasBeginRules, this.labels) tokens = tokenizer(text, highlights, hasBeginRules, this.labels)
const hasReferenceTokens = hasReferenceToken(tokens) const hasReferenceTokens = hasReferenceToken(tokens)
if (highlights.length === 0 && useCache && DEVICE_MEMORY >= 4 && !hasReferenceTokens) { 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}` style: `text-align:${align}`
}) })
} else if (type === 'div') { } else if (type === 'div') {
const code = this.muya.contentState.codeBlocks.get(block.preSibling) const code = this.codeCache.get(block.preSibling)
switch (functionType) { switch (functionType) {
case 'preview': { case 'html': {
selector += `.${CLASS_OR_ID['AG_HTML_PREVIEW']}` selector += `.${CLASS_OR_ID['AG_HTML_PREVIEW']}`
const htmlContent = sanitize(code, PREVIEW_DOMPURIFY_CONFIG) const htmlContent = sanitize(code, PREVIEW_DOMPURIFY_CONFIG)
// handle empty html bock // handle empty html bock
@ -168,7 +170,6 @@ export default function renderLeafBlock (block, cursor, activeBlocks, selectedBl
case 'flowchart': case 'flowchart':
case 'sequence': case 'sequence':
case 'vega-lite': { case 'vega-lite': {
const code = this.muya.contentState.codeBlocks.get(block.preSibling)
selector += `.${CLASS_OR_ID['AG_CONTAINER_PREVIEW']}` selector += `.${CLASS_OR_ID['AG_CONTAINER_PREVIEW']}`
if (code === '') { if (code === '') {
children = '< Empty Diagram Block >' children = '< Empty Diagram Block >'
@ -183,18 +184,6 @@ export default function renderLeafBlock (block, cursor, activeBlocks, selectedBl
break 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') { } else if (type === 'input') {
Object.assign(data.attrs, { Object.assign(data.attrs, {
type: 'checkbox' 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'), '"')
.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)) { if (lang && /\S/.test(code) && loadedCache.has(lang)) {
const wrapper = document.createElement('div') const wrapper = document.createElement('div')
wrapper.classList.add(`language-${lang}`) wrapper.classList.add(`language-${lang}`)
@ -230,7 +217,6 @@ export default function renderLeafBlock (block, cursor, activeBlocks, selectedBl
} }
} else if (type === 'span' && functionType === 'languageInput') { } else if (type === 'span' && functionType === 'languageInput') {
const html = getHighlightHtml(text, highlights) const html = getHighlightHtml(text, highlights)
selector += `.${CLASS_OR_ID['AG_LANGUAGE_INPUT']}`
children = htmlToVNode(html) children = htmlToVNode(html)
} }
if (!block.parent) { if (!block.parent) {

View File

@ -1,13 +1,17 @@
import { CLASS_OR_ID } from '../../../config' 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 className = CLASS_OR_ID['AG_HARD_LINE_BREAK']
const content = [token.spaces] const spaceClass = CLASS_OR_ID['AG_HARD_LINE_BREAK_SPACE']
if (block.type === 'span' && block.nextSibling) { if (isAtEnd) {
return [ return [
h(`span.${className}`, content) h(`span.${className}`, h(`span.${spaceClass}`, spaces)),
h(`span.${CLASS_OR_ID['AG_LINE_END']}`, lineBreak)
] ]
} else { } else {
return content return [
h(`span.${className}`, [ h(`span.${spaceClass}`, spaces), lineBreak ])
]
} }
} }

View File

@ -7,6 +7,7 @@ import htmlTag from './htmlTag'
import hr from './hr' import hr from './hr'
import tailHeader from './tailHeader' import tailHeader from './tailHeader'
import hardLineBreak from './hardLineBreak' import hardLineBreak from './hardLineBreak'
import softLineBreak from './softLineBreak'
import codeFense from './codeFense' import codeFense from './codeFense'
import inlineMath from './inlineMath' import inlineMath from './inlineMath'
import autoLink from './autoLink' import autoLink from './autoLink'
@ -37,6 +38,7 @@ export default {
hr, hr,
tailHeader, tailHeader,
hardLineBreak, hardLineBreak,
softLineBreak,
codeFense, codeFense,
inlineMath, inlineMath,
autoLink, autoLink,

View 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)
]
}

View File

@ -25,7 +25,8 @@ export const inlineRules = {
'tail_header': /^(\s{1,}#{1,})(\s*)$/, '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_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'), '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 `$` // patched math marker `$`
'backlash': /^(\\)([\\`*{}\[\]()#+\-.!_>~:\|\<\>$]{1})/, 'backlash': /^(\\)([\\`*{}\[\]()#+\-.!_>~:\|\<\>$]{1})/,

View File

@ -1,11 +1,11 @@
import Prism from 'prismjs' import Prism from 'prismjs2'
import { filter } from 'fuzzaldrin' import { filter } from 'fuzzaldrin'
import initLoadLanguage, { loadedCache } from './loadLanguage' import initLoadLanguage, { loadedCache } from './loadLanguage'
import languages from './languages' import languages from './languages'
const prism = Prism const prism = Prism
window.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 langs = Object.keys(languages).map(name => (languages[name]))
const loadLanguage = initLoadLanguage(Prism) const loadLanguage = initLoadLanguage(Prism)

View File

@ -74,7 +74,7 @@ function initLoadLanguage (Prism) {
} }
delete Prism.languages[language] delete Prism.languages[language]
await import('prismjs/components/prism-' + language) await import('prismjs2/components/prism-' + language)
loadedCache.add(language) loadedCache.add(language)
promises.push(Promise.resolve({ promises.push(Promise.resolve({
status: 'loaded', status: 'loaded',

View File

@ -4,7 +4,12 @@ import {
const CHOP_TEXT_REG = /(\*{1,3})([^*]+)(\1)/g const CHOP_TEXT_REG = /(\*{1,3})([^*]+)(\1)/g
export const getTextContent = (node, blackList) => { 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 = '' let text = ''
if (blackList.some(className => node.classList && node.classList.contains(className))) { if (blackList.some(className => node.classList && node.classList.contains(className))) {
return text return text

View File

@ -436,6 +436,7 @@ class Selection {
let { node: anchorNode, offset: anchorOffset } = getNodeAndOffset(anchorParagraph, anchor.offset) let { node: anchorNode, offset: anchorOffset } = getNodeAndOffset(anchorParagraph, anchor.offset)
let { node: focusNode, offset: focusOffset } = getNodeAndOffset(focusParagraph, focus.offset) let { node: focusNode, offset: focusOffset } = getNodeAndOffset(focusParagraph, focus.offset)
anchorOffset = Math.min(anchorOffset, anchorNode.textContent.length) anchorOffset = Math.min(anchorOffset, anchorNode.textContent.length)
focusOffset = Math.min(focusOffset, focusNode.textContent.length) focusOffset = Math.min(focusOffset, focusNode.textContent.length)
// First set the anchor node and anchor offeet, make it collapsed // First set the anchor node and anchor offeet, make it collapsed
@ -449,15 +450,14 @@ class Selection {
if (node.nodeType === 3) { if (node.nodeType === 3) {
node = node.parentNode node = node.parentNode
} }
return node.closest('p[data-role=hr]') || return node.closest('span.ag-paragraph') ||
node.closest('span.ag-paragraph.ag-line') ||
node.closest('th.ag-paragraph') || node.closest('th.ag-paragraph') ||
node.closest('td.ag-paragraph') || node.closest('td.ag-paragraph')
node.closest('.ag-paragraph[data-head]')
} }
getCursorRange () { getCursorRange () {
let { anchorNode, anchorOffset, focusNode, focusOffset } = this.doc.getSelection() let { anchorNode, anchorOffset, focusNode, focusOffset } = this.doc.getSelection()
const isAnchorValid = this.isValidCursorNode(anchorNode) const isAnchorValid = this.isValidCursorNode(anchorNode)
const isFocusValid = this.isValidCursorNode(focusNode) const isFocusValid = this.isValidCursorNode(focusNode)
let needFix = false 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 anchorParagraph = findNearestParagraph(anchorNode)
const focusParagraph = findNearestParagraph(focusNode) const focusParagraph = findNearestParagraph(focusNode)
@ -502,6 +512,7 @@ class Selection {
const aOffset = getOffsetOfParagraph(anchorNode, anchorParagraph) + anchorOffset const aOffset = getOffsetOfParagraph(anchorNode, anchorParagraph) + anchorOffset
const fOffset = getOffsetOfParagraph(focusNode, focusParagraph) + focusOffset const fOffset = getOffsetOfParagraph(focusNode, focusParagraph) + focusOffset
const anchor = { key: anchorParagraph.id, offset: aOffset } const anchor = { key: anchorParagraph.id, offset: aOffset }
const focus = { key: focusParagraph.id, offset: fOffset } const focus = { key: focusParagraph.id, offset: fOffset }
const result = new Cursor({ anchor, focus }) const result = new Cursor({ anchor, focus })

View File

@ -1,12 +1,12 @@
import marked from '../parser/marked' import marked from '../parser/marked'
import Prism from 'prismjs' import Prism from 'prismjs2'
import katex from 'katex' import katex from 'katex'
import mermaid from 'mermaid' import mermaid from 'mermaid'
import flowchart from 'flowchart.js' import flowchart from 'flowchart.js'
import Diagram from '../parser/render/sequence' import Diagram from '../parser/render/sequence'
import vegaEmbed from 'vega-embed' import vegaEmbed from 'vega-embed'
import githubMarkdownCss from 'github-markdown-css/github-markdown.css' 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 katexCss from 'katex/dist/katex.css'
import { EXPORT_DOMPURIFY_CONFIG } from '../config' import { EXPORT_DOMPURIFY_CONFIG } from '../config'
import { sanitize, unescapeHtml } from '../utils' import { sanitize, unescapeHtml } from '../utils'

View File

@ -42,7 +42,8 @@ class ExportMarkdown {
} }
switch (block.type) { switch (block.type) {
case 'p': { case 'p':
case 'hr': {
this.insertLineBreak(result, indent) this.insertLineBreak(result, indent)
result.push(this.translateBlocks2Markdown(block.children, indent)) result.push(this.translateBlocks2Markdown(block.children, indent))
break break
@ -51,11 +52,6 @@ class ExportMarkdown {
result.push(this.normalizeParagraphText(block, indent)) result.push(this.normalizeParagraphText(block, indent))
break break
} }
case 'hr': {
this.insertLineBreak(result, indent)
result.push(this.normalizeParagraphText(block, indent))
break
}
case 'h1': case 'h1':
case 'h2': case 'h2':
case 'h3': case 'h3':
@ -171,17 +167,21 @@ class ExportMarkdown {
} }
normalizeParagraphText (block, indent) { 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) { normalizeHeaderText (block, indent) {
const { headingStyle, marker } = block const { headingStyle, marker } = block
const { text } = block.children[0]
if (headingStyle === 'atx') { if (headingStyle === 'atx') {
const match = block.text.match(/(#{1,6})(.*)/) const match = text.match(/(#{1,6})(.*)/)
const text = `${match[1]} ${match[2].trim()}` const atxHeadingText = `${match[1]} ${match[2].trim()}`
return `${indent}${text}\n` return `${indent}${atxHeadingText}\n`
} else if (headingStyle === 'setext') { } 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 normalizeHTML (block, indent) { // figure
const result = [] 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) { for (const line of codeLines) {
result.push(`${indent}${line.text}\n`) result.push(`${indent}${line.text}\n`)
} }

View File

@ -13,6 +13,56 @@ import { CURSOR_DNA } from '../config'
const LINE_BREAKS_REG = /\n/ 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 => { const importRegister = ContentState => {
// turn markdown to blocks // turn markdown to blocks
ContentState.prototype.markdownToState = function (markdown) { ContentState.prototype.markdownToState = function (markdown) {
@ -37,37 +87,57 @@ const importRegister = ContentState => {
while ((token = tokens.shift())) { while ((token = tokens.shift())) {
switch (token.type) { switch (token.type) {
case 'frontmatter': { case 'frontmatter': {
const lang = 'yaml'
value = token.text value = token.text
block = this.createBlock('pre') block = this.createBlock('pre', {
const codeBlock = this.createBlock('code') functionType: token.type,
lang
})
const codeBlock = this.createBlock('code', {
lang
})
value value
.replace(/^\s+/, '') .replace(/^\s+/, '')
.replace(/\s$/, '') .replace(/\s$/, '')
.split(LINE_BREAKS_REG).forEach(line => { .split(LINE_BREAKS_REG).forEach(line => {
const codeLine = this.createBlock('span', line) const codeLine = this.createBlock('span', {
codeLine.functionType = 'codeLine' text: line,
codeLine.lang = 'yaml' lang,
functionType: 'codeLine'
})
this.appendChild(codeBlock, 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(block, codeBlock)
this.appendChild(parentList[0], block) this.appendChild(parentList[0], block)
break break
} }
case 'hr': { case 'hr': {
value = '---' value = token.marker
block = this.createBlock('hr', value) block = this.createBlock('hr')
const thematicBreakContent = this.createBlock('span', {
text: value,
functionType: 'thematicBreakLine'
})
this.appendChild(block, thematicBreakContent)
this.appendChild(parentList[0], block) this.appendChild(parentList[0], block)
break break
} }
case 'heading': { case 'heading': {
const { headingStyle, depth, text, marker } = token const { headingStyle, depth, text, marker } = token
value = headingStyle === 'atx' ? '#'.repeat(+depth) + ` ${text}` : text value = headingStyle === 'atx' ? '#'.repeat(+depth) + ` ${text}` : text
block = this.createBlock(`h${depth}`, value) block = this.createBlock(`h${depth}`, {
block.headingStyle = headingStyle headingStyle
})
const headingContent = this.createBlock('span', {
text: value,
functionType: headingStyle === 'atx'? 'atxLine' : 'paragraphContent'
})
this.appendChild(block, headingContent)
if (marker) { if (marker) {
block.marker = marker block.marker = marker
} }
@ -95,15 +165,25 @@ const importRegister = ContentState => {
block = this.createContainerBlock(lang, value) block = this.createContainerBlock(lang, value)
this.appendChild(parentList[0], block) this.appendChild(parentList[0], block)
} else { } else {
block = this.createBlock('pre') block = this.createBlock('pre', {
const codeBlock = this.createBlock('code') functionType: codeBlockStyle === 'fenced' ? 'fencecode' : 'indentcode',
lang
})
const codeBlock = this.createBlock('code', {
lang
})
value.split(LINE_BREAKS_REG).forEach(line => { value.split(LINE_BREAKS_REG).forEach(line => {
const codeLine = this.createBlock('span', line) const codeLine = this.createBlock('span', {
text: line
})
codeLine.lang = lang codeLine.lang = lang
codeLine.functionType = 'codeLine' codeLine.functionType = 'codeLine'
this.appendChild(codeBlock, 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)) { if (lang && !languageLoaded.has(lang)) {
languageLoaded.add(lang) languageLoaded.add(lang)
loadLanguage(lang) loadLanguage(lang)
@ -121,10 +201,7 @@ const importRegister = ContentState => {
console.warn(err) 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, inputBlock)
this.appendChild(block, codeBlock) this.appendChild(block, codeBlock)
this.appendChild(parentList[0], block) this.appendChild(parentList[0], block)
@ -144,7 +221,9 @@ const importRegister = ContentState => {
} }
for (const headText of header) { for (const headText of header) {
const i = header.indexOf(headText) 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 }) Object.assign(th, { align: align[i] || '', column: i })
this.appendChild(theadRow, th) this.appendChild(theadRow, th)
} }
@ -152,7 +231,9 @@ const importRegister = ContentState => {
const rowBlock = this.createBlock('tr') const rowBlock = this.createBlock('tr')
for (const cell of row) { for (const cell of row) {
const i = row.indexOf(cell) 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 }) Object.assign(td, { align: align[i] || '', column: i })
this.appendChild(rowBlock, td) this.appendChild(rowBlock, td)
} }
@ -181,20 +262,20 @@ const importRegister = ContentState => {
value += `\n${token.text}` value += `\n${token.text}`
} }
block = this.createBlock('p') block = this.createBlock('p')
const lines = value.split(LINE_BREAKS_REG).map(line => this.createBlock('span', line)) const contentBlock = this.createBlock('span', {
for (const line of lines) { text: value
this.appendChild(block, line) })
} this.appendChild(block, contentBlock)
this.appendChild(parentList[0], block) this.appendChild(parentList[0], block)
break break
} }
case 'paragraph': { case 'paragraph': {
value = token.text value = token.text
block = this.createBlock('p') block = this.createBlock('p')
const lines = value.split(LINE_BREAKS_REG).map(line => this.createBlock('span', line)) const contentBlock = this.createBlock('span', {
for (const line of lines) { text: value
this.appendChild(block, line) })
} this.appendChild(block, contentBlock)
this.appendChild(parentList[0], block) this.appendChild(parentList[0], block)
break break
} }
@ -226,13 +307,17 @@ const importRegister = ContentState => {
case 'loose_item_start': case 'loose_item_start':
case 'list_item_start': { case 'list_item_start': {
const { listItemType, bulletMarkerOrDelimiter, checked, type } = token const { listItemType, bulletMarkerOrDelimiter, checked, type } = token
block = this.createBlock('li') block = this.createBlock('li', {
block.listItemType = checked !== undefined ? 'task' : listItemType listItemType: checked !== undefined ? 'task' : listItemType,
block.bulletMarkerOrDelimiter = bulletMarkerOrDelimiter bulletMarkerOrDelimiter,
block.isLooseListItem = type === 'loose_item_start' isLooseListItem: type === 'loose_item_start'
})
if (checked !== undefined) { if (checked !== undefined) {
const input = this.createBlock('input') const input = this.createBlock('input', {
input.checked = checked checked
})
this.appendChild(block, input) this.appendChild(block, input)
} }
this.appendChild(parentList[0], block) this.appendChild(parentList[0], block)
@ -260,10 +345,10 @@ const importRegister = ContentState => {
const { turndownConfig } = this const { turndownConfig } = this
const turndownService = new TurndownService(turndownConfig) const turndownService = new TurndownService(turndownConfig)
usePluginAddRules(turndownService) 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 &nbsp; vanlished. // fix #752, but I don't know why the &nbsp; vanlished.
html = html.replace(/&nbsp;/g, ' ') html = html.replace(/&nbsp;/g, ' ')
const markdown = turndownService.turndown(html) // .replace(/(\\)\\/g, '$1') html = turnSoftBreakToSpan(html)
const markdown = turndownService.turndown(html)
return markdown return markdown
} }
@ -308,7 +393,7 @@ const importRegister = ContentState => {
// set cursor // set cursor
const travel = blocks => { const travel = blocks => {
for (const block of blocks) { for (const block of blocks) {
const { key, text, children, editable, type, functionType } = block const { key, text, children, editable } = block
if (text) { if (text) {
const offset = text.indexOf(CURSOR_DNA) const offset = text.indexOf(CURSOR_DNA)
if (offset > -1) { if (offset > -1) {
@ -318,17 +403,6 @@ const importRegister = ContentState => {
start: { key, offset }, start: { key, offset },
end: { 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 return
} }
} }
@ -351,7 +425,6 @@ const importRegister = ContentState => {
} }
ContentState.prototype.importMarkdown = function (markdown) { ContentState.prototype.importMarkdown = function (markdown) {
this.codeBlocks = new Map()
this.blocks = this.markdownToState(markdown) this.blocks = this.markdownToState(markdown)
} }
} }

View File

@ -23,6 +23,8 @@ export const isEven = number => Math.abs(number) % 2 === 0
export const isLengthEven = (str = '') => str.length % 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 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 * Are two arrays have intersection
*/ */

View File

@ -26,11 +26,13 @@ export const usePluginAddRules = turndownService => {
} }
}) })
// handle `soft line break` and `hard line break` // handle `line break` in code block
// add `LINE_BREAK` to the end of soft line break and hard line break. // add `LINE_BREAK` to the end of every code line but not the last line.
turndownService.addRule('lineBreak', { turndownService.addRule('codeLineBreak', {
filter (node, options) { 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) { replacement (content, node, options) {
return content + LINE_BREAK return content + LINE_BREAK

View File

@ -141,12 +141,6 @@ kbd {
text-decoration: none; text-decoration: none;
} }
h1 .ag-gray,
h2 .ag-gray,
h3 .ag-gray {
font-size: .6em;
}
.ag-header-tight-space { .ag-header-tight-space {
margin-left: -.3em; margin-left: -.3em;
} }
@ -227,29 +221,48 @@ kbd {
} }
h1 { h1 {
font-size: 40px; font-size: 30px;
} }
h2 { h2 {
font-size: 32px;
}
h3 {
font-size: 28px;
}
h4 {
font-size: 24px; font-size: 24px;
} }
h5 { h3 {
font-size: 22px;
}
h4 {
font-size: 20px; font-size: 20px;
} }
h5 {
font-size: 18px;
}
h6 { h6 {
font-size: 16px; 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, p,
blockquote, blockquote,
ul, ul,

View File

@ -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_ *this is in italic* and _so is this_
@ -12,17 +18,19 @@
<s>this is strike through text</s> <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 ## Paragraph
A two trailing spaces and a new line A two trailing spaces and a new line
makes a line break. makes a line break.
Two new lines make a new paragraph. 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
```

View File

@ -1,5 +1,7 @@
# Code Blocks # Code Blocks
## Indent Code Block
This line won't *have any markdown* formatting applied. This line won't *have any markdown* formatting applied.
I can even write <b>HTML</b> and it will show up as text. 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 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. Within a paragraph, you can use backquotes to do the same thing.
`This won't be *italic* or **bold** at all.` `This won't be *italic* or **bold** at all.`
## Fence Code Block
```cpp ```cpp
#include <iostream> #include <iostream>

View File

@ -1,5 +1,7 @@
# Headings # Headings
## Setext heading
This is a huge header This is a huge header
=== ===
@ -9,6 +11,14 @@ this is a smaller header
header header
--- ---
This is a huge header
==================
this is a smaller header
------------------
## Atx heading
## ATX Headings ## ATX Headings
# foo # foo
@ -53,20 +63,6 @@ foo
bar 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 ## Horizontal Rule
- - - - -- --- --- ---- - - - - -- --- --- ----
```

View File

@ -8682,10 +8682,10 @@ pretty-error@^2.0.2:
renderkid "^2.0.1" renderkid "^2.0.1"
utila "~0.4" utila "~0.4"
prismjs@^1.16.0: prismjs2@^1.15.1:
version "1.16.0" version "1.15.1"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.16.0.tgz#406eb2c8aacb0f5f0f1167930cb83835d10a4308" resolved "https://registry.yarnpkg.com/prismjs2/-/prismjs2-1.15.1.tgz#6dda1b9aa7e8ecddf55b145f2189b605f89e2738"
integrity sha512-OA4MKxjFZHSvZcisLGe14THYsug/nF6O1f0pAJc0KN0wTyAcLqmsbE+lTGKSpyh+9pEW57+k6pg2AfYR+coyHA== integrity sha512-tDYrcjuYxi5VceNCniF7YjxFTHJv7unA5KbN9EVZh0hnKmEaxdSSe43Gagobvue5UnbnUSB0y+l5b8Y3C1cXkA==
optionalDependencies: optionalDependencies:
clipboard "^2.0.0" clipboard "^2.0.0"