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",
"mermaid": "^8.0.0",
"popper.js": "^1.15.0",
"prismjs": "^1.16.0",
"prismjs2": "^1.15.1",
"snabbdom": "^0.7.3",
"snabbdom-to-html": "^5.1.1",
"snapsvg": "^0.5.1",

View File

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

View File

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

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
export const DEVICE_MEMORY = navigator.deviceMemory || 4 // Get the divice memory number(Chrome >= 63)
export const UNDO_DEPTH = DEVICE_MEMORY >= 4 ? 100 : 50
export const HAS_TEXT_BLOCK_REG = /^(h\d|span|th|td|hr)/i
export const HAS_TEXT_BLOCK_REG = /^(span|th|td)/i
export const VOID_HTML_TAGS = voidHtmlTags
export const HTML_TAGS = htmlTags
// TYPE1 ~ TYPE7 according to https://github.github.com/gfm/#html-blocks
@ -85,6 +85,8 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_FRONT_ICON',
'AG_GRAY',
'AG_HARD_LINE_BREAK',
'AG_HARD_LINE_BREAK_SPACE',
'AG_LINE_END',
'AG_HEADER_TIGHT_SPACE',
'AG_HIDE',
'AG_HIGHLIGHT',
@ -99,7 +101,6 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_INLINE_RULE',
'AG_LANGUAGE',
'AG_LANGUAGE_INPUT',
'AG_LINE',
'AG_LINK',
'AG_LINK_IN_BRACKET',
'AG_LIST_ITEM',
@ -111,6 +112,7 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_RUBY_TEXT',
'AG_RUBY_RENDER',
'AG_SELECTED',
'AG_SOFT_LINE_BREAK',
'AG_MATH_ERROR',
'AG_MATH_MARKER',
'AG_MATH_RENDER',
@ -159,7 +161,18 @@ export const DEFAULT_TURNDOWN_CONFIG = {
codeBlockStyle: 'fenced', // fenced or indented
fence: '```', // ``` or ~~~
emDelimiter: '*', // _ or *
strongDelimiter: '**' // ** or __
strongDelimiter: '**', // ** or __
blankReplacement (content, node, options) {
if (node && node.classList.contains('ag-soft-line-break')) {
return LINE_BREAK
} else if (node && node.classList.contains('ag-hard-line-break')) {
return ' ' + LINE_BREAK
} else if (node && node.classList.contains('ag-hard-line-break-sapce')) {
return ''
} else {
return node.isBlock ? '\n\n' : ''
}
}
}
export const FORMAT_MARKER_MAP = {

View File

@ -4,7 +4,7 @@ import selection from '../selection'
// If the next block is header, put cursor after the `#{1,6} *`
const adjustOffset = (offset, block, event) => {
if (/^h\d$/.test(block.type) && event.key === EVENT_KEYS.ArrowDown) {
if (/^span$/.test(block.type) && block.functionType === 'atxLine' && event.key === EVENT_KEYS.ArrowDown) {
const match = /^\s{0,3}(?:#{1,6})(?:\s{1,}|$)/.exec(block.text)
if (match) {
return match[0].length

View File

@ -106,11 +106,32 @@ const backspaceCtrl = ContentState => {
if (!start || !end) {
return
}
const startBlock = this.getBlock(start.key)
const endBlock = this.getBlock(end.key)
const maybeLastRow = this.getParent(endBlock)
const startOutmostBlock = this.findOutMostBlock(startBlock)
const endOutmostBlock = this.findOutMostBlock(endBlock)
// Just for fix delete the last `#` or all the atx heading cause error @fixme
if (
start.key === end.key &&
startBlock.type === 'span' &&
startBlock.functionType === 'atxLine'
) {
if (
start.offset === 0 && end.offset === startBlock.text.length ||
start.offset === end.offset && start.offset === 1 && startBlock.text === '#'
) {
event.preventDefault()
startBlock.text = ''
this.cursor = {
start: { key: start.key, offset: 0 },
end: { key: end.key, offset: 0 }
}
this.updateToParagraph(this.getParent(startBlock), startBlock)
return this.partialRender()
}
}
// fix: #897
const { text } = startBlock
const tokens = tokenizer(text)
@ -219,7 +240,7 @@ const backspaceCtrl = ContentState => {
) {
const preBlock = this.getParent(parent)
const pBlock = this.createBlock('p')
const lineBlock = this.createBlock('span', block.text)
const lineBlock = this.createBlock('span', { text: block.text })
const key = lineBlock.key
const offset = 0
this.appendChild(pBlock, lineBlock)
@ -235,10 +256,8 @@ const backspaceCtrl = ContentState => {
case 'mermaid':
case 'sequence':
case 'vega-lite':
referenceBlock = this.getParent(preBlock)
break
case 'html':
referenceBlock = this.getParent(this.getParent(preBlock))
referenceBlock = this.getParent(preBlock)
break
}
this.insertBefore(pBlock, referenceBlock)

View File

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

View File

@ -72,27 +72,6 @@ const codeBlockCtrl = ContentState => {
this.partialRender()
}
ContentState.prototype.indentCodeBlockUpdate = function (block) {
const oldPBlock = this.getParent(block)
const codeBlock = this.createBlock('code')
const inputBlock = this.createBlock('span', '')
const preBlock = this.createBlock('pre')
oldPBlock.children.forEach(child => {
child.lang = ''
child.functionType = 'codeLine'
child.text = child.text.replace(/^ {4}/, '')
this.appendChild(codeBlock, child)
})
codeBlock.lang = preBlock.lang = ''
inputBlock.functionType = 'languageInput'
preBlock.functionType = 'indentcode'
this.appendChild(preBlock, inputBlock)
this.appendChild(preBlock, codeBlock)
this.insertBefore(preBlock, oldPBlock)
this.removeBlock(oldPBlock)
}
/**
* [codeBlockUpdate if block updated to `pre` return true, else return false]
*/
@ -108,9 +87,9 @@ const codeBlockCtrl = ContentState => {
const match = CODE_UPDATE_REP.exec(text)
if (match || lang) {
const codeBlock = this.createBlock('code')
const firstLine = this.createBlock('span', code)
const firstLine = this.createBlock('span', { text: code })
const language = lang || (match ? match[1] : '')
const inputBlock = this.createBlock('span', language)
const inputBlock = this.createBlock('span', { text: language })
loadLanguage(language)
inputBlock.functionType = 'languageInput'
block.type = 'pre'

View File

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

View File

@ -101,6 +101,12 @@ const copyCutCtrl = ContentState => {
hb.replaceWith(pre)
}
// Just work for turndown, turndown will add `leading` and `traling` space in line-break.
const lineBreaks = wrapper.querySelectorAll('span.ag-soft-line-break, span.ag-hard-line-break')
for (const b of lineBreaks) {
b.innerHTML = ''
}
const mathBlock = wrapper.querySelectorAll(`figure.ag-container-block`)
for (const mb of mathBlock) {
const preElement = mb.querySelector('pre[data-role]')
@ -128,6 +134,7 @@ const copyCutCtrl = ContentState => {
let htmlData = wrapper.innerHTML
const textData = this.htmlToMarkdown(htmlData)
htmlData = marked(textData)
return { html: htmlData, text: textData }
}

View File

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

View File

@ -2,61 +2,20 @@ import { VOID_HTML_TAGS, HTML_TAGS } from '../config'
import { inlineRules } from '../parser/rules'
const HTML_BLOCK_REG = /^<([a-zA-Z\d-]+)(?=\s|>)[^<>]*?>$/
const LINE_BREAKS = /\n/
const htmlBlock = ContentState => {
ContentState.prototype.createCodeInHtml = function (code) {
const codeContainer = this.createBlock('div')
codeContainer.functionType = 'html'
const preview = this.createBlock('div', '', false)
preview.functionType = 'preview'
const preBlock = this.createBlock('pre')
const codeBlock = this.createBlock('code')
code.split(LINE_BREAKS).forEach(line => {
const codeLine = this.createBlock('span', line)
codeLine.functionType = 'codeLine'
codeLine.lang = 'markup'
this.appendChild(codeBlock, codeLine)
})
this.codeBlocks.set(preBlock.key, code)
preBlock.lang = 'markup'
codeBlock.lang = 'markup'
preBlock.functionType = 'html'
this.codeBlocks.set(preBlock.key, code)
this.appendChild(preBlock, codeBlock)
this.appendChild(codeContainer, preBlock)
this.appendChild(codeContainer, preview)
return codeContainer
}
ContentState.prototype.handleHtmlBlockClick = function (codeWrapper) {
const id = codeWrapper.id
const codeBlock = this.getBlock(id).children[0]
const key = codeBlock.key
const offset = 0
this.cursor = {
start: { key, offset },
end: { key, offset }
}
this.partialRender()
}
ContentState.prototype.createHtmlBlock = function (code) {
const block = this.createBlock('figure')
block.functionType = 'html'
const htmlBlock = this.createCodeInHtml(code)
this.appendChild(block, htmlBlock)
const { preBlock, preview } = this.createPreAndPreview('html', code)
this.appendChild(block, preBlock)
this.appendChild(block, preview)
return block
}
ContentState.prototype.initHtmlBlock = function (block) {
let htmlContent = ''
const text = block.type === 'p'
? block.children.map((child => {
return child.text
})).join('\n').trim()
: block.text
const text = block.children[0].text
const matches = inlineRules.html_tag.exec(text)
if (matches) {
const tag = matches[3]
@ -83,9 +42,11 @@ const htmlBlock = ContentState => {
block.functionType = 'html'
block.text = htmlContent
block.children = []
const codeContainer = this.createCodeInHtml(htmlContent)
this.appendChild(block, codeContainer)
return codeContainer.children[0] // preBlock
const { preBlock, preview } = this.createPreAndPreview('html', htmlContent)
this.appendChild(block, preBlock)
this.appendChild(block, preview)
return preBlock // preBlock
}
ContentState.prototype.updateHtmlBlock = function (block) {

View File

@ -63,7 +63,6 @@ class ContentState {
this.exemption = new Set()
this.blocks = [ this.createBlockP() ]
this.stateRender = new StateRender(muya)
this.codeBlocks = new Map()
this.renderRange = [ null, null ]
this.currentCursor = null
// you'll select the outmost block of current cursor when you click the front icon.
@ -183,24 +182,32 @@ class ContentState {
* A block in Mark Text present a paragraph(block syntax in GFM) or a line in paragraph.
* a `span` block must in a `p block` or `pre block` and `p block`'s children must be `span` blocks.
*/
createBlock (type = 'span', text = '', editable = true) {
createBlock (type = 'span', extras = {}) {
const key = getUniqueId()
return {
const blockData = {
key,
text: '',
type,
text,
editable,
editable: true,
parent: null,
preSibling: null,
nextSibling: null,
children: []
}
// give span block a default functionType `paragraphContent`
if (type === 'span' && !extras.functionType) {
blockData.functionType = 'paragraphContent'
}
Object.assign(blockData, extras)
return blockData
}
createBlockP (text = '') {
const pBlock = this.createBlock('p')
const lineBlock = this.createBlock('span', text)
this.appendChild(pBlock, lineBlock)
const contentBlock = this.createBlock('span', { text })
this.appendChild(pBlock, contentBlock)
return pBlock
}

View File

@ -32,7 +32,7 @@ const inputCtrl = ContentState => {
// Input @ to quick insert paragraph
ContentState.prototype.checkQuickInsert = function (block) {
const { type, text, functionType } = block
if (type !== 'span' || functionType) return false
if (type !== 'span' || functionType !== 'paragraphContent') return false
return /^@\S*$/.test(text)
}
@ -88,6 +88,7 @@ const inputCtrl = ContentState => {
const block = this.getBlock(key)
const paragraph = document.querySelector(`#${key}`)
let text = getTextContent(paragraph, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ])
let needRender = false
let needRenderAll = false
if (oldStart.key !== oldEnd.key) {
@ -196,7 +197,26 @@ const inputCtrl = ContentState => {
if (this.checkNotSameToken(block.text, text)) {
needRender = true
}
block.text = text
// Just work for `Shift + Enter` to create a soft and hard line break.
if (
block.text.endsWith('\n') &&
start.offset === text.length &&
event.inputType === 'insertText'
) {
block.text += event.data
start.offset++
end.offset++
} else if (
block.text.length === oldStart.offset &&
block.text[oldStart.offset - 2] === '\n' &&
event.inputType === 'deleteContentBackward'
) {
block.text = block.text.substring(0, oldStart.offset - 1)
start.offset = block.text.length
end.offset = block.text.length
} else {
block.text = text
}
if (beginRules['reference_definition'].test(text)) {
needRenderAll = true
}
@ -226,7 +246,6 @@ const inputCtrl = ContentState => {
// Update preview content of math block
if (block && block.type === 'span' && block.functionType === 'codeLine') {
needRender = true
this.updateCodeBlocks(block)
}
this.cursor = { start, end }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -66,6 +66,9 @@ class Keyboard {
if (event.target.closest('[contenteditable=false]')) {
return
}
// We need check cursor is null, because we may copy the html preview content,
// and no need to dispatch change.
const { start, end } = selection.getCursorRange()
if (!start || !end) {
return

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
* [renderBlock render one block, no matter it is a container block or text block]
*/
export default function renderBlock (block, cursor, activeBlocks, selectedBlock, matches, useCache = false) {
const method = block.children.length > 0
const method = Array.isArray(block.children) && block.children.length > 0
? 'renderContainerBlock'
: 'renderLeafBlock'

View File

@ -17,28 +17,47 @@ const PRE_BLOCK_HASH = {
export default function renderContainerBlock (block, cursor, activeBlocks, selectedBlock, matches, useCache = false) {
let selector = this.getSelector(block, cursor, activeBlocks, selectedBlock)
const {
type,
headingStyle,
editable,
functionType,
listType,
listItemType,
bulletMarkerOrDelimiter,
isLooseListItem,
lang
} = block
const children = block.children.map(child => this.renderBlock(child, cursor, activeBlocks, selectedBlock, matches, useCache))
const data = {
attrs: {},
dataset: {}
}
// handle `div` block
if (/div/.test(block.type)) {
if (block.toolBarType) {
selector += `.${'ag-tool-' + block.toolBarType}.${CLASS_OR_ID['AG_TOOL_BAR']}`
}
if (block.functionType) {
selector += `.${'ag-function-' + block.functionType}`
}
if (block.editable !== undefined && !block.editable) {
Object.assign(data.attrs, { contenteditable: 'false' })
}
if (editable === false) {
Object.assign(data.attrs, { contenteditable: 'false' })
}
// handle `figure` block
if (block.type === 'figure') {
if (block.functionType) {
Object.assign(data.dataset, { role: block.functionType.toUpperCase() })
if (block.functionType === 'table') {
if (/code|pre/.test(type) && typeof lang === 'string' && !!lang) {
selector += `.language-${lang}`
}
if (/^h/.test(type)) {
if (/^h\d$/.test(type)) {
// TODO: This should be the best place to create and update the TOC.
// Cache `block.key` and title and update only if necessary.
Object.assign(data.dataset, {
head: type
})
selector += `.${headingStyle}`
}
Object.assign(data.dataset, {
role: type
})
} else if (type === 'figure') {
if (functionType) {
Object.assign(data.dataset, { role: functionType.toUpperCase() })
if (functionType === 'table') {
children.unshift(renderTableTools(activeBlocks))
} else {
children.unshift(renderEditIcon())
@ -46,61 +65,29 @@ export default function renderContainerBlock (block, cursor, activeBlocks, selec
}
if (
/multiplemath|flowchart|mermaid|sequence|vega-lite/.test(block.functionType)
/html|multiplemath|flowchart|mermaid|sequence|vega-lite/.test(functionType)
) {
selector += `.${CLASS_OR_ID['AG_CONTAINER_BLOCK']}`
}
}
// hanle list block
if (/ul|ol/.test(block.type) && block.listType) {
switch (block.listType) {
case 'order':
selector += `.${CLASS_OR_ID['AG_ORDER_LIST']}`
break
case 'bullet':
selector += `.${CLASS_OR_ID['AG_BULLET_LIST']}`
break
case 'task':
selector += `.${CLASS_OR_ID['AG_TASK_LIST']}`
break
default:
break
} else if (/ul|ol/.test(type) && listType) {
selector += `.ag-${listType}-list`
if (type === 'ol') {
Object.assign(data.attrs, { start: block.start })
}
}
if (block.type === 'li' && block.listItemType) {
} else if (type === 'li' && listItemType) {
Object.assign(data.dataset, { marker: bulletMarkerOrDelimiter })
selector += `.${CLASS_OR_ID['AG_LIST_ITEM']}`
switch (block.listItemType) {
case 'order':
selector += `.${CLASS_OR_ID['AG_ORDER_LIST_ITEM']}`
break
case 'bullet':
selector += `.${CLASS_OR_ID['AG_BULLET_LIST_ITEM']}`
break
case 'task':
selector += `.${CLASS_OR_ID['AG_TASK_LIST_ITEM']}`
break
default:
break
}
Object.assign(data.dataset, { marker: block.bulletMarkerOrDelimiter })
selector += block.isLooseListItem ? `.${CLASS_OR_ID['AG_LOOSE_LIST_ITEM']}` : `.${CLASS_OR_ID['AG_TIGHT_LIST_ITEM']}`
}
if (block.type === 'ol') {
Object.assign(data.attrs, { start: block.start })
}
if (block.type === 'code') {
const { lang } = block
if (lang) {
selector += `.language-${lang}`
}
}
if (block.type === 'pre') {
const { lang, functionType } = block
if (lang) {
selector += `.language-${lang}`
}
selector += `.ag-${listItemType}-list-item`
selector += isLooseListItem ? `.${CLASS_OR_ID['AG_LOOSE_LIST_ITEM']}` : `.${CLASS_OR_ID['AG_TIGHT_LIST_ITEM']}`
} else if (type === 'pre') {
Object.assign(data.dataset, { role: functionType })
selector += PRE_BLOCK_HASH[block.functionType]
if (/html|multiplemath|mermaid|flowchart|wega-lite|sequence/.test(functionType)) {
const codeBlock = block.children[0]
const code = codeBlock.children.map(line => line.text).join('\n')
this.codeCache.set(block.key, code)
}
}
if (!block.parent) {

View File

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

View File

@ -1,13 +1,17 @@
import { CLASS_OR_ID } from '../../../config'
export default function hardLineBreak (h, cursor, block, token, outerClass) {
export default function softLineBreak (h, cursor, block, token, outerClass) {
const { spaces, lineBreak, isAtEnd } = token
const className = CLASS_OR_ID['AG_HARD_LINE_BREAK']
const content = [token.spaces]
if (block.type === 'span' && block.nextSibling) {
const spaceClass = CLASS_OR_ID['AG_HARD_LINE_BREAK_SPACE']
if (isAtEnd) {
return [
h(`span.${className}`, content)
h(`span.${className}`, h(`span.${spaceClass}`, spaces)),
h(`span.${CLASS_OR_ID['AG_LINE_END']}`, lineBreak)
]
} else {
return content
return [
h(`span.${className}`, [ h(`span.${spaceClass}`, spaces), lineBreak ])
]
}
}

View File

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

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*)$/,
'html_tag': /^(<!--[\s\S]*?-->|(<([a-zA-Z]{1}[a-zA-Z\d-]*) *[_\.\-/:a-zA-Z\d='";\? *]* *(?:\/)?>)(?:([\s\S]*?)(<\/\3 *>))?)/, // row html
'html_escape': new RegExp(`^(${escapeCharacters.join('|')})`, 'i'),
'hard_line_break': /^(\s{2,})$/,
'soft_line_break': /^(\n)(?!\n)/,
'hard_line_break': /^( {2,})(\n)(?!\n)/,
// patched math marker `$`
'backlash': /^(\\)([\\`*{}\[\]()#+\-.!_>~:\|\<\>$]{1})/,

View File

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

View File

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

View File

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

View File

@ -436,6 +436,7 @@ class Selection {
let { node: anchorNode, offset: anchorOffset } = getNodeAndOffset(anchorParagraph, anchor.offset)
let { node: focusNode, offset: focusOffset } = getNodeAndOffset(focusParagraph, focus.offset)
anchorOffset = Math.min(anchorOffset, anchorNode.textContent.length)
focusOffset = Math.min(focusOffset, focusNode.textContent.length)
// First set the anchor node and anchor offeet, make it collapsed
@ -449,15 +450,14 @@ class Selection {
if (node.nodeType === 3) {
node = node.parentNode
}
return node.closest('p[data-role=hr]') ||
node.closest('span.ag-paragraph.ag-line') ||
return node.closest('span.ag-paragraph') ||
node.closest('th.ag-paragraph') ||
node.closest('td.ag-paragraph') ||
node.closest('.ag-paragraph[data-head]')
node.closest('td.ag-paragraph')
}
getCursorRange () {
let { anchorNode, anchorOffset, focusNode, focusOffset } = this.doc.getSelection()
const isAnchorValid = this.isValidCursorNode(anchorNode)
const isFocusValid = this.isValidCursorNode(focusNode)
let needFix = false
@ -480,6 +480,16 @@ class Selection {
})
}
// fix bug click empty line, the cursor will jump to the end of pre line.
if (
anchorNode === focusNode &&
anchorOffset === focusOffset &&
anchorNode.textContent === '\n' &&
focusOffset === 0
) {
focusOffset = anchorOffset = 1
}
const anchorParagraph = findNearestParagraph(anchorNode)
const focusParagraph = findNearestParagraph(focusNode)
@ -502,6 +512,7 @@ class Selection {
const aOffset = getOffsetOfParagraph(anchorNode, anchorParagraph) + anchorOffset
const fOffset = getOffsetOfParagraph(focusNode, focusParagraph) + focusOffset
const anchor = { key: anchorParagraph.id, offset: aOffset }
const focus = { key: focusParagraph.id, offset: fOffset }
const result = new Cursor({ anchor, focus })

View File

@ -1,12 +1,12 @@
import marked from '../parser/marked'
import Prism from 'prismjs'
import Prism from 'prismjs2'
import katex from 'katex'
import mermaid from 'mermaid'
import flowchart from 'flowchart.js'
import Diagram from '../parser/render/sequence'
import vegaEmbed from 'vega-embed'
import githubMarkdownCss from 'github-markdown-css/github-markdown.css'
import highlightCss from 'prismjs/themes/prism.css'
import highlightCss from 'prismjs2/themes/prism.css'
import katexCss from 'katex/dist/katex.css'
import { EXPORT_DOMPURIFY_CONFIG } from '../config'
import { sanitize, unescapeHtml } from '../utils'

View File

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

View File

@ -13,6 +13,56 @@ import { CURSOR_DNA } from '../config'
const LINE_BREAKS_REG = /\n/
// Just because turndown change `\n`(soft line break) to space, So we add `span.ag-soft-line-break` to workaround.
const turnSoftBreakToSpan = html => {
const parser = new DOMParser()
const doc = parser.parseFromString(
`<x-mt id="turn-root">${html}</x-mt>`,
'text/html'
)
const root = doc.querySelector(`#turn-root`)
const travel = childNodes => {
for (const node of childNodes) {
if (node.nodeType === 3) {
let startLen = 0
let endLen = 0
const text = node.nodeValue.replace(/^(\n+)/, (_, p) => {
startLen = p.length
return ''
}).replace(/(\n+)$/, (_, p) => {
endLen = p.length
return ''
})
if (/\n/.test(text)) {
const tokens = text.split('\n')
const params = []
let i = 0
const len = tokens.length
for (; i< len; i++) {
let text = tokens[i]
if (i === 0 && startLen !== 0) {
text = '\n'.repeat(startLen) + text
} else if (i === len - 1 && endLen !== 0) {
text = text + '\n'.repeat(endLen)
}
params.push(document.createTextNode(text))
if (i !== len - 1) {
const softBreak = document.createElement('span')
softBreak.classList.add('ag-soft-line-break')
params.push(softBreak)
}
}
node.replaceWith(...params)
}
} else if (node.nodeType === 1) {
travel(node.childNodes)
}
}
}
travel(root.childNodes)
return root.innerHTML.trim()
}
const importRegister = ContentState => {
// turn markdown to blocks
ContentState.prototype.markdownToState = function (markdown) {
@ -37,37 +87,57 @@ const importRegister = ContentState => {
while ((token = tokens.shift())) {
switch (token.type) {
case 'frontmatter': {
const lang = 'yaml'
value = token.text
block = this.createBlock('pre')
const codeBlock = this.createBlock('code')
block = this.createBlock('pre', {
functionType: token.type,
lang
})
const codeBlock = this.createBlock('code', {
lang
})
value
.replace(/^\s+/, '')
.replace(/\s$/, '')
.split(LINE_BREAKS_REG).forEach(line => {
const codeLine = this.createBlock('span', line)
codeLine.functionType = 'codeLine'
codeLine.lang = 'yaml'
const codeLine = this.createBlock('span', {
text: line,
lang,
functionType: 'codeLine'
})
this.appendChild(codeBlock, codeLine)
})
block.functionType = token.type
block.lang = codeBlock.lang = 'yaml'
this.codeBlocks.set(block.key, value)
this.appendChild(block, codeBlock)
this.appendChild(parentList[0], block)
break
}
case 'hr': {
value = '---'
block = this.createBlock('hr', value)
value = token.marker
block = this.createBlock('hr')
const thematicBreakContent = this.createBlock('span', {
text: value,
functionType: 'thematicBreakLine'
})
this.appendChild(block, thematicBreakContent)
this.appendChild(parentList[0], block)
break
}
case 'heading': {
const { headingStyle, depth, text, marker } = token
value = headingStyle === 'atx' ? '#'.repeat(+depth) + ` ${text}` : text
block = this.createBlock(`h${depth}`, value)
block.headingStyle = headingStyle
block = this.createBlock(`h${depth}`, {
headingStyle
})
const headingContent = this.createBlock('span', {
text: value,
functionType: headingStyle === 'atx'? 'atxLine' : 'paragraphContent'
})
this.appendChild(block, headingContent)
if (marker) {
block.marker = marker
}
@ -95,15 +165,25 @@ const importRegister = ContentState => {
block = this.createContainerBlock(lang, value)
this.appendChild(parentList[0], block)
} else {
block = this.createBlock('pre')
const codeBlock = this.createBlock('code')
block = this.createBlock('pre', {
functionType: codeBlockStyle === 'fenced' ? 'fencecode' : 'indentcode',
lang
})
const codeBlock = this.createBlock('code', {
lang
})
value.split(LINE_BREAKS_REG).forEach(line => {
const codeLine = this.createBlock('span', line)
const codeLine = this.createBlock('span', {
text: line
})
codeLine.lang = lang
codeLine.functionType = 'codeLine'
this.appendChild(codeBlock, codeLine)
})
const inputBlock = this.createBlock('span', lang)
const inputBlock = this.createBlock('span', {
text: lang,
functionType: 'languageInput'
})
if (lang && !languageLoaded.has(lang)) {
languageLoaded.add(lang)
loadLanguage(lang)
@ -121,10 +201,7 @@ const importRegister = ContentState => {
console.warn(err)
})
}
inputBlock.functionType = 'languageInput'
this.codeBlocks.set(block.key, value)
block.functionType = codeBlockStyle === 'fenced' ? 'fencecode' : 'indentcode'
block.lang = codeBlock.lang = lang
this.appendChild(block, inputBlock)
this.appendChild(block, codeBlock)
this.appendChild(parentList[0], block)
@ -144,7 +221,9 @@ const importRegister = ContentState => {
}
for (const headText of header) {
const i = header.indexOf(headText)
const th = this.createBlock('th', restoreTableEscapeCharacters(headText))
const th = this.createBlock('th', {
text: restoreTableEscapeCharacters(headText)
})
Object.assign(th, { align: align[i] || '', column: i })
this.appendChild(theadRow, th)
}
@ -152,7 +231,9 @@ const importRegister = ContentState => {
const rowBlock = this.createBlock('tr')
for (const cell of row) {
const i = row.indexOf(cell)
const td = this.createBlock('td', restoreTableEscapeCharacters(cell))
const td = this.createBlock('td', {
text: restoreTableEscapeCharacters(cell)
})
Object.assign(td, { align: align[i] || '', column: i })
this.appendChild(rowBlock, td)
}
@ -181,20 +262,20 @@ const importRegister = ContentState => {
value += `\n${token.text}`
}
block = this.createBlock('p')
const lines = value.split(LINE_BREAKS_REG).map(line => this.createBlock('span', line))
for (const line of lines) {
this.appendChild(block, line)
}
const contentBlock = this.createBlock('span', {
text: value
})
this.appendChild(block, contentBlock)
this.appendChild(parentList[0], block)
break
}
case 'paragraph': {
value = token.text
block = this.createBlock('p')
const lines = value.split(LINE_BREAKS_REG).map(line => this.createBlock('span', line))
for (const line of lines) {
this.appendChild(block, line)
}
const contentBlock = this.createBlock('span', {
text: value
})
this.appendChild(block, contentBlock)
this.appendChild(parentList[0], block)
break
}
@ -226,13 +307,17 @@ const importRegister = ContentState => {
case 'loose_item_start':
case 'list_item_start': {
const { listItemType, bulletMarkerOrDelimiter, checked, type } = token
block = this.createBlock('li')
block.listItemType = checked !== undefined ? 'task' : listItemType
block.bulletMarkerOrDelimiter = bulletMarkerOrDelimiter
block.isLooseListItem = type === 'loose_item_start'
block = this.createBlock('li', {
listItemType: checked !== undefined ? 'task' : listItemType,
bulletMarkerOrDelimiter,
isLooseListItem: type === 'loose_item_start'
})
if (checked !== undefined) {
const input = this.createBlock('input')
input.checked = checked
const input = this.createBlock('input', {
checked
})
this.appendChild(block, input)
}
this.appendChild(parentList[0], block)
@ -260,10 +345,10 @@ const importRegister = ContentState => {
const { turndownConfig } = this
const turndownService = new TurndownService(turndownConfig)
usePluginAddRules(turndownService)
// remove double `\\` in Math but I dont know why there are two '\' when paste. @jocs
// fix #752, but I don't know why the &nbsp; vanlished.
html = html.replace(/&nbsp;/g, ' ')
const markdown = turndownService.turndown(html) // .replace(/(\\)\\/g, '$1')
html = turnSoftBreakToSpan(html)
const markdown = turndownService.turndown(html)
return markdown
}
@ -308,7 +393,7 @@ const importRegister = ContentState => {
// set cursor
const travel = blocks => {
for (const block of blocks) {
const { key, text, children, editable, type, functionType } = block
const { key, text, children, editable } = block
if (text) {
const offset = text.indexOf(CURSOR_DNA)
if (offset > -1) {
@ -318,17 +403,6 @@ const importRegister = ContentState => {
start: { key, offset },
end: { key, offset }
}
// handle cursor in Math block, need to remove `CURSOR_DNA` in preview block
if (type === 'span' && functionType === 'codeLine') {
const preBlock = this.getParent(this.getParent(block))
const code = this.codeBlocks.get(preBlock.key)
if (!code) return
const offset = code.indexOf(CURSOR_DNA)
if (offset > -1) {
const newCode = code.substring(0, offset) + code.substring(offset + CURSOR_DNA.length)
this.codeBlocks.set(preBlock.key, newCode)
}
}
return
}
}
@ -351,7 +425,6 @@ const importRegister = ContentState => {
}
ContentState.prototype.importMarkdown = function (markdown) {
this.codeBlocks = new Map()
this.blocks = this.markdownToState(markdown)
}
}

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 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
*/

View File

@ -26,11 +26,13 @@ export const usePluginAddRules = turndownService => {
}
})
// handle `soft line break` and `hard line break`
// add `LINE_BREAK` to the end of soft line break and hard line break.
turndownService.addRule('lineBreak', {
// handle `line break` in code block
// add `LINE_BREAK` to the end of every code line but not the last line.
turndownService.addRule('codeLineBreak', {
filter (node, options) {
return node.nodeName === 'SPAN' && node.classList.contains(CLASS_OR_ID['AG_LINE']) && node.nextElementSibling
return (
node.nodeName === 'SPAN' && node.classList.contains(CLASS_OR_ID['AG_CODE_LINE']) && node.nextElementSibling
)
},
replacement (content, node, options) {
return content + LINE_BREAK

View File

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

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_
@ -12,17 +18,19 @@
<s>this is strike through text</s>
So _a_ single _word_ followed _b_y _a_nother
So __a__ single __word__ followed __b__y __a__nother
## Some markdown extentions
This is emoji :man:
This is inline math $a \ne b$
## Paragraph
A two trailing spaces and a new line
makes a line break.
Two new lines make a new paragraph.
## Failing Tests
```
So _a_ single _word_ followed _b_y _a_nother
So __a__ single __word__ followed __b__y __a__nother
```

View File

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

View File

@ -1,5 +1,7 @@
# Headings
## Setext heading
This is a huge header
===
@ -9,6 +11,14 @@ this is a smaller header
header
---
This is a huge header
==================
this is a smaller header
------------------
## Atx heading
## ATX Headings
# foo
@ -53,20 +63,6 @@ foo
bar
## Failing Tests
Headings and horizontal rules are shrinked to three characters because this simplify the parsing - maybe we should change this.
```
This is a huge header
==================
this is a smaller header
------------------
```
```
## Horizontal Rule
----------------
```
- - - - -- --- --- ----

View File

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