Prism replace codemirror (#516)

* feat: basic use of code block by prism

* opti: remove codemirror from muya

* feat: add highlight to math and frontmatter

* feat: import and export in math block, html block, frontmatter, code block

* update: paragraph ctrl

* feat: copy and paste in new math block and html block

* feat: update code block style in dark theme

* feat: search and replace in code block

* fix: update menu item status when selection changed

* opti: optimization of updateCtrl divide it into clickCtrl and inputCtrl

* opti: search and replace in code block when no lang selected

* opti: copy paste in code block

* feat: insert paragraph before or after code block

* opti: change emoji.js to emoji.json

* feat: auto indent in code block

* opti: auto indent in code block

* opti: remove the use of snabbdom-virtualize

* fix: do not show format float box in code block

* opti: emoji picker

* update: delete some unused codes

* update: electron

* use a temp prismjs2 instead of prismjs
This commit is contained in:
Ran Luo 2018-10-23 21:21:58 +08:00 committed by GitHub
parent 4c9e6f643b
commit 39e1ea8081
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 16307 additions and 15068 deletions

View File

@ -45,7 +45,7 @@ const rendererConfig = {
}
},
{
test: /(katex|github\-markdown|highlight\.js\/styles\/default)\.css$/,
test: /(katex|github\-markdown|prism[\-a-z]*)\.css$/,
use: [
'to-string-loader',
'css-loader'
@ -53,7 +53,7 @@ const rendererConfig = {
},
{
test: /\.css$/,
exclude: /(katex|github\-markdown|highlight\.js\/styles\/default)\.css$/,
exclude: /(katex|github\-markdown|prism[\-a-z]*)\.css$/,
use: [
proMode ? MiniCssExtractPlugin.loader : 'style-loader',
{ loader: 'css-loader', options: { importLoader: 1 } },

View File

@ -34,7 +34,7 @@ const webConfig = {
}
},
{
test: /(katex|github\-markdown|highlight\.js\/styles\/default)\.css$/,
test: /(katex|github\-markdown|prism[\-a-z]*)\.css$/,
use: [
'to-string-loader',
'css-loader'
@ -42,7 +42,7 @@ const webConfig = {
},
{
test: /\.css$/,
exclude: /(katex|github\-markdown|highlight\.js\/styles\/default)\.css$/,
exclude: /(katex|github\-markdown|prism[\-a-z]*)\.css$/,
use: [
proMode ? MiniCssExtractPlugin.loader : 'style-loader',
{ loader: 'css-loader', options: { importLoader: 1 } },

View File

@ -93,7 +93,7 @@
<span>:es:</span>
</a>
<a href="https://github.com/marktext/marktext/blob/master/doc/i18n/pt.md#readme">
<span>Portuguese</span>
<span>:portugal:</span>
</a>
</div>

88
package-lock.json generated
View File

@ -192,6 +192,12 @@
"integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==",
"dev": true
},
"@types/node": {
"version": "8.10.36",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.36.tgz",
"integrity": "sha512-SL6KhfM7PTqiFmbCW3eVNwVBZ+88Mrzbuvn9olPsfv43mbiWaFY+nRcz/TGGku0/lc2FepdMbImdMY1JrQ+zbw==",
"dev": true
},
"@vue/component-compiler-utils": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-2.2.0.tgz",
@ -2860,6 +2866,17 @@
"integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
"dev": true
},
"clipboard": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.1.tgz",
"integrity": "sha512-7yhQBmtN+uYZmfRjjVjKa0dZdWuabzpSKGtyQZN+9C8xlC788SSJjOHWh7tzurfwTqTD5UDYAhIv5fRJg3sHjQ==",
"optional": true,
"requires": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"cliui": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
@ -3841,6 +3858,12 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
"optional": true
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@ -4167,22 +4190,14 @@
"dev": true
},
"electron": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-3.0.3.tgz",
"integrity": "sha512-5ypkMO368UbWd1e0ZwKaflYLXSHSw2wAvC5/yApv03pX/KV3uD/2/qF7rW841H9I3QPmS03YZ6UZmlQV/fNczw==",
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/electron/-/electron-3.0.5.tgz",
"integrity": "sha512-rcHNbhSGfj80Av5p06LgIUxN8wQbrdx8yblikJamDezqxe0B11CJSEJuidz6TJoCRDZuWHt+P5xMAEhp92ZUcA==",
"dev": true,
"requires": {
"@types/node": "^8.0.24",
"electron-download": "^4.1.0",
"extract-zip": "^1.0.3"
},
"dependencies": {
"@types/node": {
"version": "8.10.34",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.34.tgz",
"integrity": "sha512-alypNiaAEd0RBGXoWehJ2gchPYCITmw4CYBoB5nDlji8l8on7FsklfdfIs4DDmgpKLSX3OF3ha6SV+0W7cTzUA==",
"dev": true
}
}
},
"electron-builder": {
@ -6514,6 +6529,15 @@
"minimatch": "~3.0.2"
}
},
"good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
"optional": true,
"requires": {
"delegate": "^3.1.2"
}
},
"got": {
"version": "6.7.1",
"resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz",
@ -6819,7 +6843,8 @@
"highlight.js": {
"version": "9.12.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz",
"integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4="
"integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4=",
"dev": true
},
"hmac-drbg": {
"version": "1.0.1",
@ -6895,14 +6920,6 @@
"uglify-js": "3.3.x"
}
},
"html-parse-stringify2": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz",
"integrity": "sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=",
"requires": {
"void-elements": "^2.0.1"
}
},
"html-tags": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz",
@ -15100,6 +15117,14 @@
"utila": "~0.4"
}
},
"prismjs2": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/prismjs2/-/prismjs2-1.15.0.tgz",
"integrity": "sha512-/DT77JC3sLzWSpD4WOpIanoMuirt1KkqeFKAmTO4budSjrMwTvQgeeNECPuUm0uYQbH8fDe39s6QMFs6VR1GhQ==",
"requires": {
"clipboard": "^2.0.0"
}
},
"private": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
@ -16052,6 +16077,12 @@
}
}
},
"select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=",
"optional": true
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@ -16324,14 +16355,6 @@
"parse-sel": "^1.0.0"
}
},
"snabbdom-virtualize": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/snabbdom-virtualize/-/snabbdom-virtualize-0.7.0.tgz",
"integrity": "sha1-MfaDM4tmRXve2MHiLN2O1DjCo4c=",
"requires": {
"html-parse-stringify2": "^2"
}
},
"snake-case": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-2.1.0.tgz",
@ -17614,6 +17637,12 @@
"setimmediate": "^1.0.4"
}
},
"tiny-emitter": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz",
"integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow==",
"optional": true
},
"title-case": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/title-case/-/title-case-2.1.1.tgz",
@ -18317,7 +18346,8 @@
"void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w="
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=",
"dev": true
},
"vue": {
"version": "2.5.17",

View File

@ -132,14 +132,13 @@
"fs-extra": "^7.0.0",
"fuzzaldrin": "^2.1.0",
"github-markdown-css": "^2.10.0",
"highlight.js": "^9.12.0",
"html-tags": "^2.0.0",
"katex": "^0.10.0-rc.1",
"markdown-toc": "^1.2.0",
"popper.js": "^1.14.4",
"prismjs2": "^1.15.0",
"snabbdom": "^0.7.2",
"snabbdom-to-html": "^5.1.1",
"snabbdom-virtualize": "^0.7.0",
"turndown": "^5.0.1",
"turndown-plugin-gfm": "^1.0.2",
"vue": "^2.5.17",
@ -164,7 +163,7 @@
"css-loader": "^1.0.0",
"del": "^3.0.0",
"devtron": "^1.4.0",
"electron": "^3.0.3",
"electron": "^3.0.5",
"electron-builder": "^20.28.4",
"electron-debug": "^2.0.0",
"electron-devtools-installer": "^2.2.4",

View File

@ -66,10 +66,10 @@ const setCheckedMenuItem = affiliation => {
} else if (b.type === 'pre' && b.functionType) {
if (b.functionType === 'frontmatter') {
return item.id === 'frontMatterMenuItem'
} else if (b.functionType === 'code') {
} else if (/code$/.test(b.functionType)) {
return item.id === 'codeFencesMenuItem'
} else if (b.functionType === 'html') {
return false
return item.id === 'htmlBlockMenuItem'
} else if (b.functionType === 'multiplemath') {
return item.id === 'mathBlockMenuItem'
}
@ -101,12 +101,8 @@ ipcMain.on('AGANI::selection-change', (e, { start, end, affiliation }) => {
if (
(/th|td/.test(start.type) && /th|td/.test(end.type)) ||
(start.type === 'span' && start.block.functionType === 'frontmatter') ||
(end.type === 'span' && end.block.functionType === 'frontmatter') ||
(start.type === 'span' && start.block.functionType === 'multiplemath') ||
(end.type === 'span' && end.block.functionType === 'multiplemath') ||
(start.type === 'pre' && start.block.functionType === 'html') ||
(end.type === 'pre' && end.block.functionType === 'html')
(start.type === 'span' && start.block.functionType === 'codeLine') ||
(end.type === 'span' && end.block.functionType === 'codeLine')
) {
setParagraphMenuItemStatus(false)
} else if (start.key !== end.key) {

View File

@ -1,3 +1,4 @@
/* Common CSS use by both light and dark themes */
:root {
--brandColor: #5b3cc4;
--successColor: rgb(23, 201, 100);
@ -119,6 +120,9 @@ div.ag-function-html pre.ag-html-block {
opacity: 0;
z-index: -1;
position: absolute;
margin-top: 0;
margin-bottom: 0;
overflow: visible;
}
div.ag-function-html.ag-active pre.ag-html-block,
@ -431,12 +435,17 @@ pre.ag-front-matter {
margin: 1rem 0;
}
span.ag-front-matter-line:first-of-type:empty::after {
pre.ag-front-matter span.ag-code-line:first-of-type:empty::after {
content: 'Input YAML Front Matter...';
color: var(--placeholerColor);
}
span.ag-multiple-math-line:first-of-type:empty::after {
pre[data-role$='code'] span.ag-language-input:empty::after {
content: 'Input Language...';
color: var(--placeholerColor);
}
pre.ag-multiple-math span.ag-code-line:first-of-type:empty::after {
content: 'Input Mathematical Formula...';
color: var(--placeholerColor);
}
@ -444,7 +453,8 @@ span.ag-multiple-math-line:first-of-type:empty::after {
figure,
pre.ag-html-block,
div.ag-function-html,
pre.ag-code-block,
pre.ag-fence-code,
pre.ag-indent-code,
li.ag-list-item > p.ag-paragraph {
position: relative;
display: inline-flex;
@ -460,9 +470,14 @@ li.ag-list-item > p.ag-paragraph > span {
width: 100%;
}
pre.ag-code-block {
pre.ag-fence-code,
pre.ag-indent-code {
margin: 1rem 0;
padding: 0 .5rem;
}
pre > code {
width: 100%;
display: block;
}
pre.ag-active.ag-front-matter::before,
@ -475,15 +490,19 @@ pre.ag-active.ag-multiple-math::after {
content: '$$';
}
pre.ag-active.ag-code-block::before,
pre.ag-active.ag-code-block::after {
pre.ag-active.ag-fence-code::before,
pre.ag-active.ag-indent-code::after,
pre.ag-active.ag-fence-code::after,
pre.ag-active.ag-indent-code::before {
content: '```';
}
pre.ag-active.ag-front-matter::before,
pre.ag-active.ag-front-matter::after,
pre.ag-active.ag-code-block::before,
pre.ag-active.ag-code-block::after,
pre.ag-active.ag-fence-code::before,
pre.ag-active.ag-fence-code::after,
pre.ag-active.ag-indent-code::before,
pre.ag-active.ag-indent-code::after,
pre.ag-active.ag-multiple-math::before,
pre.ag-active.ag-multiple-math::after {
color: var(--regularColor);
@ -495,13 +514,15 @@ pre.ag-active.ag-multiple-math::after {
pre.ag-active.ag-front-matter::before,
pre.ag-active.ag-multiple-math::before,
pre.ag-active.ag-code-block::before {
pre.ag-active.ag-indent-code::before,
pre.ag-active.ag-fence-code::before {
top: -20px;
}
pre.ag-active.ag-front-matter::after,
pre.ag-active.ag-multiple-math::after,
pre.ag-active.ag-code-block::after {
pre.ag-active.ag-fence-code::after,
pre.ag-active.ag-indent-code::after {
bottom: -23px;
}
@ -520,7 +541,7 @@ figure.ag-active div.ag-math-preview {
top: calc(100% + 8px);
left: 50%;
width: auto;
z-index: 1;
z-index: 10000;
transform: translateX(-50%);
padding: .5rem;
background: #fff;
@ -534,10 +555,6 @@ div.ag-html-preview {
width: 100%;
}
pre .CodeMirror {
width: 100%;
}
img {
max-width: 100%;
}
@ -595,12 +612,12 @@ span.ag-emoji-marked-text {
}
.ag-language-input {
outline: none;
padding: 0 1rem;
display: none;
min-width: 80px;
position: absolute;
top: -20px;
left: 30px;
top: -23px;
left: 20px;
font-size: 14px;
font-family: monospace;
font-weight: 600;
@ -618,6 +635,13 @@ pre.ag-active .ag-language-input {
display: block;
}
.ag-language {
color: var(--activeColor);
font-weight: 600;
text-decoration: none;
font-family: monospace;
}
span.ag-image-marked-text, span.ag-link-in-bracket, span.ag-link-in-bracket .ag-backlash {
color: var(--regularColor);
font-size: 16px;
@ -625,11 +649,6 @@ span.ag-image-marked-text, span.ag-link-in-bracket, span.ag-link-in-bracket .ag-
font-family: monospace;
}
.ag-language {
color: var(--dangerColor);
text-decoration: none;
font-family: monospace;
}
.ag-backlash {
text-decoration: none;
color: rgb(51, 51, 51);

View File

@ -1,18 +0,0 @@
/**
* check edit language
*/
export const checkEditLanguage = (paragraph, selectionState) => {
const text = paragraph.textContent
const { start } = selectionState
const token = text.match(/(^`{3,})([^`]+)/)
if (token) {
const len = token[1].length
const lang = token[2].trim()
if (start < len) return false
if (!lang) return false
return lang
} else {
return false
}
}

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|pre)/i
export const HAS_TEXT_BLOCK_REG = /^(h\d|span|th|td|hr)/i
export const VOID_HTML_TAGS = voidHtmlTags
export const HTML_TAGS = htmlTags
// TYPE1 ~ TYPE7 according to https://github.github.com/gfm/#html-blocks
@ -68,21 +68,15 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_LINE',
'AG_ACTIVE',
'AG_EDITOR_ID',
'AG_FLOAT_BOX_ID',
'AG_FUNCTION_HTML',
'AG_FLOAT_BOX',
'AG_SHOW_FLOAT_BOX',
'AG_FLOAT_ITEM', // LI element
'AG_FLOAT_ITEM_ACTIVE',
'AG_FLOAT_ITEM_ICON', // icon wrapper in li
'AG_EMOJI_MARKED_TEXT',
'AG_CODE_BLOCK',
'AG_FENCE_CODE',
'AG_INDENT_CODE',
'AG_HTML_BLOCK',
'AG_HTML_ESCAPE',
'AG_FRONT_MATTER',
'AG_FRONT_MATTER_LINE',
'AG_MULTIPLE_MATH_LINE',
'AG_CODEMIRROR_BLOCK',
'AG_CODE_LINE',
'AG_CODE_LINE_ADD',
'AG_CODE_LINE_MINUS',
'AG_SHOW_PREVIEW',
'AG_HTML_PREVIEW',
'AG_LANGUAGE',
@ -134,20 +128,6 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_REFERENCE_LINK'
])
export const codeMirrorConfig = {
// theme: 'railscasts',
lineWrapping: true,
autoCloseBrackets: true,
lineWiseCopyCut: false,
autoCloseTags: true,
autofocus: true,
tabSize: 2,
extraKeys: {
'Cmd-Z': false,
'Cmd-Y': false
}
}
export const DAED_REMOVE_SELECTOR = new Set([
'.ag-image-marked-text::before',
'.ag-image-marked-text.ag-image-fail::before',
@ -209,7 +189,8 @@ export const HTML_TOOLS = [{
export const LINE_BREAK = '\n'
export const PREVIEW_DOMPURIFY_CONFIG = {
FORBID_ATTR: ['style', 'class', 'contenteditable'],
// do not forbit `class` because `code` element use class to present language
FORBID_ATTR: ['style', 'contenteditable'],
ALLOW_DATA_ATTR: false,
USE_PROFILES: {
html: true,

View File

@ -1,12 +1,4 @@
import { EVENT_KEYS, CLASS_OR_ID } from '../config'
import {
isCursorAtFirstLine,
isCursorAtLastLine,
isCursorAtBegin,
isCursorAtEnd,
getBeginPosition,
getEndPosition
} from '../codeMirror'
import { findNearestParagraph } from '../selection/dom'
import selection from '../selection'
@ -59,7 +51,6 @@ const arrowCtrl = ContentState => {
const preBlock = this.findPreBlockInLocation(block)
const nextBlock = this.findNextBlockInLocation(block)
const { left, right } = selection.getCaretOffsets(paragraph)
const { start, end } = selection.getCursorRange()
const { topOffset, bottomOffset } = selection.getCursorYOffset(paragraph)
@ -89,60 +80,6 @@ const arrowCtrl = ContentState => {
}
}
// handle `html` and `code` block when press arrow key
if (block.type === 'pre' && /code|html/.test(block.functionType)) {
// handle cursor in code block. the case at firstline or lastline.
const cm = this.codeBlocks.get(id)
const anchorBlock = block.functionType === 'html' ? this.getParent(this.getParent(block)) : block
let activeBlock
event.preventDefault()
event.stopPropagation()
switch (event.key) {
case EVENT_KEYS.ArrowLeft: // fallthrough
case EVENT_KEYS.ArrowUp:
if (
(event.key === EVENT_KEYS.ArrowUp && isCursorAtFirstLine(cm) && preBlock) ||
(event.key === EVENT_KEYS.ArrowLeft && isCursorAtBegin(cm) && preBlock)
) {
activeBlock = preBlock
}
break
case EVENT_KEYS.ArrowRight: // fallthrough
case EVENT_KEYS.ArrowDown:
if (
(event.key === EVENT_KEYS.ArrowDown && isCursorAtLastLine(cm)) ||
(event.key === EVENT_KEYS.ArrowRight && isCursorAtEnd(cm))
) {
if (nextBlock) {
activeBlock = nextBlock
} else {
activeBlock = this.createBlockP()
this.insertAfter(activeBlock, anchorBlock)
}
}
break
}
if (activeBlock) {
const cursorBlock = activeBlock.type === 'p' ? activeBlock.children[0] : activeBlock
const offset = cursorBlock.text.length
const key = cursorBlock.key
this.cursor = {
start: {
key,
offset
},
end: {
key,
offset
}
}
return this.partialRender()
}
return
}
if (/th|td/.test(block.type)) {
let activeBlock
const cellInNextRow = this.findNextRowCell(block)
@ -193,39 +130,6 @@ const arrowCtrl = ContentState => {
}
if (
(preBlock && preBlock.type === 'pre' && /code|html/.test(preBlock.functionType) && event.key === EVENT_KEYS.ArrowUp) ||
(preBlock && preBlock.type === 'pre' && /code|html/.test(preBlock.functionType) && event.key === EVENT_KEYS.ArrowLeft && left === 0)
) {
event.preventDefault()
event.stopPropagation()
const key = preBlock.key
const offset = 0
this.cursor = {
start: { key, offset },
end: { key, offset }
}
const cm = this.codeBlocks.get(preBlock.key)
preBlock.selection = getEndPosition(cm)
return this.partialRender()
} else if (
(nextBlock && nextBlock.type === 'pre' && /code|html/.test(nextBlock.functionType) && event.key === EVENT_KEYS.ArrowDown) ||
(nextBlock && nextBlock.type === 'pre' && /code|html/.test(nextBlock.functionType) && event.key === EVENT_KEYS.ArrowRight && right === 0)
) {
event.preventDefault()
event.stopPropagation()
const key = nextBlock.key
const offset = 0
this.cursor = {
start: { key, offset },
end: { key, offset }
}
nextBlock.selection = getBeginPosition()
return this.partialRender()
} else if (
(event.key === EVENT_KEYS.ArrowUp) ||
(event.key === EVENT_KEYS.ArrowLeft && start.offset === 0)
) {

View File

@ -1,6 +1,5 @@
import selection from '../selection'
import { findNearestParagraph, findOutMostParagraph } from '../selection/dom'
import { isCursorAtBegin, onlyHaveOneLine, getEndPosition } from '../codeMirror'
const backspaceCtrl = ContentState => {
ContentState.prototype.checkBackspaceCase = function () {
@ -128,6 +127,10 @@ const backspaceCtrl = ContentState => {
return this.render()
}
if (startBlock.functionType === 'languageInput' && start.offset === 0) {
return event.preventDefault()
}
// If select multiple paragraph or multiple characters in one paragraph, just let
// updateCtrl to handle this case.
if (start.key !== end.key || start.offset !== end.offset) {
@ -151,19 +154,39 @@ const backspaceCtrl = ContentState => {
return tHeadHasContent || tBodyHasContent
}
if (block.type === 'pre' && /code|html/.test(block.functionType)) {
const cm = this.codeBlocks.get(id)
// if event.preventDefault(), you can not use backspace in language input.
if (isCursorAtBegin(cm) && onlyHaveOneLine(cm)) {
const anchorBlock = block.functionType === 'html' ? this.getParent(this.getParent(block)) : block
event.preventDefault()
const value = cm.getValue()
const newBlock = this.createBlockP(value)
this.insertBefore(newBlock, anchorBlock)
this.removeBlock(anchorBlock)
this.codeBlocks.delete(id)
const key = newBlock.children[0].key
if (
block.type === 'span' &&
block.functionType === 'codeLine' &&
left === 0 &&
!block.preSibling
) {
event.preventDefault()
event.stopPropagation()
if (
!block.nextSibling
) {
const preBlock = this.getParent(parent)
const pBlock = this.createBlock('p')
const lineBlock = this.createBlock('span', block.text)
const key = lineBlock.key
const offset = 0
this.appendChild(pBlock, lineBlock)
let referenceBlock = null
switch (preBlock.functionType) {
case 'fencecode':
case 'indentcode':
case 'frontmatter':
referenceBlock = preBlock
break
case 'multiplemath':
referenceBlock = this.getParent(preBlock)
break
case 'html':
referenceBlock = this.getParent(this.getParent(preBlock))
break
}
this.insertBefore(pBlock, referenceBlock)
this.removeBlock(referenceBlock)
this.cursor = {
start: { key, offset },
@ -171,31 +194,6 @@ const backspaceCtrl = ContentState => {
}
this.partialRender()
}
} else if (
block.type === 'span' && /frontmatter|multiplemath/.test(block.functionType) &&
left === 0 && !block.preSibling
) {
const isMathLine = block.functionType === 'multiplemath'
event.preventDefault()
event.stopPropagation()
const { key } = block
const offset = 0
const pBlock = this.createBlock('p')
for (const line of parent.children) {
delete line.functionType
this.appendChild(pBlock, line)
}
if (isMathLine) {
parent = this.getParent(parent)
}
this.insertBefore(pBlock, parent)
this.removeBlock(parent)
this.cursor = {
start: { key, offset },
end: { key, offset }
}
this.partialRender()
} else if (left === 0 && /th|td/.test(block.type)) {
event.preventDefault()
event.stopPropagation()
@ -298,19 +296,7 @@ const backspaceCtrl = ContentState => {
const { text } = block
const key = preBlock.key
const offset = preBlock.text.length
if (preBlock.type === 'pre' && /code|html/.test(preBlock.functionType)) {
const cm = this.codeBlocks.get(key)
const value = cm.getValue() + text
cm.setValue(value)
const { line, ch } = getEndPosition(cm).anchor
preBlock.selection = {
anchor: { line, ch: ch - text.length },
head: { line, ch: ch - text.length }
}
} else {
preBlock.text += text
}
preBlock.text += text
// If block is a line block and its parent paragraph only has one text line,
// also need to remove the paragrah
if (this.isOnlyChild(block) && block.type === 'span') {

View File

@ -1,6 +1,59 @@
import selection from '../selection'
import { HAS_TEXT_BLOCK_REG } from '../config'
const clickCtrl = ContentState => {
ContentState.prototype.clickHandler = function (event) {
// todo
const { eventCenter } = this.muya
const { start, end } = selection.getCursorRange()
const block = this.getBlock(start.key)
let needRender = false
// is show format float box?
if (
start.key === end.key &&
start.offset !== end.offset &&
HAS_TEXT_BLOCK_REG.test(block.type) &&
block.functionType !== 'codeLine'
) {
const reference = this.getPositionReference()
const { formats } = this.selectionFormats()
eventCenter.dispatch('muya-format-picker', { reference, formats })
}
// bugfix: #67 problem 1
if (block && block.icon) return event.preventDefault()
// bugfix: figure block click
if (event.type === 'click' && block.type === 'figure' && block.functionType === 'table') {
// first cell in thead
const cursorBlock = block.children[1].children[0].children[0].children[0]
const offset = cursorBlock.text.length
const key = cursorBlock.key
this.cursor = {
start: { key, offset },
end: { key, offset }
}
needRender = true
}
// update '```xxx' to code block when you click other place or use press arrow key.
if (block && start.key !== this.cursor.start.key) {
const oldBlock = this.getBlock(this.cursor.start.key)
if (oldBlock) {
needRender = needRender || this.codeBlockUpdate(oldBlock)
}
}
// change active status when paragraph changed
if (
start.key !== this.cursor.start.key ||
end.key !== this.cursor.end.key
) {
needRender = true
}
const needMarkedUpdate = this.checkNeedRender(this.cursor) || this.checkNeedRender({ start, end })
this.cursor = { start, end }
if (needMarkedUpdate || needRender) {
return this.partialRender()
}
}
}

View File

@ -1,44 +1,32 @@
import codeMirror, { setMode, setCursorAtLastLine } from '../codeMirror'
import { createInputInCodeBlock } from '../utils/domManipulate'
import { sanitize, getParagraphReference } from '../utils'
import { codeMirrorConfig, BLOCK_TYPE7, PREVIEW_DOMPURIFY_CONFIG, CLASS_OR_ID } from '../config'
import { loadLanguage } from '../prism/index'
const CODE_UPDATE_REP = /^`{3,}(.*)/
const beautifyHtml = html => {
const HTML_REG = /^<([a-zA-Z\d-]+)(?=\s|>).*?>/
const HTML_NEWLINE_REG = /^<([a-zA-Z\d-]+)(?=\s|>).*?>\n/
const match = HTML_REG.exec(html)
const tag = match ? match[1] : null
// no empty line in block html
let result = html // .split(/\n/).filter(line => /\S/.test(line)).join('\n')
// start inline tag must ends with `\n`
if (tag) {
if (BLOCK_TYPE7.indexOf(tag) > -1 && !HTML_NEWLINE_REG.test(html)) {
result = result.replace(HTML_REG, (m, p) => `${m}\n`)
}
}
return result
}
const codeBlockCtrl = ContentState => {
ContentState.prototype.selectLanguage = function (paragraph, name) {
const block = this.getBlock(paragraph.id)
block.text = block.text.replace(/^(`+)([^`]+$)/g, `$1${name}`)
this.codeBlockUpdate(block)
this.partialRender()
}
// Fix bug: when click the edge at the code block, the code block will be not focused.
ContentState.prototype.focusCodeBlock = function (event) {
const key = event.target.id
const offset = 0
this.cursor = {
start: { key, offset },
end: { key, offset }
loadLanguage(name)
if (block.functionType === 'languageInput') {
block.text = name
const preBlock = this.getParent(block)
const nextSibling = this.getNextSibling(block)
preBlock.lang = name
preBlock.functionType = 'fencecode'
nextSibling.lang = name
nextSibling.children.forEach(c => (c.lang = name))
const { key } = nextSibling.children[0]
const offset = 0
this.cursor = {
start: { key, offset },
end: { key, offset }
}
} else {
block.text = block.text.replace(/^(`+)([^`]+$)/g, `$1${name}`)
this.codeBlockUpdate(block)
}
this.partialRender()
}
/**
* [codeBlockUpdate if block updated to `pre` return true, else return false]
*/
@ -53,15 +41,26 @@ const codeBlockCtrl = ContentState => {
const { text } = block.children[0]
const match = CODE_UPDATE_REP.exec(text)
if (match || lang) {
const codeBlock = this.createBlock('code')
const firstLine = this.createBlock('span', code)
const language = lang || (match ? match[1] : '')
const inputBlock = this.createBlock('span', language)
loadLanguage(language)
inputBlock.functionType = 'languageInput'
block.type = 'pre'
block.functionType = 'code'
block.codeBlockStyle = 'fenced'
block.text = code
block.functionType = 'fencecode'
block.lang = language
block.text = ''
block.history = null
block.lang = lang || (match ? match[1] : '')
block.children = []
const { key } = block
const offset = 0
codeBlock.lang = language
firstLine.lang = language
firstLine.functionType = 'codeLine'
this.appendChild(codeBlock, firstLine)
this.appendChild(block, inputBlock)
this.appendChild(block, codeBlock)
const { key } = firstLine
const offset = code.length
this.cursor = {
start: { key, offset },
end: { key, offset }
@ -70,138 +69,6 @@ const codeBlockCtrl = ContentState => {
}
return false
}
ContentState.prototype.pre2CodeMirror = function (isRenderCursor, blocks) {
const { eventCenter } = this.muya
let selector = ''
if (blocks) {
selector = blocks.map(({ type, key }) => {
if (type === 'pre') {
return `pre#${key}.${CLASS_OR_ID['AG_CODEMIRROR_BLOCK']}`
} else {
return `#${key} pre.${CLASS_OR_ID['AG_CODEMIRROR_BLOCK']}`
}
}).join(', ')
} else {
selector = `pre.${CLASS_OR_ID['AG_CODEMIRROR_BLOCK']}`
}
const pres = document.querySelectorAll(selector)
Array.from(pres).forEach(pre => {
// If pre element has children, means that this code block is not editing,
// and don't need to update to codeMirror.
if (pre.children.length) return
const id = pre.id
const block = this.getBlock(id)
const value = block.text
const autofocus = id === this.cursor.start.key && isRenderCursor
const config = Object.assign(codeMirrorConfig, { autofocus, value })
const codeBlock = codeMirror(pre, config)
const mode = pre.getAttribute('data-lang')
let input
if (block.functionType === 'code') {
input = createInputInCodeBlock(pre)
}
const handler = ({ name }) => {
setMode(codeBlock, name)
.then(m => {
pre.setAttribute('data-lang', m.name)
block.lang = m.name.toLowerCase()
// change indent code block to fence code block
if (block.codeBlockStyle !== 'fenced') {
block.codeBlockStyle = 'fenced'
}
if (input) {
input.value = m.name
input.blur()
}
if (this.cursor.start.key === block.key && isRenderCursor) {
if (block.selection) {
codeBlock.focus()
const { anchor, head } = block.selection
codeBlock.setSelection(anchor, head)
} else {
setCursorAtLastLine(codeBlock)
}
}
})
.catch(err => {
console.warn(err)
})
}
this.codeBlocks.set(id, codeBlock)
if (mode) {
handler({ name: mode })
}
if (block.selection && this.cursor.start.key === block.key && isRenderCursor) {
const { anchor, head } = block.selection
codeBlock.focus()
codeBlock.setSelection(anchor, head)
}
if (block.history) {
codeBlock.setHistory(block.history)
}
if (input) {
eventCenter.attachDOMEvent(input, 'input', () => {
const value = input.value
eventCenter.dispatch('muya-code-picker', {
reference: getParagraphReference(input, id),
lang: value.trim(),
cb: handler
})
})
}
codeBlock.on('focus', (cm, event) => {
block.selection = cm.listSelections()[0]
})
codeBlock.on('blur', (cm, event) => {
block.selection = cm.listSelections()[0]
if (block.functionType === 'html') {
const value = cm.getValue()
block.text = beautifyHtml(value)
}
})
codeBlock.on('cursorActivity', (cm, event) => {
block.coords = cm.cursorCoords()
block.selection = cm.listSelections()[0]
})
let lastUndoLength = 0
codeBlock.on('change', (cm, change) => {
const value = cm.getValue()
block.text = value
block.history = cm.getHistory()
if (block.functionType === 'html') {
const preBlock = this.getNextSibling(block)
const htmlBlock = this.getParent(this.getParent(block))
const escapedHtml = sanitize(block.text, PREVIEW_DOMPURIFY_CONFIG)
htmlBlock.text = block.text
const preEle = document.querySelector(`#${preBlock.key}`)
preEle.innerHTML = escapedHtml
preBlock.htmlContent = escapedHtml
}
const { undo } = cm.historySize()
if (undo > lastUndoLength) {
this.history.push({
type: 'codeBlock',
id
})
lastUndoLength = undo
}
})
})
}
}
export default codeBlockCtrl

View File

@ -5,9 +5,6 @@ import ExportMarkdown from '../utils/exportMarkdown'
const copyCutCtrl = ContentState => {
ContentState.prototype.cutHandler = function () {
if (this.checkInCodeBlock()) {
return
}
const { start, end } = this.cursor
const startBlock = this.getBlock(start.key)
const endBlock = this.getBlock(end.key)
@ -22,16 +19,6 @@ const copyCutCtrl = ContentState => {
this.partialRender()
}
ContentState.prototype.checkInCodeBlock = function () {
const { start, end } = selection.getCursorRange()
const { type, functionType } = this.getBlock(start.key)
if (start.key === end.key && type === 'pre' && /code|html/.test(functionType)) {
return true
}
return false
}
ContentState.prototype.getClipBoradData = function () {
const html = selection.getSelectionHtml()
const wrapper = document.createElement('div')
@ -41,7 +28,9 @@ const copyCutCtrl = ContentState => {
.${CLASS_OR_ID['AG_MATH_RENDER']},
.${CLASS_OR_ID['AG_HTML_PREVIEW']},
.${CLASS_OR_ID['AG_MATH_PREVIEW']},
.${CLASS_OR_ID['AG_COPY_REMOVE']}`
.${CLASS_OR_ID['AG_COPY_REMOVE']},
.${CLASS_OR_ID['AG_LANGUAGE_INPUT']}`
)
;[...removedElements].forEach(e => e.remove())
@ -78,31 +67,32 @@ const copyCutCtrl = ContentState => {
l.replaceWith(span)
})
const codefense = wrapper.querySelectorAll(`pre.${CLASS_OR_ID['AG_CODE_BLOCK']}`)
const codefense = wrapper.querySelectorAll(`pre[data-role$='code']`)
;[...codefense].forEach(cf => {
const id = cf.id
const language = cf.getAttribute('data-lang') || ''
const cm = this.codeBlocks.get(id)
const value = cm.getValue()
cf.innerHTML = `<code class="language-${language}" lang="${language}">${value}</code>`
const block = this.getBlock(id)
const language = block.lang || ''
const selectedCodeLines = cf.querySelectorAll('.ag-code-line')
const value = [...selectedCodeLines].map(codeLine => codeLine.textContent).join('\n')
cf.innerHTML = `<code class="language-${language}">${value}</code>`
})
const htmlBlock = wrapper.querySelectorAll(`figure[data-role='HTML']`)
;[...htmlBlock].forEach(hb => {
const id = hb.id
const { text } = this.getBlock(id)
const selectedCodeLines = hb.querySelectorAll('span.ag-code-line')
const value = [...selectedCodeLines].map(codeLine => codeLine.textContent).join('\n')
const pre = document.createElement('pre')
pre.textContent = text
pre.textContent = value
hb.replaceWith(pre)
})
const mathBlock = wrapper.querySelectorAll(`figure.ag-multiple-math-block`)
;[...mathBlock].forEach(mb => {
const id = mb.id
const { math } = this.getBlock(id).children[1]
const selectedCodeLines = mb.querySelectorAll('span.ag-code-line')
const value = [...selectedCodeLines].map(codeLine => codeLine.textContent).join('\n')
const pre = document.createElement('pre')
pre.classList.add('multiple-math')
pre.textContent = math
pre.textContent = value
mb.replaceWith(pre)
})
@ -113,9 +103,6 @@ const copyCutCtrl = ContentState => {
}
ContentState.prototype.copyHandler = function (event, type) {
if (this.checkInCodeBlock()) {
return
}
event.preventDefault()
const { html, text } = this.getClipBoradData()

View File

@ -1,5 +1,13 @@
import selection from '../selection'
const checkAutoIndent = (text, offset) => {
const pairStr = text.substring(offset - 1, offset + 1)
return /^(\{\}|\[\]|\(\)|><)$/.test(pairStr)
}
const getIndentSpace = text => {
return /^(\s*)\S/.exec(text)[1]
}
const enterCtrl = ContentState => {
ContentState.prototype.chopBlockByCursor = function (block, key, offset) {
const newBlock = this.createBlock('p')
@ -140,25 +148,15 @@ const enterCtrl = ContentState => {
const endBlock = this.getBlock(end.key)
let parent = this.getParent(block)
// handle cursor in code block
if (block.type === 'pre' && block.functionType === 'code') {
return
}
event.preventDefault()
// handle select multiple blocks
if (start.key !== end.key) {
const key = start.key
const offset = start.offset
const startRemainText = block.type === 'pre'
? block.text.substring(0, start.offset - 1)
: block.text.substring(0, start.offset)
const startRemainText = block.text.substring(0, start.offset)
const endRemainText = endBlock.type === 'pre'
? endBlock.text.substring(end.offset - 1)
: endBlock.text.substring(end.offset)
const endRemainText = endBlock.text.substring(end.offset)
block.text = startRemainText + endRemainText
@ -186,18 +184,31 @@ 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' && /frontmatter|multiplemath/.test(block.functionType))
(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', newLineText)
const newLine = this.createBlock('span', `${indent}${newLineText}`)
newLine.functionType = block.functionType
newLine.lang = block.lang
this.insertAfter(newLine, block)
const { key } = newLine
const offset = 0
let { key } = newLine
let offset = indent.length
if (autoIndent) {
const emptyLine = this.createBlock('span', indent + ' '.repeat(this.tabSize))
emptyLine.functionType = block.functionType
emptyLine.lang = block.lang
this.insertAfter(emptyLine, block)
key = emptyLine.key
offset = indent.length + this.tabSize
}
this.cursor = {
start: { key, offset },
end: { key, offset }
@ -374,7 +385,7 @@ const enterCtrl = ContentState => {
cursorBlock = tableNeedFocus
break
case !!htmlNeedFocus:
cursorBlock = htmlNeedFocus
cursorBlock = htmlNeedFocus.children[0].children[1] // the second line
break
case !!mathNeedFocus:
cursorBlock = mathNeedFocus

View File

@ -13,22 +13,12 @@ export class History {
this.index = this.index - 1
const state = deepCopy(this.stack[this.index])
switch (state.type) {
case 'normal':
const { blocks, cursor, renderRange } = state
cursor.noHistory = true
this.contentState.blocks = blocks
this.contentState.renderRange = renderRange
this.contentState.cursor = cursor
this.contentState.render()
break
case 'codeBlock':
const id = state.id
const codeBlock = this.contentState.codeBlocks.get(id)
codeBlock.focus()
codeBlock.undo()
break
}
const { blocks, cursor, renderRange } = state
cursor.noHistory = true
this.contentState.blocks = blocks
this.contentState.renderRange = renderRange
this.contentState.cursor = cursor
this.contentState.render()
}
}
@ -38,22 +28,12 @@ export class History {
if (index < len - 1) {
this.index = index + 1
const state = deepCopy(stack[this.index])
switch (state.type) {
case 'normal':
const { blocks, cursor, renderRange } = state
cursor.noHistory = true
this.contentState.blocks = blocks
this.contentState.renderRange = renderRange
this.contentState.cursor = cursor
this.contentState.render()
break
case 'codeBlock':
const id = state.id
const codeBlock = this.contentState.codeBlocks.get(id)
codeBlock.focus()
codeBlock.redo()
break
}
const { blocks, cursor, renderRange } = state
cursor.noHistory = true
this.contentState.blocks = blocks
this.contentState.renderRange = renderRange
this.contentState.cursor = cursor
this.contentState.render()
}
}

View File

@ -1,7 +1,7 @@
import { sanitize } from '../utils'
import { VOID_HTML_TAGS, HTML_TAGS, HTML_TOOLS, PREVIEW_DOMPURIFY_CONFIG } from '../config'
import { VOID_HTML_TAGS, HTML_TAGS, HTML_TOOLS } from '../config'
const HTML_BLOCK_REG = /^<([a-zA-Z\d-]+)(?=\s|>)[^<>]*?>$/
const LINE_BREAKS = /\n/
const htmlBlock = ContentState => {
ContentState.prototype.createToolBar = function (tools, toolBarType) {
@ -22,28 +22,36 @@ const htmlBlock = ContentState => {
return toolBar
}
ContentState.prototype.createCodeInHtml = function (code, selection) {
ContentState.prototype.createCodeInHtml = function (code) {
const codeContainer = this.createBlock('div')
codeContainer.functionType = 'html'
const preview = this.createBlock('div', '', false)
preview.htmlContent = sanitize(code, PREVIEW_DOMPURIFY_CONFIG)
preview.functionType = 'preview'
const codePre = this.createBlock('pre')
codePre.lang = 'html'
codePre.functionType = 'html'
codePre.text = code
if (selection) {
codePre.selection = selection
}
this.appendChild(codeContainer, codePre)
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.htmlToolBarClick = function (type) {
const { start: { key } } = this.cursor
const block = this.getBlock(key)
const codeBlockContainer = this.getParent(block)
const codeLine = this.getBlock(key)
const codeBlock = this.getParent(codeLine)
const preBlock = this.getParent(codeBlock)
const codeBlockContainer = this.getParent(preBlock)
const htmlBlock = this.getParent(codeBlockContainer)
switch (type) {
@ -79,7 +87,6 @@ const htmlBlock = ContentState => {
ContentState.prototype.createHtmlBlock = function (code) {
const block = this.createBlock('figure')
block.functionType = 'html'
block.text = code
const toolBar = this.createToolBar(HTML_TOOLS, 'html')
const htmlBlock = this.createCodeInHtml(code)
this.appendChild(block, toolBar)
@ -91,24 +98,15 @@ const htmlBlock = ContentState => {
const isVoidTag = VOID_HTML_TAGS.indexOf(tagName) > -1
const { text } = block.children[0]
const htmlContent = isVoidTag ? text : `${text}\n\n</${tagName}>`
const pos = {
line: isVoidTag ? 0 : 1,
ch: isVoidTag ? text.length : 0
}
const range = {
anchor: pos,
head: pos
}
block.type = 'figure'
block.functionType = 'html'
block.text = htmlContent
block.children = []
const toolBar = this.createToolBar(HTML_TOOLS, 'html')
const codeContainer = this.createCodeInHtml(htmlContent, range)
const codeContainer = this.createCodeInHtml(htmlContent)
this.appendChild(block, toolBar)
this.appendChild(block, codeContainer)
return codeContainer.children[0]
return codeContainer.children[0] // preBlock
}
ContentState.prototype.updateHtmlBlock = function (block) {

View File

@ -1,5 +1,4 @@
import { HAS_TEXT_BLOCK_REG, DEFAULT_TURNDOWN_CONFIG } from '../config'
import { setCursorAtLastLine } from '../codeMirror'
import { getUniqueId } from '../utils'
import selection from '../selection'
import StateRender from '../parser/render'
@ -21,6 +20,8 @@ import searchCtrl from './searchCtrl'
import mathCtrl from './mathCtrl'
import imagePathCtrl from './imagePathCtrl'
import htmlBlockCtrl from './htmlBlock'
import clickCtrl from './clickCtrl'
import inputCtrl from './inputCtrl'
import importMarkdown from '../utils/importMarkdown'
const prototypes = [
@ -41,12 +42,13 @@ const prototypes = [
mathCtrl,
imagePathCtrl,
htmlBlockCtrl,
clickCtrl,
inputCtrl,
importMarkdown
]
class ContentState {
constructor (muya, options) {
const { eventCenter } = muya
const { bulletListMarker } = options
this.muya = muya
@ -55,7 +57,7 @@ class ContentState {
// Use to cache the keys which you don't want to remove.
this.exemption = new Set()
this.blocks = [ this.createBlockP() ]
this.stateRender = new StateRender(eventCenter)
this.stateRender = new StateRender(muya)
this.codeBlocks = new Map()
this.renderRange = [ null, null ]
this.currentCursor = null
@ -69,22 +71,9 @@ class ContentState {
}
set cursor (cursor) {
// if (this.currentCursor) {
// const { start, end } = this.currentCursor
// if (
// start.key === cursor.start.key &&
// start.offset === cursor.start.offset &&
// end.key === cursor.end.key &&
// end.offset === cursor.end.offset
// ) {
// return
// }
// }
const handler = () => {
const { blocks, renderRange, currentCursor } = this
this.history.push({
type: 'normal',
blocks,
renderRange,
cursor: currentCursor
@ -136,34 +125,20 @@ class ContentState {
}
setCursor () {
const { start: { key } } = this.cursor
const block = this.getBlock(key)
if (block.type === 'pre' && /code|html/.test(block.functionType)) {
const cm = this.codeBlocks.get(key)
const { selection: codeSel } = block
if (codeSel) {
const { anchor, head } = codeSel
cm.focus()
cm.setSelection(anchor, head)
} else {
setCursorAtLastLine(cm)
}
} else {
selection.setCursorRange(this.cursor)
}
selection.setCursorRange(this.cursor)
}
setNextRenderRange () {
const { start, end } = this.cursor
// console.log(JSON.stringify(this.cursor, null, 2))
const startBlock = this.getBlock(start.key)
const endBlock = this.getBlock(end.key)
const startOutMostBlock = this.findOutMostBlock(startBlock)
const endOutMostBlock = this.findOutMostBlock(endBlock)
this.renderRange = [ startOutMostBlock.preSibling, endOutMostBlock.nextSibling ]
}
render (isRenderCursor = true, refreshCodeBlock = false) {
render (isRenderCursor = true) {
const { blocks, cursor, searchMatches: { matches, index } } = this
const activeBlocks = this.getActiveBlocks()
matches.forEach((m, i) => {
@ -171,15 +146,13 @@ class ContentState {
})
this.setNextRenderRange()
this.stateRender.collectLabels(blocks)
this.stateRender.render(blocks, cursor, activeBlocks, matches, refreshCodeBlock)
this.pre2CodeMirror(isRenderCursor)
this.stateRender.render(blocks, cursor, activeBlocks, matches)
if (isRenderCursor) this.setCursor()
}
partialRender () {
const { blocks, cursor, searchMatches: { matches, index } } = this
const activeBlocks = this.getActiveBlocks()
const cursorOutMostBlock = activeBlocks[activeBlocks.length - 1]
const [ startKey, endKey ] = this.renderRange
matches.forEach((m, i) => {
m.active = i === index
@ -191,7 +164,6 @@ class ContentState {
this.setNextRenderRange()
this.stateRender.collectLabels(blocks)
this.stateRender.partialRender(needRenderBlocks, cursor, activeBlocks, matches, startKey, endKey)
this.pre2CodeMirror(true, [...new Set([cursorOutMostBlock, ...needRenderBlocks])])
this.setCursor()
}
@ -299,6 +271,7 @@ class ContentState {
}
removeTextOrBlock (block) {
if (block.functionType === 'languageInput') return
const checkerIn = block => {
if (this.exemption.has(block.key)) {
return true
@ -382,7 +355,7 @@ class ContentState {
if (!afterEnd) {
const parent = this.getParent(after)
if (parent) {
const removeAfter = isRemoveAfter && this.isOnlyEditableChild(after)
const removeAfter = isRemoveAfter && (this.isOnlyRemoveableChild(after))
this.removeBlocks(before, parent, removeAfter, true)
}
}
@ -395,12 +368,6 @@ class ContentState {
}
removeBlock (block, fromBlocks = this.blocks) {
if (block.type === 'pre') {
const codeBlockId = block.key
if (this.codeBlocks.has(codeBlockId)) {
this.codeBlocks.delete(codeBlockId)
}
}
const remove = (blocks, block) => {
const len = blocks.length
let i
@ -531,11 +498,11 @@ class ContentState {
return !block.nextSibling && !block.preSibling
}
isOnlyEditableChild (block) {
isOnlyRemoveableChild (block) {
if (block.editable === false) return false
const parent = this.getParent(block)
if (!parent) throw new Error('isOnlyEditableChild method only apply for child block')
return parent.children.filter(child => child.editable).length === 1
if (!parent) throw new Error('isOnlyRemoveableChild method only apply for child block')
return parent.children.filter(child => child.editable && child.functionType !== 'languageInput').length === 1
}
getLastChild (block) {
@ -553,7 +520,11 @@ class ContentState {
if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
return block
} else if (children.length) {
if (children[0].type === 'input' || (children[0].type === 'div' && children[0].editable === false)) { // handle task item
if (
children[0].type === 'input' ||
(children[0].type === 'div' && children[0].editable === false) ||
(children[0].type === 'span' && children[0].functionType === 'languageInput')
) { // handle task item
return this.firstInDescendant(children[1])
} else {
return this.firstInDescendant(children[0])
@ -577,7 +548,13 @@ class ContentState {
findPreBlockInLocation (block) {
const parent = this.getParent(block)
const preBlock = this.getPreSibling(block)
if (block.preSibling && preBlock.type !== 'input' && preBlock.type !== 'div' && preBlock.editable !== false) { // handle task item and table
if (
block.preSibling &&
preBlock.type !== 'input' &&
preBlock.type !== 'div' &&
preBlock.editable !== false &&
preBlock.functionType !== 'languageInput'
) { // handle task item and table
return this.lastInDescendant(preBlock)
} else if (parent) {
return this.findPreBlockInLocation(parent)
@ -590,7 +567,9 @@ class ContentState {
const parent = this.getParent(block)
const nextBlock = this.getNextSibling(block)
if (nextBlock && nextBlock.editable !== false) {
if (
nextBlock && nextBlock.editable !== false
) {
return this.firstInDescendant(nextBlock)
} else if (parent) {
return this.findNextBlockInLocation(parent)
@ -627,7 +606,7 @@ class ContentState {
}
clear () {
this.codeBlocks.clear()
this.history.clearHistory()
}
}

View File

@ -1,6 +1,123 @@
import selection from '../selection'
import { getTextContent } from '../selection/dom'
import { beginRules } from '../parser/rules'
import { CLASS_OR_ID } from '../config'
const BRACKET_HASH = {
'{': '}',
'[': ']',
'(': ')',
'*': '*',
'_': '_',
'"': '"',
'\'': '\''
}
const inputCtrl = ContentState => {
// Input @ to quick insert paragraph
ContentState.prototype.checkQuickInsert = function (block) {
const { type, text, functionType } = block
if (type !== 'span' || functionType) return false
return /^@[a-zA-Z\d]*$/.test(text)
}
ContentState.prototype.inputHandler = function (event) {
// todo
const { start, end } = selection.getCursorRange()
const { start: oldStart, end: oldEnd } = this.cursor
const key = start.key
const block = this.getBlock(key)
const paragraph = document.querySelector(`#${key}`)
let text = getTextContent(paragraph, [ CLASS_OR_ID['AG_MATH_RENDER'] ])
let needRender = false
let needRenderAll = false
if (oldStart.key !== oldEnd.key) {
const startBlock = this.getBlock(oldStart.key)
const endBlock = this.getBlock(oldEnd.key)
this.removeBlocks(startBlock, endBlock)
needRender = true
}
// auto pair (not need to auto pair in math block)
if (block && block.text !== text) {
if (
start.key === end.key &&
start.offset === end.offset
) {
const { offset } = start
const { autoPairBracket, autoPairMarkdownSyntax, autoPairQuote } = this
const inputChar = text.charAt(+offset - 1)
const preInputChar = text.charAt(+offset - 2)
const postInputChar = text.charAt(+offset)
/* eslint-disable no-useless-escape */
if (
(event.inputType.indexOf('delete') === -1) &&
(inputChar === postInputChar) &&
(
(autoPairQuote && /[']{1}/.test(inputChar)) ||
(autoPairQuote && /["]{1}/.test(inputChar)) ||
(autoPairBracket && /[\}\]\)]{1}/.test(inputChar)) ||
(autoPairMarkdownSyntax && /[*_]{1}/.test(inputChar))
)
) {
text = text.substring(0, offset) + text.substring(offset + 1)
} else {
/* eslint-disable no-useless-escape */
// Not Unicode aware, since things like \p{Alphabetic} or \p{L} are not supported yet
if (
(autoPairQuote && /[']{1}/.test(inputChar) && !(/[a-zA-Z\d]{1}/.test(preInputChar))) ||
(autoPairQuote && /["]{1}/.test(inputChar)) ||
(autoPairBracket && /[\{\[\(]{1}/.test(inputChar)) ||
(block.functionType !== 'codeLine' && autoPairMarkdownSyntax && /[*_]{1}/.test(inputChar))
) {
needRender = true
text = BRACKET_HASH[event.data]
? text.substring(0, offset) + BRACKET_HASH[inputChar] + text.substring(offset)
: text
}
/* eslint-enable no-useless-escape */
if (/\s/.test(event.data) && preInputChar === '*' && postInputChar === '*') {
text = text.substring(0, offset) + text.substring(offset + 1)
}
}
}
block.text = text
if (beginRules['reference_definition'].test(text)) {
needRenderAll = true
}
}
// show quick insert
const rect = paragraph.getBoundingClientRect()
const checkQuickInsert = this.checkQuickInsert(block)
const reference = this.getPositionReference()
reference.getBoundingClientRect = function () {
const { x, y, left, top, height, bottom } = rect
return Object.assign({}, {
left,
x,
top,
y,
bottom,
height,
width: 0,
right: left
})
}
this.muya.eventCenter.dispatch('muya-quick-insert', reference, block, checkQuickInsert)
// Update preview content of math block
if (block && block.type === 'span' && block.functionType === 'codeLine') {
needRender = true
this.updateCodeBlocks(block)
}
this.cursor = { start, end }
const checkMarkedUpdate = this.checkNeedRender()
const inlineUpdatedBlock = this.isCollapse() && this.checkInlineUpdate(block)
if (checkMarkedUpdate || inlineUpdatedBlock || needRender) {
return needRenderAll ? this.render() : this.partialRender()
}
}
}

View File

@ -5,44 +5,54 @@ const mathCtrl = ContentState => {
ContentState.prototype.createMathBlock = function (value = '') {
const FUNCTION_TYPE = 'multiplemath'
const mathBlock = this.createBlock('figure')
const textArea = this.createBlock('pre')
const mathPreview = this.createBlock('div', '', false)
mathBlock.functionType = FUNCTION_TYPE
const { preBlock, mathPreview } = this.createMathAndPreview(value)
this.appendChild(mathBlock, preBlock)
this.appendChild(mathBlock, mathPreview)
this.codeBlocks.set(preBlock.key, value)
return mathBlock
}
ContentState.prototype.createMathAndPreview = function (value = '') {
const FUNCTION_TYPE = 'multiplemath'
const preBlock = this.createBlock('pre')
const codeBlock = this.createBlock('code')
preBlock.functionType = FUNCTION_TYPE
preBlock.lang = codeBlock.lang = 'latex'
this.appendChild(preBlock, codeBlock)
if (typeof value === 'string' && value) {
const lines = value.replace(/^\s+/, '').split(LINE_BREAKS_REG).map(line => this.createBlock('span', line))
for (const line of lines) {
line.functionType = FUNCTION_TYPE
this.appendChild(textArea, line)
}
value.replace(/^\s+/, '').split(LINE_BREAKS_REG).forEach(line => {
const codeLine = this.createBlock('span', line)
codeLine.functionType = 'codeLine'
codeLine.lang = 'latex'
this.appendChild(codeBlock, codeLine)
})
} else {
const emptyLine = this.createBlock('span')
emptyLine.functionType = FUNCTION_TYPE
this.appendChild(textArea, emptyLine)
emptyLine.functionType = 'codeLine'
emptyLine.lang = 'latex'
this.appendChild(codeBlock, emptyLine)
}
mathBlock.functionType = textArea.functionType = mathPreview.functionType = FUNCTION_TYPE
mathPreview.math = value
this.appendChild(mathBlock, textArea)
this.appendChild(mathBlock, mathPreview)
return mathBlock
const mathPreview = this.createBlock('div', '', false)
this.codeBlocks.set(preBlock.key, '')
mathPreview.functionType = FUNCTION_TYPE
return { preBlock, mathPreview }
}
ContentState.prototype.initMathBlock = function (block) { // p block
const FUNCTION_TYPE = 'multiplemath'
const textArea = this.createBlock('pre')
const emptyLine = this.createBlock('span')
textArea.functionType = emptyLine.functionType = FUNCTION_TYPE
this.appendChild(textArea, emptyLine)
block.type = 'figure'
block.functionType = FUNCTION_TYPE
block.children = []
const mathPreview = this.createBlock('div', '', false)
mathPreview.math = ''
mathPreview.functionType = FUNCTION_TYPE
const { preBlock, mathPreview } = this.createMathAndPreview()
this.appendChild(block, textArea)
this.appendChild(block, preBlock)
this.appendChild(block, mathPreview)
return emptyLine
return preBlock.children[0].children[0]
}
ContentState.prototype.handleMathBlockClick = function (mathFigure) {

View File

@ -17,7 +17,6 @@ const getCurrentLevel = type => {
const paragraphCtrl = ContentState => {
ContentState.prototype.selectionChange = function (cursor) {
const { fontSize, lineHeight } = this
const { start, end } = cursor || selection.getCursorRange()
const cursorCoords = selection.getCursorCoords()
const startBlock = this.getBlock(start.key)
@ -33,13 +32,6 @@ const paragraphCtrl = ContentState => {
end.type = endBlock.type
end.block = endBlock
if (start.type === 'pre' && end.type === 'pre' && startBlock.functionType !== 'frontmatter') {
const preElement = document.querySelector(`#${start.key}`)
const { top } = preElement.getBoundingClientRect()
const { line } = start.block.selection.anchor
cursorCoords.y = top + line * lineHeight * fontSize
}
return {
start,
end,
@ -73,10 +65,13 @@ const paragraphCtrl = ContentState => {
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')
emptyLine.functionType = 'frontmatter'
frontMatter.lang = codeBlock.lang = emptyLine.lang = 'yaml'
emptyLine.functionType = 'codeLine'
frontMatter.functionType = 'frontmatter'
this.appendChild(frontMatter, emptyLine)
this.appendChild(codeBlock, emptyLine)
this.appendChild(frontMatter, codeBlock)
this.insertBefore(frontMatter, firstBlock)
const { key } = emptyLine
const offset = 0
@ -197,70 +192,75 @@ const paragraphCtrl = ContentState => {
const startParents = this.getParents(startBlock)
const endParents = this.getParents(endBlock)
const hasFencedCodeBlockParent = () => {
return startParents.some(b => b.type === 'pre' && b.functionType === 'code') ||
endParents.some(b => b.type === 'pre' && b.functionType === 'code')
return startParents.some(b => b.type === 'pre' && /code/.test(b.functionType)) ||
endParents.some(b => b.type === 'pre' && /code/.test(b.functionType))
}
// change fenced code block to p paragraph
if (affiliation.length && affiliation[0].type === 'pre' && affiliation[0].functionType === 'code') {
const codeBlock = affiliation[0]
this.codeBlocks.delete(codeBlock.key)
codeBlock.type = 'p'
codeBlock.children = []
const lines = codeBlock.text.split(LINE_BREAKS_REG).map(line => this.createBlock('span', line))
for (const line of lines) {
this.appendChild(codeBlock, line)
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 = []
for (const line of codeLines) {
delete line.lang
delete line.functionType
this.appendChild(preBlock, line)
}
const { key } = codeBlock.children[codeBlock.selection.anchor.line]
const offset = codeBlock.selection.anchor.ch
delete codeBlock.selection
delete codeBlock.history
delete codeBlock.lang
delete codeBlock.coords
delete codeBlock.text
delete codeBlock.codeBlockStyle
delete codeBlock.functionType
delete preBlock.lang
delete preBlock.functionType
this.cursor = {
start: { key, offset },
end: { key, offset }
start: this.cursor.start,
end: this.cursor.end
}
} else {
if (start.key === end.key) {
if (startBlock.type === 'span') {
startBlock = this.getParent(startBlock)
startBlock.text = startBlock.children.map(line => line.text).join('\n')
const line = startBlock.children.findIndex(line => line.key === start.key)
const ch = start.offset
startBlock.selection = {
anchor: { line, ch },
head: { line, ch }
}
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)
})
this.appendChild(startBlock, inputBlock)
this.appendChild(startBlock, codeBlock)
}
const { key } = startBlock
const offset = 0
startBlock.type = 'pre'
startBlock.codeBlockStyle = 'fenced'
startBlock.functionType = 'code'
startBlock.history = null
startBlock.lang = ''
this.cursor = {
start: { key, offset },
end: { key, offset }
start: this.cursor.start,
end: this.cursor.end
}
} else if (!hasFencedCodeBlockParent()) {
const { parent, startIndex, endIndex } = this.getCommonParent()
const children = parent ? parent.children : this.blocks
const referBlock = children[endIndex]
const codeBlock = this.createBlock('pre')
codeBlock.codeBlockStyle = 'fenced'
codeBlock.functionType = 'code'
codeBlock.history = null
codeBlock.lang = ''
const preBlock = this.createBlock('pre')
const codeBlock = this.createBlock('code')
preBlock.functionType = 'fencecode'
preBlock.lang = codeBlock.lang = ''
const markdown = new ExportMarkdown(children.slice(startIndex, endIndex + 1)).generate()
codeBlock.text = markdown
this.insertAfter(codeBlock, referBlock)
markdown.split(LINE_BREAKS_REG).forEach(text => {
const codeLine = this.createBlock('span', text)
codeLine.lang = ''
codeLine.functionType = 'codeLine'
this.appendChild(codeBlock, codeLine)
})
const inputBlock = this.createBlock('span', '')
inputBlock.functionType = 'languageInput'
this.appendChild(preBlock, inputBlock)
this.appendChild(preBlock, codeBlock)
this.insertAfter(preBlock, referBlock)
let i
const removeCache = []
for (i = startIndex; i <= endIndex; i++) {
@ -268,7 +268,7 @@ const paragraphCtrl = ContentState => {
removeCache.push(child)
}
removeCache.forEach(b => this.removeBlock(b))
const key = codeBlock.key
const key = codeBlock.children[0].key
const offset = 0
this.cursor = {
start: { key, offset },
@ -353,8 +353,8 @@ const paragraphCtrl = ContentState => {
ContentState.prototype.insertHtmlBlock = function (block) {
const parentBlock = this.getParent(block)
block.text = '<div>'
const cursorBlock = this.initHtmlBlock(parentBlock, 'div')
const key = cursorBlock.key
const preBlock = this.initHtmlBlock(parentBlock, 'div')
const key = preBlock.children[0].children[1].key
const offset = 0
this.cursor = {
start: { key, offset },
@ -519,8 +519,30 @@ const paragraphCtrl = ContentState => {
// 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)
if (block.type === 'span') {
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))))

View File

@ -43,6 +43,7 @@ const pasteCtrl = ContentState => {
const sanitizedHtml = sanitize(html, PREVIEW_DOMPURIFY_CONFIG)
const tempWrapper = document.createElement('div')
tempWrapper.innerHTML = sanitizedHtml
// special process for Number app in macOs
const tables = Array.from(tempWrapper.querySelectorAll('table'))
for (const table of tables) {
const row = table.querySelector('tr')
@ -66,15 +67,11 @@ const pasteCtrl = ContentState => {
// handle `normal` and `pasteAsPlainText` paste
ContentState.prototype.pasteHandler = function (event, type) {
if (this.checkInCodeBlock()) {
return
}
event.preventDefault()
const text = event.clipboardData.getData('text/plain')
let html = event.clipboardData.getData('text/html')
html = this.standardizeHTML(html)
// console.log(text)
// console.log(html)
const copyType = this.checkCopyType(html, text)
const { start, end } = this.cursor
const startBlock = this.getBlock(start.key)
@ -95,6 +92,31 @@ const pasteCtrl = ContentState => {
}
}
if (startBlock.type === 'span' && startBlock.functionType === 'codeLine') {
let referenceBlock = startBlock
const textList = text.split(LINE_BREAKS_REG)
textList.forEach((line, i) => {
if (i === 0) {
startBlock.text += line
} else {
const lineBlock = this.createBlock('span', line)
lineBlock.functionType = startBlock.functionType
lineBlock.lang = startBlock.lang
this.insertAfter(lineBlock, referenceBlock)
referenceBlock = lineBlock
if (i === textList.length - 1) {
const { key } = lineBlock
const offset = line.length
this.cursor = {
start: { key, offset },
end: { key, offset }
}
}
}
})
return this.partialRender()
}
// handle copyAsHtml
if (copyType === 'copyAsHtml') {
switch (type) {
@ -196,6 +218,7 @@ const pasteCtrl = ContentState => {
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 (/^h\d$/.test(firstFragment.type)) {
@ -234,8 +257,8 @@ const pasteCtrl = ContentState => {
cursorBlock = startBlock
}
// TODO @Jocs duplicate with codes in updateCtrl.js
if (cursorBlock && cursorBlock.type === 'span' && cursorBlock.functionType === 'multiplemath') {
this.updateMathContent(cursorBlock)
if (cursorBlock && cursorBlock.type === 'span' && cursorBlock.functionType === 'codeLine') {
this.updateCodeBlocks(cursorBlock)
}
this.cursor = {
start: {

View File

@ -123,7 +123,7 @@ const tabCtrl = ContentState => {
ContentState.prototype.insertTab = function () {
const tabSize = this.tabSize
const tabCharacter = ' '.repeat(tabSize)
const tabCharacter = String.fromCharCode(160).repeat(tabSize)
const { start, end } = this.cursor
const startBlock = this.getBlock(start.key)
const endBlock = this.getBlock(end.key)

View File

@ -1,9 +1,6 @@
import selection from '../selection'
import { tokenizer } from '../parser/parse'
import { conflict } from '../utils'
import { getTextContent } from '../selection/dom'
import { CLASS_OR_ID, DEFAULT_TURNDOWN_CONFIG } from '../config'
import { beginRules } from '../parser/rules'
const INLINE_UPDATE_FRAGMENTS = [
'^([*+-]\\s)', // Bullet list
@ -16,8 +13,6 @@ const INLINE_UPDATE_FRAGMENTS = [
const INLINE_UPDATE_REG = new RegExp(INLINE_UPDATE_FRAGMENTS.join('|'), 'i')
let lastCursor = null
const updateCtrl = ContentState => {
// handle task list item checkbox click
ContentState.prototype.listItemCheckBoxClick = function (checkbox) {
@ -32,29 +27,41 @@ const updateCtrl = ContentState => {
return list.children[0].isLooseListItem === isLooseType
}
ContentState.prototype.checkNeedRender = function (block) {
const { start: cStart, end: cEnd } = this.cursor
ContentState.prototype.checkNeedRender = function (cursor = this.cursor) {
const { start: cStart, end: cEnd } = cursor
const startBlock = this.getBlock(cStart.key)
const endBlock = this.getBlock(cEnd.key)
const startOffset = cStart.offset
const endOffset = cEnd.offset
const tokens = tokenizer(block.text)
const textLen = block.text.length
for (const token of tokens) {
for (const token of tokenizer(startBlock.text)) {
if (token.type === 'text') continue
const { start, end } = token.range
const textLen = startBlock.text.length
if (
conflict([Math.max(0, start - 1), Math.min(textLen, end + 1)], [startOffset, startOffset])
) {
return true
}
}
for (const token of tokenizer(endBlock.text)) {
if (token.type === 'text') continue
const { start, end } = token.range
const textLen = endBlock.text.length
if (
conflict([Math.max(0, start - 1), Math.min(textLen, end + 1)], [startOffset, startOffset]) ||
conflict([Math.max(0, start - 1), Math.min(textLen, end + 1)], [endOffset, endOffset])
) {
return true
}
}
return false
}
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
// only first line block can update to other block
if (block.type === 'span' && block.preSibling) return false
if (block.type === 'span') {
@ -90,13 +97,6 @@ const updateCtrl = ContentState => {
}
}
// Input @ to quick insert paragraph
ContentState.prototype.checkQuickInsert = function (block) {
const { type, text, functionType } = block
if (type !== 'span' || functionType) return false
return /^@[a-zA-Z\d]*$/.test(text)
}
// thematic break
ContentState.prototype.updateHr = function (block, marker) {
if (block.type !== 'hr') {
@ -293,192 +293,11 @@ const updateCtrl = ContentState => {
return null
}
ContentState.prototype.updateMathContent = function (block) {
const preBlock = this.getParent(block)
const mathPreview = this.getNextSibling(preBlock)
const math = preBlock.children.map(line => line.text).join('\n')
mathPreview.math = math
}
ContentState.prototype.updateState = function (event) {
const { start, end } = selection.getCursorRange()
const key = start.key
const block = this.getBlock(key)
// bugfix: #67 problem 1
if (block && block.icon) return event.preventDefault()
if (event.type === 'click' && start.key !== end.key) {
setTimeout(() => {
this.updateState(event)
})
}
const { start: oldStart, end: oldEnd } = this.cursor
if (event.type === 'input' && oldStart.key !== oldEnd.key) {
const startBlock = this.getBlock(oldStart.key)
const endBlock = this.getBlock(oldEnd.key)
this.removeBlocks(startBlock, endBlock)
// there still has little bug, when the oldstart block is `pre`, the input value will be ignored.
// and act as `backspace`
if (startBlock.type === 'pre' && /code|html/.test(startBlock.functionType)) {
event.preventDefault()
const startRemainText = startBlock.type === 'pre'
? startBlock.text.substring(0, oldStart.offset - 1)
: startBlock.text.substring(0, oldStart.offset)
const endRemainText = endBlock.type === 'pre'
? endBlock.text.substring(oldEnd.offset - 1)
: endBlock.text.substring(oldEnd.offset)
startBlock.text = startRemainText + endRemainText
const key = oldStart.key
const offset = oldStart.offset
this.cursor = {
start: { key, offset },
end: { key, offset }
}
return this.partialRender()
}
}
if (start.key !== end.key) {
if (
start.key !== oldStart.key ||
end.key !== oldEnd.key ||
start.offset !== oldStart.offset ||
end.offset !== oldEnd.offset
) {
this.cursor = { start, end }
return this.partialRender()
}
}
const oldKey = lastCursor ? lastCursor.start.key : null
const paragraph = document.querySelector(`#${key}`)
let text = getTextContent(paragraph, [ CLASS_OR_ID['AG_MATH_RENDER'] ])
let needRender = false
let needRenderAll = false
if (event.type === 'click' && block.type === 'figure' && block.functionType === 'table') {
// first cell in thead
const cursorBlock = block.children[1].children[0].children[0].children[0]
const offset = cursorBlock.text.length
const key = cursorBlock.key
this.cursor = {
start: { key, offset },
end: { key, offset }
}
return this.partialRender()
}
// update '```xxx' to code block when you click other place or use press arrow key.
if (block && key !== oldKey) {
const oldBlock = this.getBlock(oldKey)
if (oldBlock) this.codeBlockUpdate(oldBlock)
}
if (block && block.type === 'pre' && /code|html/.test(block.functionType)) {
if (block.key !== oldKey) {
this.cursor = lastCursor = { start, end }
if (event.type === 'click' && oldKey) {
this.partialRender()
}
}
return
}
// auto pair (not need to auto pair in math block)
if (block && block.text !== text) {
const BRACKET_HASH = {
'{': '}',
'[': ']',
'(': ')',
'*': '*',
'_': '_',
'"': '"',
'\'': '\''
}
if (start.key === end.key && start.offset === end.offset && event.type === 'input' && block.functionType !== 'multiplemath') {
const { offset } = start
const { autoPairBracket, autoPairMarkdownSyntax, autoPairQuote } = this
const inputChar = text.charAt(+offset - 1)
const preInputChar = text.charAt(+offset - 2)
const postInputChar = text.charAt(+offset)
/* eslint-disable no-useless-escape */
if (
(event.inputType.indexOf('delete') === -1) &&
(inputChar === postInputChar) &&
(
(autoPairQuote && /[']{1}/.test(inputChar)) ||
(autoPairQuote && /["]{1}/.test(inputChar)) ||
(autoPairBracket && /[\}\]\)]{1}/.test(inputChar)) ||
(autoPairMarkdownSyntax && /[*_]{1}/.test(inputChar))
)
) {
text = text.substring(0, offset) + text.substring(offset + 1)
} else {
/* eslint-disable no-useless-escape */
// Not Unicode aware, since things like \p{Alphabetic} or \p{L} are not supported yet
if (
(autoPairQuote && /[']{1}/.test(inputChar) && !(/[a-zA-Z\d]{1}/.test(preInputChar))) ||
(autoPairQuote && /["]{1}/.test(inputChar)) ||
(autoPairBracket && /[\{\[\(]{1}/.test(inputChar)) ||
(autoPairMarkdownSyntax && /[*_]{1}/.test(inputChar))
) {
text = BRACKET_HASH[event.data]
? text.substring(0, offset) + BRACKET_HASH[inputChar] + text.substring(offset)
: text
}
/* eslint-enable no-useless-escape */
if (/\s/.test(event.data) && preInputChar === '*' && postInputChar === '*') {
text = text.substring(0, offset) + text.substring(offset + 1)
}
}
}
block.text = text
if (beginRules['reference_definition'].test(text)) {
needRenderAll = true
}
}
// Update preview content of math block
if (block && block.type === 'span' && block.functionType === 'multiplemath') {
this.updateMathContent(block)
}
if (oldKey !== key || oldStart.offset !== start.offset || oldEnd.offset !== end.offset) {
needRender = true
}
this.cursor = lastCursor = { start, end }
const checkMarkedUpdate = this.checkNeedRender(block)
if (event.type === 'input') {
const rect = paragraph.getBoundingClientRect()
const checkQuickInsert = this.checkQuickInsert(block)
const reference = this.getPositionReference()
reference.getBoundingClientRect = function () {
const { x, y, left, top, height, bottom } = rect
return Object.assign({}, {
left,
x,
top,
y,
bottom,
height,
width: 0,
right: left
})
}
this.muya.eventCenter.dispatch('muya-quick-insert', reference, block, checkQuickInsert)
}
const inlineUpdatedBlock = this.isCollapse() && !/frontmatter|multiplemath/.test(block.functionType) && this.checkInlineUpdate(block)
if (checkMarkedUpdate || inlineUpdatedBlock || needRender) {
needRenderAll ? this.render() : this.partialRender()
}
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)
}
}

View File

@ -12,10 +12,6 @@ class ClickEvent {
const { container, eventCenter, contentState } = this.muya
const handler = event => {
const { target } = event
// handler code block click.
if (target.tagName === 'PRE' && target.classList.contains(CLASS_OR_ID['AG_CODE_BLOCK'])) {
contentState.focusCodeBlock(event)
}
// handler table | html toolbar click
const toolItem = getToolItem(target)
if (toolItem) {
@ -56,13 +52,7 @@ class ClickEvent {
if (target.tagName === 'INPUT' && target.classList.contains(CLASS_OR_ID['AG_TASK_LIST_ITEM_CHECKBOX'])) {
contentState.listItemCheckBoxClick(target)
}
// is show format float box?
const { start, end } = selection.getCursorRange()
if (start.key === end.key && start.offset !== end.offset) {
const reference = contentState.getPositionReference()
const { formats } = contentState.selectionFormats()
eventCenter.dispatch('muya-format-picker', { reference, formats })
}
contentState.clickHandler(event)
}
eventCenter.attachDOMEvent(container, 'click', handler)

View File

@ -1,8 +1,8 @@
import { EVENT_KEYS, CLASS_OR_ID } from '../config'
import { EVENT_KEYS } from '../config'
import selection from '../selection'
import { findNearestParagraph } from '../selection/dom'
import { getParagraphReference } from '../utils'
import { checkEditLanguage } from '../codeMirror/language'
import { checkEditLanguage } from '../prism/index'
import { checkEditEmoji } from '../ui/emojis'
class Keyboard {
@ -11,7 +11,7 @@ class Keyboard {
this._isEditChinese = false
this.shownFloat = new Set()
this.recordEditChinese()
this.dispatchUpdateState()
this.dispatchEditorState()
this.keydownBinding()
this.keyupBinding()
this.inputBinding()
@ -39,32 +39,25 @@ class Keyboard {
eventCenter.attachDOMEvent(container, 'compositionstart', handler)
}
dispatchUpdateState () {
dispatchEditorState () {
const { container, eventCenter, contentState } = this.muya
let timer = null
const changeHandler = event => {
const target = event.target
if (event.type === 'click' && target.classList.contains(CLASS_OR_ID['AG_FUNCTION_HTML'])) return
if (event.type === 'keyup' && (event.key === EVENT_KEYS.ArrowUp || event.key === EVENT_KEYS.ArrowDown) && this.shownFloat.size > 0) return
if (!this._isEditChinese) {
contentState.updateState(event)
}
if (event.type === 'click' || event.type === 'keyup') {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
const selectionChanges = contentState.selectionChange()
const { formats } = contentState.selectionFormats()
eventCenter.dispatch('selectionChange', selectionChanges)
eventCenter.dispatch('selectionFormats', formats)
this.muya.dispatchChange()
})
}
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
const selectionChanges = contentState.selectionChange()
const { formats } = contentState.selectionFormats()
eventCenter.dispatch('selectionChange', selectionChanges)
eventCenter.dispatch('selectionFormats', formats)
this.muya.dispatchChange()
})
}
eventCenter.attachDOMEvent(container, 'click', changeHandler)
eventCenter.attachDOMEvent(container, 'keyup', changeHandler)
eventCenter.attachDOMEvent(container, 'input', changeHandler)
}
keydownBinding () {
@ -123,7 +116,7 @@ class Keyboard {
inputBinding () {
const { container, eventCenter, contentState } = this.muya
const inputHandler = _ => {
const inputHandler = event => {
const node = selection.getSelectionStart()
const paragraph = findNearestParagraph(node)
const selectionState = selection.exportSelection(paragraph)
@ -136,6 +129,12 @@ class Keyboard {
contentState.selectLanguage(paragraph, item.name)
}
})
} else {
// hide code picker float box
eventCenter.dispatch('muya-code-picker', { reference: null })
}
if (!this._isEditChinese) {
contentState.inputHandler(event)
}
}
@ -154,7 +153,9 @@ class Keyboard {
emojiNode &&
event.key !== EVENT_KEYS.Enter &&
event.key !== EVENT_KEYS.ArrowDown &&
event.key !== EVENT_KEYS.ArrowUp
event.key !== EVENT_KEYS.ArrowUp &&
event.key !== EVENT_KEYS.Tab &&
event.key !== EVENT_KEYS.Escape
) {
const reference = getParagraphReference(emojiNode, paragraph.id)
eventCenter.dispatch('muya-emoji-picker', {
@ -169,7 +170,8 @@ class Keyboard {
}
// is show format float box?
const { start, end } = selection.getCursorRange()
if (start.key === end.key && start.offset !== end.offset) {
const block = contentState.getBlock(start.key)
if (start.key === end.key && start.offset !== end.offset && block.functionType !== 'codeLine') {
const reference = contentState.getPositionReference()
const { formats } = contentState.selectionFormats()
eventCenter.dispatch('muya-format-picker', { reference, formats })

View File

@ -3,7 +3,7 @@ import EventCenter from './eventHandler/event'
import Clipboard from './eventHandler/clipboard'
import Keyboard from './eventHandler/keyboard'
import ClickEvent from './eventHandler/clickEvent'
import { CLASS_OR_ID, codeMirrorConfig } from './config'
import { CLASS_OR_ID } from './config'
import { wordCount } from './utils'
import ExportMarkdown from './utils/exportMarkdown'
import ExportHtml from './utils/exportHtml'
@ -137,14 +137,10 @@ class Muya {
setTheme (name) {
if (!name) return
const { eventCenter } = this
if (name === 'dark') {
codeMirrorConfig.theme = 'railscasts'
} else {
delete codeMirrorConfig.theme
}
this.theme = name
// Render cursor and refresh code block
this.contentState.render(true, true)
this.contentState.render(true)
// notice the ui components to change theme
eventCenter.dispatch('theme-change', name)
}

View File

@ -7,9 +7,9 @@ import renderInlines from './renderInlines'
import renderBlock from './renderBlock'
class StateRender {
constructor (eventCenter) {
this.eventCenter = eventCenter
this.refreshCodeBlock = false
constructor (muya) {
this.muya = muya
this.eventCenter = muya.eventCenter
this.loadImageMap = new Map()
this.loadMathMap = new Map()
this.tokenCache = new Map()
@ -81,14 +81,10 @@ class StateRender {
if (type === 'span') {
selector += `.${CLASS_OR_ID['AG_LINE']}`
}
if (block.temp) {
selector += `.${CLASS_OR_ID['AG_TEMP']}`
}
return selector
}
render (blocks, cursor, activeBlocks, matches, refreshCodeBlock) {
this.refreshCodeBlock = refreshCodeBlock
render (blocks, cursor, activeBlocks, matches) {
const selector = `div#${CLASS_OR_ID['AG_EDITOR_ID']}`
const children = blocks.map(block => {

View File

@ -1,6 +1,14 @@
import { CLASS_OR_ID } from '../../../config'
import { h } from '../snabbdom'
const PRE_BLOCK_HASH = {
'fencecode': `.${CLASS_OR_ID['AG_FENCE_CODE']}`,
'indentcode': `.${CLASS_OR_ID['AG_INDENT_CODE']}`,
'html': `.${CLASS_OR_ID['AG_HTML_BLOCK']}`,
'frontmatter': `.${CLASS_OR_ID['AG_FRONT_MATTER']}`,
'multiplemath': `.${CLASS_OR_ID['AG_MULTIPLE_MATH']}`
}
export default function renderContainerBlock (block, cursor, activeBlocks, matches, useCache = false) {
let selector = this.getSelector(block, cursor, activeBlocks)
const data = {
@ -76,11 +84,19 @@ export default function renderContainerBlock (block, cursor, activeBlocks, match
if (block.type === 'ol') {
Object.assign(data.attrs, { start: block.start })
}
if (block.type === 'pre' && /frontmatter|multiplemath/.test(block.functionType)) {
const role = block.functionType === 'frontmatter' ? 'YAML' : 'MATH'
const className = block.functionType === 'frontmatter' ? CLASS_OR_ID['AG_FRONT_MATTER'] : CLASS_OR_ID['AG_MULTIPLE_MATH']
Object.assign(data.dataset, { role })
selector += `.${className}`
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}`
}
Object.assign(data.dataset, { role: functionType })
selector += PRE_BLOCK_HASH[block.functionType]
}
return h(selector, data, block.children.map(child => this.renderBlock(child, cursor, activeBlocks, matches, useCache)))

View File

@ -1,17 +1,28 @@
import katex from 'katex'
import { CLASS_OR_ID, DEVICE_MEMORY, isInElectron } from '../../../config'
import prism, { loadedCache } from '../../../prism/'
import { CLASS_OR_ID, DEVICE_MEMORY, isInElectron, PREVIEW_DOMPURIFY_CONFIG } from '../../../config'
import { tokenizer } from '../../parse'
import { snakeToCamel } from '../../../utils'
import { snakeToCamel, sanitize, escapeHtml } from '../../../utils'
import { h, htmlToVNode } from '../snabbdom'
const PRE_BLOCK_HASH = {
'code': `.${CLASS_OR_ID['AG_CODE_BLOCK']}`,
'html': `.${CLASS_OR_ID['AG_HTML_BLOCK']}`,
'frontmatter': `.${CLASS_OR_ID['AG_FRONT_MATTER']}`
const getHighlightHtml = (text, highlights) => {
let code = ''
let pos = 0
for (const highlight of highlights) {
const { start, end, active } = highlight
code += text.substring(pos, start)
const className = active ? 'ag-highlight' : 'ag-selection'
code += `<span class="${className}">${text.substring(start, end)}</span>`
pos = end
}
if (pos !== text.length) {
code += text.substring(pos)
}
return code
}
export default function renderLeafBlock (block, cursor, activeBlocks, matches, useCache = false) {
const { loadMathMap, refreshCodeBlock } = this
const { loadMathMap } = this
let selector = this.getSelector(block, cursor, activeBlocks)
// highlight search key in block
const highlights = matches.filter(m => m.key === block.key)
@ -20,17 +31,15 @@ export default function renderLeafBlock (block, cursor, activeBlocks, matches, u
type,
headingStyle,
align,
htmlContent,
icon,
checked,
key,
lang,
functionType,
codeBlockStyle,
math,
editable
} = block
const data = {
props: {},
attrs: {},
dataset: {}
}
@ -57,15 +66,17 @@ export default function renderLeafBlock (block, cursor, activeBlocks, matches, u
style: `text-align:${align}`
})
} else if (type === 'div') {
if (typeof htmlContent === 'string') {
if (functionType === 'preview') {
selector += `.${CLASS_OR_ID['AG_HTML_PREVIEW']}`
const htmlContent = sanitize(this.muya.contentState.codeBlocks.get(block.preSibling), PREVIEW_DOMPURIFY_CONFIG)
// handle empty html bock
if (/<([a-z][a-z\d]*).*>\s*<\/\1>/.test(htmlContent)) {
children = htmlToVNode('<div class="ag-empty">&lt;Empty HTML Block&gt;</div>')
} else {
children = htmlToVNode(htmlContent)
}
} else if (typeof math === 'string') {
} else if (functionType === 'multiplemath') {
const math = this.muya.contentState.codeBlocks.get(block.preSibling)
const key = `${math}_display_math`
selector += `.${CLASS_OR_ID['AG_MATH_PREVIEW']}`
if (math === '') {
@ -122,42 +133,32 @@ export default function renderLeafBlock (block, cursor, activeBlocks, matches, u
selector += `.${CLASS_OR_ID['AG_CHECKBOX_CHECKED']}`
}
children = ''
} else if (type === 'pre') {
selector += `.${CLASS_OR_ID['AG_CODEMIRROR_BLOCK']}`
selector += PRE_BLOCK_HASH[functionType]
Object.assign(data.attrs, { contenteditable: 'false' })
data.hook = {
prepatch (oldvnode, vnode) {
// cheat snabbdom that the pre block is not changed!!!
if (!refreshCodeBlock) {
vnode.children = oldvnode.children
}
}
}
if (lang) {
Object.assign(data.dataset, {
lang
})
} else if (type === 'span' && functionType === 'codeLine') {
let code
if (lang && lang === 'markup') {
code = getHighlightHtml(escapeHtml(text), highlights)
} else {
code = getHighlightHtml(text, highlights)
}
if (codeBlockStyle) {
Object.assign(data.dataset, {
codeBlockStyle
})
}
selector += `.${CLASS_OR_ID['AG_CODE_LINE']}`
if (/code|html/.test(functionType)) {
// do not set it to '' (empty string)
children = []
if (lang && /\S/.test(code) && loadedCache.has(lang)) {
const wrapper = document.createElement('div')
wrapper.classList.add(`language-${lang}`)
wrapper.innerHTML = code
prism.highlightElement(wrapper, false, function () {
const highlightedCode = this.innerHTML
selector += `.language-${lang}`
children = htmlToVNode(highlightedCode)
})
} else {
children = htmlToVNode(code)
}
} else if (type === 'span' && /frontmatter|multiplemath/.test(functionType)) {
if (functionType === 'frontmatter') {
selector += `.${CLASS_OR_ID['AG_FRONT_MATTER_LINE']}`
}
if (functionType === 'multiplemath') {
selector += `.${CLASS_OR_ID['AG_MULTIPLE_MATH_LINE']}`
}
children = text
} else if (type === 'span' && functionType === 'languageInput') {
const html = getHighlightHtml(text, highlights)
selector += `.${CLASS_OR_ID['AG_LANGUAGE_INPUT']}`
children = htmlToVNode(html)
}
return h(selector, data, children)

View File

@ -1,4 +1,4 @@
import virtualize from 'snabbdom-virtualize/strings'
// import virtualize from 'snabbdom-virtualize/strings'
const snabbdom = require('snabbdom')
export const patch = snabbdom.init([ // Init patch function with chosen modules
@ -10,6 +10,10 @@ export const patch = snabbdom.init([ // Init patch function with chosen modules
require('snabbdom/modules/eventlisteners').default // attaches event listeners
])
export const h = require('snabbdom/h').default // helper function for creating vnodes
export const toHTML = require('snabbdom-to-html') // helper function for convert DOM to HTML string
export const toHTML = require('snabbdom-to-html') // helper function for convert vnode to HTML string
export const toVNode = require('snabbdom/tovnode').default // helper function for convert DOM to vnode
export const htmlToVNode = virtualize // helper function for convert HTML string to vnode
export const htmlToVNode = html => { // helper function for convert html to vnode
const wrapper = document.createElement('div')
wrapper.innerHTML = html
return toVNode(wrapper).children
}

View File

@ -0,0 +1,49 @@
import Prism from 'prismjs2'
import { filter } from 'fuzzaldrin'
import initLoadLanguage, { loadedCache } from './loadLanguage'
import languages from './languages'
const prism = Prism
window.Prism = Prism
import('prismjs2/plugins/keep-markup/prism-keep-markup')
const langs = Object.keys(languages).map(name => (languages[name]))
const loadLanguage = initLoadLanguage(Prism)
/**
* check edit language
*/
const checkEditLanguage = (paragraph, selectionState) => {
const text = paragraph.textContent
const { start } = selectionState
const token = text.match(/(^`{3,})([^`]+)/)
if (paragraph.tagName !== 'SPAN') return false
if (token) {
const len = token[1].length
const lang = token[2].trim()
if (start < len) return false
if (!lang) return false
return lang
} else if (paragraph.classList.contains('ag-language-input')) {
return text.trim()
} else {
return false
}
}
const search = text => {
return filter(langs, text, { key: 'name' })
}
// pre load latex and yaml and html for `math block` \ `front matter` and `html block`
loadLanguage('latex')
loadLanguage('yaml')
export {
search,
loadLanguage,
loadedCache,
languages,
checkEditLanguage
}
export default prism

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,87 @@
import languages from './languages'
let peerDependentsMap = null
export const loadedCache = new Set(['markup', 'css', 'clike', 'javascript'])
function getPeerDependentsMap () {
const peerDependentsMap = {}
Object.keys(languages).forEach(function (language) {
if (language === 'meta') {
return false
}
if (languages[language].peerDependencies) {
let peerDependencies = languages[language].peerDependencies
if (!Array.isArray(peerDependencies)) {
peerDependencies = [peerDependencies]
}
peerDependencies.forEach(function (peerDependency) {
if (!peerDependentsMap[peerDependency]) {
peerDependentsMap[peerDependency] = []
}
peerDependentsMap[peerDependency].push(language)
})
}
})
return peerDependentsMap
}
function getPeerDependents (mainLanguage) {
if (!peerDependentsMap) {
peerDependentsMap = getPeerDependentsMap()
}
return peerDependentsMap[mainLanguage] || []
}
function initLoadLanguage (Prism) {
return function loadLanguages (arr, withoutDependencies) {
// If no argument is passed, load all components
if (!arr) {
arr = Object.keys(languages).filter(function (language) {
return language !== 'meta'
})
}
if (arr && !arr.length) {
return
}
if (!Array.isArray(arr)) {
arr = [arr]
}
arr.forEach(function (language) {
if (!languages[language]) {
console.warn('Language does not exist ' + language)
return
}
if (loadedCache.has(language)) {
return
}
// Load dependencies first
if (!withoutDependencies && languages[language].require) {
loadLanguages(languages[language].require)
}
delete Prism.languages[language]
import('prismjs2/components/prism-' + language)
.then(_ => {
loadedCache.add(language)
})
// Reload dependents
const dependents = getPeerDependents(language).filter(function (dependent) {
// If dependent language was already loaded,
// we want to reload it.
if (Prism.languages[dependent]) {
delete Prism.languages[dependent]
return true
}
return false
})
if (dependents.length) {
loadLanguages(dependents, true)
}
})
}
}
export default initLoadLanguage

View File

@ -842,7 +842,6 @@ class Selection {
if (range.getClientRects) {
range.collapse(true)
const rects = range.getClientRects()
if (rects.length) {
const { left, top, x: rectX, y: rectY } = rects[0]
x = rectX || left

View File

@ -1,6 +1,6 @@
import BaseScrollFloat from '../baseScrollFloat'
import { patch, h } from '../../parser/render/snabbdom'
import { search } from '../../codeMirror'
import { search } from '../../prism/index'
import fileIcons from '../fileIcons'
import './index.css'
@ -19,10 +19,8 @@ class CodePicker extends BaseScrollFloat {
super.listen()
const { eventCenter } = this.muya
eventCenter.subscribe('muya-code-picker', ({ reference, lang, cb }) => {
const modes = search(lang).map(mode => {
return Object.assign(mode, { text: mode.name })
})
if (modes.length) {
const modes = search(lang)
if (modes.length && reference) {
this.show(reference, cb)
this.renderArray = modes
this.activeItem = modes[0]
@ -37,19 +35,19 @@ class CodePicker extends BaseScrollFloat {
const { renderArray, oldVnode, scrollElement, activeItem } = this
let children = renderArray.map(item => {
let iconClassNames
if (item.mode.ext && Array.isArray(item.mode.ext)) {
for (const ext of item.mode.ext) {
if (item.ext && Array.isArray(item.ext)) {
for (const ext of item.ext) {
iconClassNames = fileIcons.getClassWithColor(`fackname.${ext}`)
if (iconClassNames) break
}
} else if (item.mode.name) {
iconClassNames = fileIcons.getClassWithColor(item.mode.name)
} else if (item.name) {
iconClassNames = fileIcons.getClassWithColor(item.name)
}
// Because `markdown mode in Codemirror` don't have extensions.
// if still can not get the className, add a common className 'atom-icon light-cyan'
if (!iconClassNames) {
iconClassNames = item.text === 'markdown' ? fileIcons.getClassWithColor('fackname.md') : 'atom-icon light-cyan'
iconClassNames = item.name === 'markdown' ? fileIcons.getClassWithColor('fackname.md') : 'atom-icon light-cyan'
}
const iconSelector = 'span' + iconClassNames.split(/\s/).map(s => `.${s}`).join('')
const icon = h('div.icon-wrapper', h(iconSelector))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -29,11 +29,8 @@ export const validEmoji = text => {
*/
export const checkEditEmoji = node => {
const preSibling = node.previousElementSibling
if (node.classList.contains(CLASS_OR_ID['AG_EMOJI_MARKED_TEXT'])) {
return node
} else if (preSibling && preSibling.classList.contains(CLASS_OR_ID['AG_EMOJI_MARKED_TEXT'])) {
return preSibling
}
return false
}

View File

@ -1,8 +1,8 @@
import marked from '../parser/marked'
import highlight from 'highlight.js'
import Prism from 'prismjs2'
import katex from 'katex'
import githubMarkdownCss from 'github-markdown-css/github-markdown.css'
import highlightCss from 'highlight.js/styles/default.css'
import highlightCss from 'prismjs2/themes/prism.css'
import katexCss from 'katex/dist/katex.css'
import { EXPORT_DOMPURIFY_CONFIG } from '../config'
import { sanitize } from '../utils'
@ -20,8 +20,8 @@ class ExportHtml {
// render pure html by marked
renderHtml () {
return marked(this.markdown, {
highlight (code) {
return highlight.highlightAuto(code).value
highlight (code, lang) {
return Prism.highlight(code, Prism.languages[lang], lang)
},
emojiRenderer (emoji) {
const validate = validEmoji(emoji)

View File

@ -7,7 +7,7 @@
* and GitHub Flavored Markdown Spec: https://github.github.com/gfm/
* The output markdown needs to obey the standards of the two Spec.
*/
const LINE_BREAKS = /\n/
// const LINE_BREAKS = /\n/
class ExportMarkdown {
constructor (blocks) {
@ -153,10 +153,10 @@ class ExportMarkdown {
return this.translateBlocks2Markdown(children, newIndent)
}
normalizeFrontMatter (block, indent) {
normalizeFrontMatter (block, indent) { // preBlock
const result = []
result.push('---\n')
for (const line of block.children) {
for (const line of block.children[0].children) {
result.push(`${line.text}\n`)
}
result.push('---\n')
@ -166,7 +166,7 @@ class ExportMarkdown {
normalizeMultipleMath (block, /* figure */ indent) {
const result = []
result.push('$$\n')
for (const line of block.children[0].children) {
for (const line of block.children[0].children[0].children) {
result.push(`${line.text}\n`)
}
result.push('$$\n')
@ -175,9 +175,9 @@ class ExportMarkdown {
normalizeCodeBlock (block, indent) {
const result = []
const textList = block.text.split(LINE_BREAKS)
const { codeBlockStyle } = block
if (codeBlockStyle === 'fenced') {
const textList = block.children[1].children.map(codeLine => codeLine.text)
const { functionType } = block
if (functionType === 'fencecode') {
result.push(`${indent}${block.lang ? '```' + block.lang + '\n' : '```\n'}`)
textList.forEach(text => {
result.push(`${indent}${text}\n`)
@ -192,12 +192,11 @@ class ExportMarkdown {
return result.join('')
}
normalizeHTML (block, indent) {
normalizeHTML (block, indent) { // figure
const result = []
const codeContent = block.children[1].children[0].text
const textList = codeContent.split(LINE_BREAKS)
for (const text of textList) {
result.push(`${indent}${text}\n`)
const codeLines = block.children[1].children[0].children[0].children
for (const line of codeLines) {
result.push(`${indent}${line.text}\n`)
}
return result.join('')
}

View File

@ -6,6 +6,7 @@
import { Lexer } from '../parser/marked'
import ExportMarkdown from './exportMarkdown'
import TurndownService, { usePluginAddRules } from './turndownService'
import { loadLanguage } from '../prism/index'
// To be disabled rules when parse markdown, Because content state don't need to parse inline rules
import { CURSOR_DNA, TABLE_TOOLS } from '../config'
@ -27,7 +28,6 @@ const importRegister = ContentState => {
}
const tokens = new Lexer({ disableInline: true }).lex(markdown)
console.log(JSON.stringify(tokens, null, 2))
let token
let block
@ -39,15 +39,21 @@ const importRegister = ContentState => {
case 'frontmatter': {
value = token.text
block = this.createBlock('pre')
const lines = value
const codeBlock = this.createBlock('code')
value
.replace(/^\s+/, '')
.replace(/\s$/, '')
.split(LINE_BREAKS_REG).map(line => this.createBlock('span', line))
for (const line of lines) {
line.functionType = token.type
this.appendChild(block, line)
}
.split(LINE_BREAKS_REG).forEach(line => {
const codeLine = this.createBlock('span', line)
codeLine.functionType = 'codeLine'
codeLine.lang = 'yaml'
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
}
@ -72,14 +78,29 @@ const importRegister = ContentState => {
break
}
case 'code': {
const { codeBlockStyle, text, lang, type } = token
const { codeBlockStyle, text, lang = '' } = token
value = text
if (value.endsWith('\n')) {
value = value.replace(/\n+$/, '')
}
block = this.createBlock('pre', value)
block.functionType = type
Object.assign(block, { lang, codeBlockStyle })
block = this.createBlock('pre')
const codeBlock = this.createBlock('code')
value.split(LINE_BREAKS_REG).forEach(line => {
const codeLine = this.createBlock('span', line)
codeLine.lang = lang
codeLine.functionType = 'codeLine'
this.appendChild(codeBlock, codeLine)
})
const inputBlock = this.createBlock('span', lang)
if (lang) {
loadLanguage(lang)
}
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)
break
}
@ -249,12 +270,14 @@ const importRegister = ContentState => {
end: { key, offset }
}
// handle cursor in Math block, need to remove `CURSOR_DNA` in preview block
if (type === 'span' && functionType === 'multiplemath') {
const mathPreview = this.getNextSibling(this.getParent(block))
const { math } = mathPreview
const offset = math.indexOf(CURSOR_DNA)
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) {
mathPreview.math = math.substring(0, offset) + math.substring(offset + CURSOR_DNA.length)
const newCode = code.substring(0, offset) + code.substring(offset + CURSOR_DNA.length)
this.codeBlocks.set(preBlock.key, newCode)
}
}
return
@ -279,7 +302,6 @@ const importRegister = ContentState => {
}
ContentState.prototype.importMarkdown = function (markdown) {
// empty the blocks and codeBlocks
this.codeBlocks = new Map()
this.blocks = this.markdownToState(markdown)
}

View File

@ -1,4 +1,5 @@
// DOTO: Don't use Node API in editor folder, remove `path` @jocs
// todo@jocs: remove the use of `axios` in muya
import axios from 'axios'
import createDOMPurify from 'dompurify'
@ -161,17 +162,6 @@ export const checkImageContentType = async url => {
}
}
export const collectImportantComments = css => {
const once = new Set()
const cleaned = css.replace(/(\/\*![\s\S]*?\*\/)\n*/gm, (match, p1) => {
once.add(p1)
return ''
})
const combined = Array.from(once)
combined.push(cleaned)
return combined.join('\n')
}
export const getImageInfo = src => {
const EXT_REG = /\.(jpeg|jpg|png|gif|svg|webp)(?=\?|$)/i
// http[s] (domain or IPv4 or localhost or IPv6) [port] /not-white-space

View File

@ -64,11 +64,6 @@ body {
caret-color: #efefef;
}
#ag-editor-id>ul:first-child,
#ag-editor-id>ol:first-child {
margin-top: 30px;
}
.ag-float-box {
background: #303133;
border: 1px solid #303133;
@ -350,26 +345,13 @@ table tr td:last-child {
margin-bottom: 0;
}
.CodeMirror-gutters {
border-right: 1px solid #ddd;
}
code,
tt {
border: 1px solid #ddd;
background-color: #606266;
border-radius: 3px;
padding: 0;
font-family: Consolas, "Liberation Mono", Courier, monospace;
padding: 2px 4px 0px 4px;
font-size: 0.9em;
}
/* custom add */
code {
span.ag-line code,
th code,
td code {
border: none;
padding: 2px 4px;
border-radius: 3px;
font-size: 90%;
color: #efefef;
background-color: #606266;
@ -428,7 +410,6 @@ code {
border-color: #333;
}
#ag-editor-id pre.ag-code-block,
#ag-editor-id pre.ag-html-block {
font-size: 90%;
line-height: 1.6;
@ -436,11 +417,134 @@ code {
color: #777777;
}
#ag-editor-id pre.ag-code-block .cm-s-railscasts.CodeMirror,
#ag-editor-id pre.ag-html-block .cm-s-railscasts.CodeMirror {
background: var(--primaryColor);
}
.ag-color-dark {
color: #c6c6c6;
}
/**
* okaidia theme for JavaScript, CSS and HTML
* Loosely based on Monokai textmate theme by http://www.monokai.nl/
* @author ocodia
*/
code[class*="language-"],
pre.ag-paragraph {
color: #f8f8f2;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
overflow: visible;
}
/* Code blocks */
pre.ag-paragraph {
padding: 1em;
margin: 1em 0;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre.ag-paragraph {
background: #272822;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #f8f8f2;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol {
color: #f92672;
}
.token.boolean,
.token.number {
color: #ae81ff;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin {
color: #a6e22e;
}
.token.inserted {
color: #22863a;
background: #f0fff4;
}
.token.deleted {
color: #b31d28;
background: #ffeef0;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: #f8f8f2;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.class-name {
color: #e6db74;
}
.token.keyword {
color: #66d9ef;
}
.token.regex,
.token.important {
color: #fd971f;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@ -4,28 +4,28 @@
font-family: 'Open Sans';
font-style: normal;
font-weight: normal;
src: local('Open Sans Regular'),url('./github/400.woff') format('woff')
src: local('Open Sans Regular'),url('./github/400.woff') format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: normal;
src: local('Open Sans Italic'),url('./github/400i.woff') format('woff')
src: local('Open Sans Italic'),url('./github/400i.woff') format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: bold;
src: local('Open Sans Bold'),url('./github/700.woff') format('woff')
src: local('Open Sans Bold'),url('./github/700.woff') format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: bold;
src: local('Open Sans Bold Italic'),url('./github/700i.woff') format('woff')
src: local('Open Sans Bold Italic'),url('./github/700i.woff') format('woff');
}
html, body {
@ -65,11 +65,6 @@ body {
caret-color: #000000;
}
#ag-editor-id>ul:first-child,
#ag-editor-id>ol:first-child {
margin-top: 30px;
}
.ag-gray {
color: #C0C4CC;
text-decoration: none;
@ -322,32 +317,28 @@ table tr td:last-child {
margin-bottom: 0;
}
.CodeMirror-gutters {
border-right: 1px solid #ddd;
}
code,
tt {
border: 1px solid #ddd;
background-color: #f8f8f8;
span code,
td code,
th code {
background-color: rgba(27, 31, 35, 0.05);
border-radius: 3px;
padding: 0;
font-family: Consolas, "Liberation Mono", Courier, monospace;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
padding: 2px 4px 0px 4px;
font-size: 0.9em;
font-size: 85%;
margin: 0;
padding: 0.2em 0.4em;
color: #24292e;
}
/* custom add */
code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(27,31,35,0.05);
:not(pre) > code[class*="language-"],
pre {
font-size: 90%;
line-height: 1.6;
background: #f6f8fa;
border: 0;
border-radius: 3px;
border: none;
color: #24292e;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
color: #777777;
}
@media print {
@ -376,16 +367,6 @@ code {
border-bottom-color: #333333;
}
#ag-editor-id pre.ag-code-block,
#ag-editor-id pre.ag-html-block {
font-size: 90%;
line-height: 1.6;
background: #f6f8fa;
border: 0;
border-radius: 3px;
color: #777777;
}
#ag-editor-id pre.ag-html-block {
background: transparent;
padding: 0 .5rem;
@ -403,3 +384,116 @@ p:not(.ag-active)[data-role="hr"]::before {
.fg-color-dark {
color: #303133;
}
/* prismjs default theme */
code[class*="language-"],
pre.ag-paragraph {
color: black;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre.ag-paragraph {
padding: 1em;
margin: 1em 0;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin {
color: #690;
}
.token.inserted {
color: #22863a;
background: #f0fff4;
}
.token.deleted {
color: #b31d28;
background: #ffeef0;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@ -240,7 +240,6 @@
this.editor.on('selectionChange', changes => {
const { y } = changes.cursorCoords
if (this.typewriter) {
animatedScrollTo(container, container.scrollTop + y - STANDAR_Y, 100)
}

View File

@ -8,7 +8,7 @@
</template>
<script>
import codeMirror, { setMode, setCursorAtLastLine, setTextDirection } from 'muya/lib/codeMirror'
import codeMirror, { setMode, setCursorAtLastLine, setTextDirection } from '../../codeMirror'
import { wordCount as getWordCount } from 'muya/lib/utils'
import { adjustCursor } from '../../util'
import bus from '../../bus'