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

View File

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

88
package-lock.json generated
View File

@ -192,6 +192,12 @@
"integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==", "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==",
"dev": true "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": { "@vue/component-compiler-utils": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-2.2.0.tgz", "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-2.2.0.tgz",
@ -2860,6 +2866,17 @@
"integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
"dev": true "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": { "cliui": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", "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", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" "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": { "depd": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@ -4167,22 +4190,14 @@
"dev": true "dev": true
}, },
"electron": { "electron": {
"version": "3.0.3", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/electron/-/electron-3.0.3.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-3.0.5.tgz",
"integrity": "sha512-5ypkMO368UbWd1e0ZwKaflYLXSHSw2wAvC5/yApv03pX/KV3uD/2/qF7rW841H9I3QPmS03YZ6UZmlQV/fNczw==", "integrity": "sha512-rcHNbhSGfj80Av5p06LgIUxN8wQbrdx8yblikJamDezqxe0B11CJSEJuidz6TJoCRDZuWHt+P5xMAEhp92ZUcA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "^8.0.24", "@types/node": "^8.0.24",
"electron-download": "^4.1.0", "electron-download": "^4.1.0",
"extract-zip": "^1.0.3" "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": { "electron-builder": {
@ -6514,6 +6529,15 @@
"minimatch": "~3.0.2" "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": { "got": {
"version": "6.7.1", "version": "6.7.1",
"resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz", "resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz",
@ -6819,7 +6843,8 @@
"highlight.js": { "highlight.js": {
"version": "9.12.0", "version": "9.12.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz", "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": { "hmac-drbg": {
"version": "1.0.1", "version": "1.0.1",
@ -6895,14 +6920,6 @@
"uglify-js": "3.3.x" "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": { "html-tags": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz",
@ -15100,6 +15117,14 @@
"utila": "~0.4" "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": { "private": {
"version": "0.1.8", "version": "0.1.8",
"resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", "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": { "select-hose": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@ -16324,14 +16355,6 @@
"parse-sel": "^1.0.0" "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": { "snake-case": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-2.1.0.tgz", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-2.1.0.tgz",
@ -17614,6 +17637,12 @@
"setimmediate": "^1.0.4" "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": { "title-case": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/title-case/-/title-case-2.1.1.tgz", "resolved": "https://registry.npmjs.org/title-case/-/title-case-2.1.1.tgz",
@ -18317,7 +18346,8 @@
"void-elements": { "void-elements": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=",
"dev": true
}, },
"vue": { "vue": {
"version": "2.5.17", "version": "2.5.17",

View File

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

View File

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

View File

@ -1,3 +1,4 @@
/* Common CSS use by both light and dark themes */
:root { :root {
--brandColor: #5b3cc4; --brandColor: #5b3cc4;
--successColor: rgb(23, 201, 100); --successColor: rgb(23, 201, 100);
@ -119,6 +120,9 @@ div.ag-function-html pre.ag-html-block {
opacity: 0; opacity: 0;
z-index: -1; z-index: -1;
position: absolute; position: absolute;
margin-top: 0;
margin-bottom: 0;
overflow: visible;
} }
div.ag-function-html.ag-active pre.ag-html-block, div.ag-function-html.ag-active pre.ag-html-block,
@ -431,12 +435,17 @@ pre.ag-front-matter {
margin: 1rem 0; 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...'; content: 'Input YAML Front Matter...';
color: var(--placeholerColor); 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...'; content: 'Input Mathematical Formula...';
color: var(--placeholerColor); color: var(--placeholerColor);
} }
@ -444,7 +453,8 @@ span.ag-multiple-math-line:first-of-type:empty::after {
figure, figure,
pre.ag-html-block, pre.ag-html-block,
div.ag-function-html, div.ag-function-html,
pre.ag-code-block, pre.ag-fence-code,
pre.ag-indent-code,
li.ag-list-item > p.ag-paragraph { li.ag-list-item > p.ag-paragraph {
position: relative; position: relative;
display: inline-flex; display: inline-flex;
@ -460,9 +470,14 @@ li.ag-list-item > p.ag-paragraph > span {
width: 100%; width: 100%;
} }
pre.ag-code-block { pre.ag-fence-code,
pre.ag-indent-code {
margin: 1rem 0; margin: 1rem 0;
padding: 0 .5rem; }
pre > code {
width: 100%;
display: block;
} }
pre.ag-active.ag-front-matter::before, pre.ag-active.ag-front-matter::before,
@ -475,15 +490,19 @@ pre.ag-active.ag-multiple-math::after {
content: '$$'; content: '$$';
} }
pre.ag-active.ag-code-block::before, pre.ag-active.ag-fence-code::before,
pre.ag-active.ag-code-block::after { pre.ag-active.ag-indent-code::after,
pre.ag-active.ag-fence-code::after,
pre.ag-active.ag-indent-code::before {
content: '```'; content: '```';
} }
pre.ag-active.ag-front-matter::before, pre.ag-active.ag-front-matter::before,
pre.ag-active.ag-front-matter::after, pre.ag-active.ag-front-matter::after,
pre.ag-active.ag-code-block::before, pre.ag-active.ag-fence-code::before,
pre.ag-active.ag-code-block::after, 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::before,
pre.ag-active.ag-multiple-math::after { pre.ag-active.ag-multiple-math::after {
color: var(--regularColor); 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-front-matter::before,
pre.ag-active.ag-multiple-math::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; top: -20px;
} }
pre.ag-active.ag-front-matter::after, pre.ag-active.ag-front-matter::after,
pre.ag-active.ag-multiple-math::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; bottom: -23px;
} }
@ -520,7 +541,7 @@ figure.ag-active div.ag-math-preview {
top: calc(100% + 8px); top: calc(100% + 8px);
left: 50%; left: 50%;
width: auto; width: auto;
z-index: 1; z-index: 10000;
transform: translateX(-50%); transform: translateX(-50%);
padding: .5rem; padding: .5rem;
background: #fff; background: #fff;
@ -534,10 +555,6 @@ div.ag-html-preview {
width: 100%; width: 100%;
} }
pre .CodeMirror {
width: 100%;
}
img { img {
max-width: 100%; max-width: 100%;
} }
@ -595,12 +612,12 @@ span.ag-emoji-marked-text {
} }
.ag-language-input { .ag-language-input {
outline: none; padding: 0 1rem;
display: none; display: none;
min-width: 80px; min-width: 80px;
position: absolute; position: absolute;
top: -20px; top: -23px;
left: 30px; left: 20px;
font-size: 14px; font-size: 14px;
font-family: monospace; font-family: monospace;
font-weight: 600; font-weight: 600;
@ -618,6 +635,13 @@ pre.ag-active .ag-language-input {
display: block; 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 { span.ag-image-marked-text, span.ag-link-in-bracket, span.ag-link-in-bracket .ag-backlash {
color: var(--regularColor); color: var(--regularColor);
font-size: 16px; 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; font-family: monospace;
} }
.ag-language {
color: var(--dangerColor);
text-decoration: none;
font-family: monospace;
}
.ag-backlash { .ag-backlash {
text-decoration: none; text-decoration: none;
color: rgb(51, 51, 51); 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 // Electron 2.0.2 not support yet! So give a default value 4
export const DEVICE_MEMORY = navigator.deviceMemory || 4 // Get the divice memory number(Chrome >= 63) export const DEVICE_MEMORY = navigator.deviceMemory || 4 // Get the divice memory number(Chrome >= 63)
export const UNDO_DEPTH = DEVICE_MEMORY >= 4 ? 100 : 50 export const UNDO_DEPTH = DEVICE_MEMORY >= 4 ? 100 : 50
export const HAS_TEXT_BLOCK_REG = /^(h\d|span|th|td|hr|pre)/i export const HAS_TEXT_BLOCK_REG = /^(h\d|span|th|td|hr)/i
export const VOID_HTML_TAGS = voidHtmlTags export const VOID_HTML_TAGS = voidHtmlTags
export const HTML_TAGS = htmlTags export const HTML_TAGS = htmlTags
// TYPE1 ~ TYPE7 according to https://github.github.com/gfm/#html-blocks // TYPE1 ~ TYPE7 according to https://github.github.com/gfm/#html-blocks
@ -68,21 +68,15 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_LINE', 'AG_LINE',
'AG_ACTIVE', 'AG_ACTIVE',
'AG_EDITOR_ID', '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_EMOJI_MARKED_TEXT',
'AG_CODE_BLOCK', 'AG_FENCE_CODE',
'AG_INDENT_CODE',
'AG_HTML_BLOCK', 'AG_HTML_BLOCK',
'AG_HTML_ESCAPE', 'AG_HTML_ESCAPE',
'AG_FRONT_MATTER', 'AG_FRONT_MATTER',
'AG_FRONT_MATTER_LINE', 'AG_CODE_LINE',
'AG_MULTIPLE_MATH_LINE', 'AG_CODE_LINE_ADD',
'AG_CODEMIRROR_BLOCK', 'AG_CODE_LINE_MINUS',
'AG_SHOW_PREVIEW', 'AG_SHOW_PREVIEW',
'AG_HTML_PREVIEW', 'AG_HTML_PREVIEW',
'AG_LANGUAGE', 'AG_LANGUAGE',
@ -134,20 +128,6 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_REFERENCE_LINK' '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([ export const DAED_REMOVE_SELECTOR = new Set([
'.ag-image-marked-text::before', '.ag-image-marked-text::before',
'.ag-image-marked-text.ag-image-fail::before', '.ag-image-marked-text.ag-image-fail::before',
@ -209,7 +189,8 @@ export const HTML_TOOLS = [{
export const LINE_BREAK = '\n' export const LINE_BREAK = '\n'
export const PREVIEW_DOMPURIFY_CONFIG = { 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, ALLOW_DATA_ATTR: false,
USE_PROFILES: { USE_PROFILES: {
html: true, html: true,

View File

@ -1,12 +1,4 @@
import { EVENT_KEYS, CLASS_OR_ID } from '../config' import { EVENT_KEYS, CLASS_OR_ID } from '../config'
import {
isCursorAtFirstLine,
isCursorAtLastLine,
isCursorAtBegin,
isCursorAtEnd,
getBeginPosition,
getEndPosition
} from '../codeMirror'
import { findNearestParagraph } from '../selection/dom' import { findNearestParagraph } from '../selection/dom'
import selection from '../selection' import selection from '../selection'
@ -59,7 +51,6 @@ const arrowCtrl = ContentState => {
const preBlock = this.findPreBlockInLocation(block) const preBlock = this.findPreBlockInLocation(block)
const nextBlock = this.findNextBlockInLocation(block) const nextBlock = this.findNextBlockInLocation(block)
const { left, right } = selection.getCaretOffsets(paragraph)
const { start, end } = selection.getCursorRange() const { start, end } = selection.getCursorRange()
const { topOffset, bottomOffset } = selection.getCursorYOffset(paragraph) 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)) { if (/th|td/.test(block.type)) {
let activeBlock let activeBlock
const cellInNextRow = this.findNextRowCell(block) const cellInNextRow = this.findNextRowCell(block)
@ -193,39 +130,6 @@ const arrowCtrl = ContentState => {
} }
if ( 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.ArrowUp) ||
(event.key === EVENT_KEYS.ArrowLeft && start.offset === 0) (event.key === EVENT_KEYS.ArrowLeft && start.offset === 0)
) { ) {

View File

@ -1,6 +1,5 @@
import selection from '../selection' import selection from '../selection'
import { findNearestParagraph, findOutMostParagraph } from '../selection/dom' import { findNearestParagraph, findOutMostParagraph } from '../selection/dom'
import { isCursorAtBegin, onlyHaveOneLine, getEndPosition } from '../codeMirror'
const backspaceCtrl = ContentState => { const backspaceCtrl = ContentState => {
ContentState.prototype.checkBackspaceCase = function () { ContentState.prototype.checkBackspaceCase = function () {
@ -128,6 +127,10 @@ const backspaceCtrl = ContentState => {
return this.render() 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 // If select multiple paragraph or multiple characters in one paragraph, just let
// updateCtrl to handle this case. // updateCtrl to handle this case.
if (start.key !== end.key || start.offset !== end.offset) { if (start.key !== end.key || start.offset !== end.offset) {
@ -151,19 +154,39 @@ const backspaceCtrl = ContentState => {
return tHeadHasContent || tBodyHasContent return tHeadHasContent || tBodyHasContent
} }
if (block.type === 'pre' && /code|html/.test(block.functionType)) { if (
const cm = this.codeBlocks.get(id) block.type === 'span' &&
// if event.preventDefault(), you can not use backspace in language input. block.functionType === 'codeLine' &&
if (isCursorAtBegin(cm) && onlyHaveOneLine(cm)) { left === 0 &&
const anchorBlock = block.functionType === 'html' ? this.getParent(this.getParent(block)) : block !block.preSibling
event.preventDefault() ) {
const value = cm.getValue() event.preventDefault()
const newBlock = this.createBlockP(value) event.stopPropagation()
this.insertBefore(newBlock, anchorBlock) if (
this.removeBlock(anchorBlock) !block.nextSibling
this.codeBlocks.delete(id) ) {
const key = newBlock.children[0].key const preBlock = this.getParent(parent)
const pBlock = this.createBlock('p')
const lineBlock = this.createBlock('span', block.text)
const key = lineBlock.key
const offset = 0 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 = { this.cursor = {
start: { key, offset }, start: { key, offset },
@ -171,31 +194,6 @@ const backspaceCtrl = ContentState => {
} }
this.partialRender() 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)) { } else if (left === 0 && /th|td/.test(block.type)) {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
@ -298,19 +296,7 @@ const backspaceCtrl = ContentState => {
const { text } = block const { text } = block
const key = preBlock.key const key = preBlock.key
const offset = preBlock.text.length const offset = preBlock.text.length
if (preBlock.type === 'pre' && /code|html/.test(preBlock.functionType)) { preBlock.text += text
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
}
// If block is a line block and its parent paragraph only has one text line, // If block is a line block and its parent paragraph only has one text line,
// also need to remove the paragrah // also need to remove the paragrah
if (this.isOnlyChild(block) && block.type === 'span') { 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 => { const clickCtrl = ContentState => {
ContentState.prototype.clickHandler = function (event) { 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 { loadLanguage } from '../prism/index'
import { createInputInCodeBlock } from '../utils/domManipulate'
import { sanitize, getParagraphReference } from '../utils'
import { codeMirrorConfig, BLOCK_TYPE7, PREVIEW_DOMPURIFY_CONFIG, CLASS_OR_ID } from '../config'
const CODE_UPDATE_REP = /^`{3,}(.*)/ 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 => { const codeBlockCtrl = ContentState => {
ContentState.prototype.selectLanguage = function (paragraph, name) { ContentState.prototype.selectLanguage = function (paragraph, name) {
const block = this.getBlock(paragraph.id) const block = this.getBlock(paragraph.id)
block.text = block.text.replace(/^(`+)([^`]+$)/g, `$1${name}`) loadLanguage(name)
this.codeBlockUpdate(block) if (block.functionType === 'languageInput') {
this.partialRender() block.text = name
} const preBlock = this.getParent(block)
// Fix bug: when click the edge at the code block, the code block will be not focused. const nextSibling = this.getNextSibling(block)
ContentState.prototype.focusCodeBlock = function (event) { preBlock.lang = name
const key = event.target.id preBlock.functionType = 'fencecode'
const offset = 0 nextSibling.lang = name
nextSibling.children.forEach(c => (c.lang = name))
this.cursor = { const { key } = nextSibling.children[0]
start: { key, offset }, const offset = 0
end: { key, offset } this.cursor = {
start: { key, offset },
end: { key, offset }
}
} else {
block.text = block.text.replace(/^(`+)([^`]+$)/g, `$1${name}`)
this.codeBlockUpdate(block)
} }
this.partialRender() this.partialRender()
} }
/** /**
* [codeBlockUpdate if block updated to `pre` return true, else return false] * [codeBlockUpdate if block updated to `pre` return true, else return false]
*/ */
@ -53,15 +41,26 @@ const codeBlockCtrl = ContentState => {
const { text } = block.children[0] const { text } = block.children[0]
const match = CODE_UPDATE_REP.exec(text) const match = CODE_UPDATE_REP.exec(text)
if (match || lang) { if (match || lang) {
const codeBlock = this.createBlock('code')
const 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.type = 'pre'
block.functionType = 'code' block.functionType = 'fencecode'
block.codeBlockStyle = 'fenced' block.lang = language
block.text = code block.text = ''
block.history = null block.history = null
block.lang = lang || (match ? match[1] : '')
block.children = [] block.children = []
const { key } = block codeBlock.lang = language
const offset = 0 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 = { this.cursor = {
start: { key, offset }, start: { key, offset },
end: { key, offset } end: { key, offset }
@ -70,138 +69,6 @@ const codeBlockCtrl = ContentState => {
} }
return false 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 export default codeBlockCtrl

View File

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

View File

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

View File

@ -13,22 +13,12 @@ export class History {
this.index = this.index - 1 this.index = this.index - 1
const state = deepCopy(this.stack[this.index]) const state = deepCopy(this.stack[this.index])
switch (state.type) { const { blocks, cursor, renderRange } = state
case 'normal': cursor.noHistory = true
const { blocks, cursor, renderRange } = state this.contentState.blocks = blocks
cursor.noHistory = true this.contentState.renderRange = renderRange
this.contentState.blocks = blocks this.contentState.cursor = cursor
this.contentState.renderRange = renderRange this.contentState.render()
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
}
} }
} }
@ -38,22 +28,12 @@ export class History {
if (index < len - 1) { if (index < len - 1) {
this.index = index + 1 this.index = index + 1
const state = deepCopy(stack[this.index]) const state = deepCopy(stack[this.index])
switch (state.type) { const { blocks, cursor, renderRange } = state
case 'normal': cursor.noHistory = true
const { blocks, cursor, renderRange } = state this.contentState.blocks = blocks
cursor.noHistory = true this.contentState.renderRange = renderRange
this.contentState.blocks = blocks this.contentState.cursor = cursor
this.contentState.renderRange = renderRange this.contentState.render()
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
}
} }
} }

View File

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

View File

@ -1,5 +1,4 @@
import { HAS_TEXT_BLOCK_REG, DEFAULT_TURNDOWN_CONFIG } from '../config' import { HAS_TEXT_BLOCK_REG, DEFAULT_TURNDOWN_CONFIG } from '../config'
import { setCursorAtLastLine } from '../codeMirror'
import { getUniqueId } from '../utils' import { getUniqueId } from '../utils'
import selection from '../selection' import selection from '../selection'
import StateRender from '../parser/render' import StateRender from '../parser/render'
@ -21,6 +20,8 @@ import searchCtrl from './searchCtrl'
import mathCtrl from './mathCtrl' import mathCtrl from './mathCtrl'
import imagePathCtrl from './imagePathCtrl' import imagePathCtrl from './imagePathCtrl'
import htmlBlockCtrl from './htmlBlock' import htmlBlockCtrl from './htmlBlock'
import clickCtrl from './clickCtrl'
import inputCtrl from './inputCtrl'
import importMarkdown from '../utils/importMarkdown' import importMarkdown from '../utils/importMarkdown'
const prototypes = [ const prototypes = [
@ -41,12 +42,13 @@ const prototypes = [
mathCtrl, mathCtrl,
imagePathCtrl, imagePathCtrl,
htmlBlockCtrl, htmlBlockCtrl,
clickCtrl,
inputCtrl,
importMarkdown importMarkdown
] ]
class ContentState { class ContentState {
constructor (muya, options) { constructor (muya, options) {
const { eventCenter } = muya
const { bulletListMarker } = options const { bulletListMarker } = options
this.muya = muya this.muya = muya
@ -55,7 +57,7 @@ class ContentState {
// Use to cache the keys which you don't want to remove. // Use to cache the keys which you don't want to remove.
this.exemption = new Set() this.exemption = new Set()
this.blocks = [ this.createBlockP() ] this.blocks = [ this.createBlockP() ]
this.stateRender = new StateRender(eventCenter) this.stateRender = new StateRender(muya)
this.codeBlocks = new Map() this.codeBlocks = new Map()
this.renderRange = [ null, null ] this.renderRange = [ null, null ]
this.currentCursor = null this.currentCursor = null
@ -69,22 +71,9 @@ class ContentState {
} }
set cursor (cursor) { 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 handler = () => {
const { blocks, renderRange, currentCursor } = this const { blocks, renderRange, currentCursor } = this
this.history.push({ this.history.push({
type: 'normal',
blocks, blocks,
renderRange, renderRange,
cursor: currentCursor cursor: currentCursor
@ -136,34 +125,20 @@ class ContentState {
} }
setCursor () { setCursor () {
const { start: { key } } = this.cursor selection.setCursorRange(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)
}
} }
setNextRenderRange () { setNextRenderRange () {
const { start, end } = this.cursor const { start, end } = this.cursor
// console.log(JSON.stringify(this.cursor, null, 2))
const startBlock = this.getBlock(start.key) const startBlock = this.getBlock(start.key)
const endBlock = this.getBlock(end.key) const endBlock = this.getBlock(end.key)
const startOutMostBlock = this.findOutMostBlock(startBlock) const startOutMostBlock = this.findOutMostBlock(startBlock)
const endOutMostBlock = this.findOutMostBlock(endBlock) const endOutMostBlock = this.findOutMostBlock(endBlock)
this.renderRange = [ startOutMostBlock.preSibling, endOutMostBlock.nextSibling ] this.renderRange = [ startOutMostBlock.preSibling, endOutMostBlock.nextSibling ]
} }
render (isRenderCursor = true, refreshCodeBlock = false) { render (isRenderCursor = true) {
const { blocks, cursor, searchMatches: { matches, index } } = this const { blocks, cursor, searchMatches: { matches, index } } = this
const activeBlocks = this.getActiveBlocks() const activeBlocks = this.getActiveBlocks()
matches.forEach((m, i) => { matches.forEach((m, i) => {
@ -171,15 +146,13 @@ class ContentState {
}) })
this.setNextRenderRange() this.setNextRenderRange()
this.stateRender.collectLabels(blocks) this.stateRender.collectLabels(blocks)
this.stateRender.render(blocks, cursor, activeBlocks, matches, refreshCodeBlock) this.stateRender.render(blocks, cursor, activeBlocks, matches)
this.pre2CodeMirror(isRenderCursor)
if (isRenderCursor) this.setCursor() if (isRenderCursor) this.setCursor()
} }
partialRender () { partialRender () {
const { blocks, cursor, searchMatches: { matches, index } } = this const { blocks, cursor, searchMatches: { matches, index } } = this
const activeBlocks = this.getActiveBlocks() const activeBlocks = this.getActiveBlocks()
const cursorOutMostBlock = activeBlocks[activeBlocks.length - 1]
const [ startKey, endKey ] = this.renderRange const [ startKey, endKey ] = this.renderRange
matches.forEach((m, i) => { matches.forEach((m, i) => {
m.active = i === index m.active = i === index
@ -191,7 +164,6 @@ class ContentState {
this.setNextRenderRange() this.setNextRenderRange()
this.stateRender.collectLabels(blocks) this.stateRender.collectLabels(blocks)
this.stateRender.partialRender(needRenderBlocks, cursor, activeBlocks, matches, startKey, endKey) this.stateRender.partialRender(needRenderBlocks, cursor, activeBlocks, matches, startKey, endKey)
this.pre2CodeMirror(true, [...new Set([cursorOutMostBlock, ...needRenderBlocks])])
this.setCursor() this.setCursor()
} }
@ -299,6 +271,7 @@ class ContentState {
} }
removeTextOrBlock (block) { removeTextOrBlock (block) {
if (block.functionType === 'languageInput') return
const checkerIn = block => { const checkerIn = block => {
if (this.exemption.has(block.key)) { if (this.exemption.has(block.key)) {
return true return true
@ -382,7 +355,7 @@ class ContentState {
if (!afterEnd) { if (!afterEnd) {
const parent = this.getParent(after) const parent = this.getParent(after)
if (parent) { if (parent) {
const removeAfter = isRemoveAfter && this.isOnlyEditableChild(after) const removeAfter = isRemoveAfter && (this.isOnlyRemoveableChild(after))
this.removeBlocks(before, parent, removeAfter, true) this.removeBlocks(before, parent, removeAfter, true)
} }
} }
@ -395,12 +368,6 @@ class ContentState {
} }
removeBlock (block, fromBlocks = this.blocks) { 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 remove = (blocks, block) => {
const len = blocks.length const len = blocks.length
let i let i
@ -531,11 +498,11 @@ class ContentState {
return !block.nextSibling && !block.preSibling return !block.nextSibling && !block.preSibling
} }
isOnlyEditableChild (block) { isOnlyRemoveableChild (block) {
if (block.editable === false) return false if (block.editable === false) return false
const parent = this.getParent(block) const parent = this.getParent(block)
if (!parent) throw new Error('isOnlyEditableChild method only apply for child block') if (!parent) throw new Error('isOnlyRemoveableChild method only apply for child block')
return parent.children.filter(child => child.editable).length === 1 return parent.children.filter(child => child.editable && child.functionType !== 'languageInput').length === 1
} }
getLastChild (block) { getLastChild (block) {
@ -553,7 +520,11 @@ class ContentState {
if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) { if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
return block return block
} else if (children.length) { } 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]) return this.firstInDescendant(children[1])
} else { } else {
return this.firstInDescendant(children[0]) return this.firstInDescendant(children[0])
@ -577,7 +548,13 @@ class ContentState {
findPreBlockInLocation (block) { findPreBlockInLocation (block) {
const parent = this.getParent(block) const parent = this.getParent(block)
const preBlock = this.getPreSibling(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) return this.lastInDescendant(preBlock)
} else if (parent) { } else if (parent) {
return this.findPreBlockInLocation(parent) return this.findPreBlockInLocation(parent)
@ -590,7 +567,9 @@ class ContentState {
const parent = this.getParent(block) const parent = this.getParent(block)
const nextBlock = this.getNextSibling(block) const nextBlock = this.getNextSibling(block)
if (nextBlock && nextBlock.editable !== false) { if (
nextBlock && nextBlock.editable !== false
) {
return this.firstInDescendant(nextBlock) return this.firstInDescendant(nextBlock)
} else if (parent) { } else if (parent) {
return this.findNextBlockInLocation(parent) return this.findNextBlockInLocation(parent)
@ -627,7 +606,7 @@ class ContentState {
} }
clear () { 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 => { 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) { 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 = '') { ContentState.prototype.createMathBlock = function (value = '') {
const FUNCTION_TYPE = 'multiplemath' const FUNCTION_TYPE = 'multiplemath'
const mathBlock = this.createBlock('figure') const mathBlock = this.createBlock('figure')
const textArea = this.createBlock('pre') mathBlock.functionType = FUNCTION_TYPE
const mathPreview = this.createBlock('div', '', false) 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) { if (typeof value === 'string' && value) {
const lines = value.replace(/^\s+/, '').split(LINE_BREAKS_REG).map(line => this.createBlock('span', line)) value.replace(/^\s+/, '').split(LINE_BREAKS_REG).forEach(line => {
for (const line of lines) { const codeLine = this.createBlock('span', line)
line.functionType = FUNCTION_TYPE codeLine.functionType = 'codeLine'
this.appendChild(textArea, line) codeLine.lang = 'latex'
} this.appendChild(codeBlock, codeLine)
})
} else { } else {
const emptyLine = this.createBlock('span') const emptyLine = this.createBlock('span')
emptyLine.functionType = FUNCTION_TYPE emptyLine.functionType = 'codeLine'
this.appendChild(textArea, emptyLine) emptyLine.lang = 'latex'
this.appendChild(codeBlock, emptyLine)
} }
mathBlock.functionType = textArea.functionType = mathPreview.functionType = FUNCTION_TYPE const mathPreview = this.createBlock('div', '', false)
mathPreview.math = value this.codeBlocks.set(preBlock.key, '')
this.appendChild(mathBlock, textArea) mathPreview.functionType = FUNCTION_TYPE
this.appendChild(mathBlock, mathPreview)
return mathBlock return { preBlock, mathPreview }
} }
ContentState.prototype.initMathBlock = function (block) { // p block ContentState.prototype.initMathBlock = function (block) { // p block
const FUNCTION_TYPE = 'multiplemath' 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.type = 'figure'
block.functionType = FUNCTION_TYPE block.functionType = FUNCTION_TYPE
block.children = [] block.children = []
const mathPreview = this.createBlock('div', '', false) const { preBlock, mathPreview } = this.createMathAndPreview()
mathPreview.math = ''
mathPreview.functionType = FUNCTION_TYPE
this.appendChild(block, textArea) this.appendChild(block, preBlock)
this.appendChild(block, mathPreview) this.appendChild(block, mathPreview)
return emptyLine return preBlock.children[0].children[0]
} }
ContentState.prototype.handleMathBlockClick = function (mathFigure) { ContentState.prototype.handleMathBlockClick = function (mathFigure) {

View File

@ -17,7 +17,6 @@ const getCurrentLevel = type => {
const paragraphCtrl = ContentState => { const paragraphCtrl = ContentState => {
ContentState.prototype.selectionChange = function (cursor) { ContentState.prototype.selectionChange = function (cursor) {
const { fontSize, lineHeight } = this
const { start, end } = cursor || selection.getCursorRange() const { start, end } = cursor || selection.getCursorRange()
const cursorCoords = selection.getCursorCoords() const cursorCoords = selection.getCursorCoords()
const startBlock = this.getBlock(start.key) const startBlock = this.getBlock(start.key)
@ -33,13 +32,6 @@ const paragraphCtrl = ContentState => {
end.type = endBlock.type end.type = endBlock.type
end.block = endBlock 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 { return {
start, start,
end, end,
@ -73,10 +65,13 @@ const paragraphCtrl = ContentState => {
const firstBlock = this.blocks[0] const firstBlock = this.blocks[0]
if (firstBlock.type === 'pre' && firstBlock.functionType === 'frontmatter') return if (firstBlock.type === 'pre' && firstBlock.functionType === 'frontmatter') return
const frontMatter = this.createBlock('pre') const frontMatter = this.createBlock('pre')
const codeBlock = this.createBlock('code')
const emptyLine = this.createBlock('span') const emptyLine = this.createBlock('span')
emptyLine.functionType = 'frontmatter' frontMatter.lang = codeBlock.lang = emptyLine.lang = 'yaml'
emptyLine.functionType = 'codeLine'
frontMatter.functionType = 'frontmatter' frontMatter.functionType = 'frontmatter'
this.appendChild(frontMatter, emptyLine) this.appendChild(codeBlock, emptyLine)
this.appendChild(frontMatter, codeBlock)
this.insertBefore(frontMatter, firstBlock) this.insertBefore(frontMatter, firstBlock)
const { key } = emptyLine const { key } = emptyLine
const offset = 0 const offset = 0
@ -197,70 +192,75 @@ const paragraphCtrl = ContentState => {
const startParents = this.getParents(startBlock) const startParents = this.getParents(startBlock)
const endParents = this.getParents(endBlock) const endParents = this.getParents(endBlock)
const hasFencedCodeBlockParent = () => { const hasFencedCodeBlockParent = () => {
return startParents.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' && b.functionType === 'code') endParents.some(b => b.type === 'pre' && /code/.test(b.functionType))
} }
// change fenced code block to p paragraph // change fenced code block to p paragraph
if (affiliation.length && affiliation[0].type === 'pre' && affiliation[0].functionType === 'code') { if (affiliation.length && affiliation[0].type === 'pre' && /code/.test(affiliation[0].functionType)) {
const codeBlock = affiliation[0] const preBlock = affiliation[0]
this.codeBlocks.delete(codeBlock.key) const codeLines = preBlock.children[1].children
codeBlock.type = 'p' this.codeBlocks.delete(preBlock.key)
codeBlock.children = [] preBlock.type = 'p'
const lines = codeBlock.text.split(LINE_BREAKS_REG).map(line => this.createBlock('span', line)) preBlock.children = []
for (const line of lines) {
this.appendChild(codeBlock, line) for (const line of codeLines) {
delete line.lang
delete line.functionType
this.appendChild(preBlock, line)
} }
const { key } = codeBlock.children[codeBlock.selection.anchor.line] delete preBlock.lang
const offset = codeBlock.selection.anchor.ch delete preBlock.functionType
delete codeBlock.selection
delete codeBlock.history
delete codeBlock.lang
delete codeBlock.coords
delete codeBlock.text
delete codeBlock.codeBlockStyle
delete codeBlock.functionType
this.cursor = { this.cursor = {
start: { key, offset }, start: this.cursor.start,
end: { key, offset } end: this.cursor.end
} }
} else { } else {
if (start.key === end.key) { if (start.key === end.key) {
if (startBlock.type === 'span') { if (startBlock.type === 'span') {
startBlock = this.getParent(startBlock) startBlock = this.getParent(startBlock)
startBlock.text = startBlock.children.map(line => line.text).join('\n') startBlock.type = 'pre'
const line = startBlock.children.findIndex(line => line.key === start.key) const codeBlock = this.createBlock('code')
const ch = start.offset const inputBlock = this.createBlock('span', '')
startBlock.selection = { inputBlock.functionType = 'languageInput'
anchor: { line, ch }, startBlock.functionType = 'fencecode'
head: { line, ch } startBlock.lang = codeBlock.lang = ''
} const codeLines = startBlock.children
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 = { this.cursor = {
start: { key, offset }, start: this.cursor.start,
end: { key, offset } end: this.cursor.end
} }
} else if (!hasFencedCodeBlockParent()) { } else if (!hasFencedCodeBlockParent()) {
const { parent, startIndex, endIndex } = this.getCommonParent() const { parent, startIndex, endIndex } = this.getCommonParent()
const children = parent ? parent.children : this.blocks const children = parent ? parent.children : this.blocks
const referBlock = children[endIndex] const referBlock = children[endIndex]
const codeBlock = this.createBlock('pre') const preBlock = this.createBlock('pre')
codeBlock.codeBlockStyle = 'fenced' const codeBlock = this.createBlock('code')
codeBlock.functionType = 'code' preBlock.functionType = 'fencecode'
codeBlock.history = null preBlock.lang = codeBlock.lang = ''
codeBlock.lang = ''
const markdown = new ExportMarkdown(children.slice(startIndex, endIndex + 1)).generate() const markdown = new ExportMarkdown(children.slice(startIndex, endIndex + 1)).generate()
codeBlock.text = markdown markdown.split(LINE_BREAKS_REG).forEach(text => {
this.insertAfter(codeBlock, referBlock) 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 let i
const removeCache = [] const removeCache = []
for (i = startIndex; i <= endIndex; i++) { for (i = startIndex; i <= endIndex; i++) {
@ -268,7 +268,7 @@ const paragraphCtrl = ContentState => {
removeCache.push(child) removeCache.push(child)
} }
removeCache.forEach(b => this.removeBlock(b)) removeCache.forEach(b => this.removeBlock(b))
const key = codeBlock.key const key = codeBlock.children[0].key
const offset = 0 const offset = 0
this.cursor = { this.cursor = {
start: { key, offset }, start: { key, offset },
@ -353,8 +353,8 @@ const paragraphCtrl = ContentState => {
ContentState.prototype.insertHtmlBlock = function (block) { ContentState.prototype.insertHtmlBlock = function (block) {
const parentBlock = this.getParent(block) const parentBlock = this.getParent(block)
block.text = '<div>' block.text = '<div>'
const cursorBlock = this.initHtmlBlock(parentBlock, 'div') const preBlock = this.initHtmlBlock(parentBlock, 'div')
const key = cursorBlock.key const key = preBlock.children[0].children[1].key
const offset = 0 const offset = 0
this.cursor = { this.cursor = {
start: { key, offset }, start: { key, offset },
@ -519,8 +519,30 @@ const paragraphCtrl = ContentState => {
// if cursor is not in one line or paragraph, can not insert paragraph // if cursor is not in one line or paragraph, can not insert paragraph
if (start.key !== end.key) return if (start.key !== end.key) return
let block = this.getBlock(start.key) let block = this.getBlock(start.key)
if (block.type === 'span') { if (block.type === 'span' && !block.functionType) {
block = this.getParent(block) 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)) { } else if (/th|td/.test(block.type)) {
// get figure block from table cell // get figure block from table cell
block = this.getParent(this.getParent(this.getParent(this.getParent(block)))) 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 sanitizedHtml = sanitize(html, PREVIEW_DOMPURIFY_CONFIG)
const tempWrapper = document.createElement('div') const tempWrapper = document.createElement('div')
tempWrapper.innerHTML = sanitizedHtml tempWrapper.innerHTML = sanitizedHtml
// special process for Number app in macOs
const tables = Array.from(tempWrapper.querySelectorAll('table')) const tables = Array.from(tempWrapper.querySelectorAll('table'))
for (const table of tables) { for (const table of tables) {
const row = table.querySelector('tr') const row = table.querySelector('tr')
@ -66,15 +67,11 @@ const pasteCtrl = ContentState => {
// handle `normal` and `pasteAsPlainText` paste // handle `normal` and `pasteAsPlainText` paste
ContentState.prototype.pasteHandler = function (event, type) { ContentState.prototype.pasteHandler = function (event, type) {
if (this.checkInCodeBlock()) {
return
}
event.preventDefault() event.preventDefault()
const text = event.clipboardData.getData('text/plain') const text = event.clipboardData.getData('text/plain')
let html = event.clipboardData.getData('text/html') let html = event.clipboardData.getData('text/html')
html = this.standardizeHTML(html) html = this.standardizeHTML(html)
// console.log(text)
// console.log(html)
const copyType = this.checkCopyType(html, text) const copyType = this.checkCopyType(html, text)
const { start, end } = this.cursor const { start, end } = this.cursor
const startBlock = this.getBlock(start.key) 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 // handle copyAsHtml
if (copyType === 'copyAsHtml') { if (copyType === 'copyAsHtml') {
switch (type) { switch (type) {
@ -196,6 +218,7 @@ const pasteCtrl = ContentState => {
startBlock.text += firstFragment.children[0].text startBlock.text += firstFragment.children[0].text
firstFragment.children.slice(1).forEach(line => { firstFragment.children.slice(1).forEach(line => {
if (startBlock.functionType) line.functionType = startBlock.functionType if (startBlock.functionType) line.functionType = startBlock.functionType
if (startBlock.lang) line.lang = startBlock.lang
this.appendChild(parent, line) this.appendChild(parent, line)
}) })
} else if (/^h\d$/.test(firstFragment.type)) { } else if (/^h\d$/.test(firstFragment.type)) {
@ -234,8 +257,8 @@ const pasteCtrl = ContentState => {
cursorBlock = startBlock cursorBlock = startBlock
} }
// TODO @Jocs duplicate with codes in updateCtrl.js // TODO @Jocs duplicate with codes in updateCtrl.js
if (cursorBlock && cursorBlock.type === 'span' && cursorBlock.functionType === 'multiplemath') { if (cursorBlock && cursorBlock.type === 'span' && cursorBlock.functionType === 'codeLine') {
this.updateMathContent(cursorBlock) this.updateCodeBlocks(cursorBlock)
} }
this.cursor = { this.cursor = {
start: { start: {

View File

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

View File

@ -1,9 +1,6 @@
import selection from '../selection'
import { tokenizer } from '../parser/parse' import { tokenizer } from '../parser/parse'
import { conflict } from '../utils' import { conflict } from '../utils'
import { getTextContent } from '../selection/dom'
import { CLASS_OR_ID, DEFAULT_TURNDOWN_CONFIG } from '../config' import { CLASS_OR_ID, DEFAULT_TURNDOWN_CONFIG } from '../config'
import { beginRules } from '../parser/rules'
const INLINE_UPDATE_FRAGMENTS = [ const INLINE_UPDATE_FRAGMENTS = [
'^([*+-]\\s)', // Bullet list '^([*+-]\\s)', // Bullet list
@ -16,8 +13,6 @@ const INLINE_UPDATE_FRAGMENTS = [
const INLINE_UPDATE_REG = new RegExp(INLINE_UPDATE_FRAGMENTS.join('|'), 'i') const INLINE_UPDATE_REG = new RegExp(INLINE_UPDATE_FRAGMENTS.join('|'), 'i')
let lastCursor = null
const updateCtrl = ContentState => { const updateCtrl = ContentState => {
// handle task list item checkbox click // handle task list item checkbox click
ContentState.prototype.listItemCheckBoxClick = function (checkbox) { ContentState.prototype.listItemCheckBoxClick = function (checkbox) {
@ -32,29 +27,41 @@ const updateCtrl = ContentState => {
return list.children[0].isLooseListItem === isLooseType return list.children[0].isLooseListItem === isLooseType
} }
ContentState.prototype.checkNeedRender = function (block) { ContentState.prototype.checkNeedRender = function (cursor = this.cursor) {
const { start: cStart, end: cEnd } = 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 startOffset = cStart.offset
const endOffset = cEnd.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 if (token.type === 'text') continue
const { start, end } = token.range 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 ( 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]) conflict([Math.max(0, start - 1), Math.min(textLen, end + 1)], [endOffset, endOffset])
) { ) {
return true return true
} }
} }
return false return false
} }
ContentState.prototype.checkInlineUpdate = function (block) { ContentState.prototype.checkInlineUpdate = function (block) {
// table cell can not have blocks in it // table cell can not have blocks in it
if (/th|td|figure/.test(block.type)) return false if (/th|td|figure/.test(block.type)) return false
if (/codeLine|languageInput/.test(block.functionType)) return false
// only first line block can update to other block // only first line block can update to other block
if (block.type === 'span' && block.preSibling) return false if (block.type === 'span' && block.preSibling) return false
if (block.type === 'span') { 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 // thematic break
ContentState.prototype.updateHr = function (block, marker) { ContentState.prototype.updateHr = function (block, marker) {
if (block.type !== 'hr') { if (block.type !== 'hr') {
@ -293,192 +293,11 @@ const updateCtrl = ContentState => {
return null return null
} }
ContentState.prototype.updateMathContent = function (block) { ContentState.prototype.updateCodeBlocks = function (block) {
const preBlock = this.getParent(block) const codeBlock = this.getParent(block)
const mathPreview = this.getNextSibling(preBlock) const preBlock = this.getParent(codeBlock)
const math = preBlock.children.map(line => line.text).join('\n') const code = codeBlock.children.map(line => line.text).join('\n')
mathPreview.math = math this.codeBlocks.set(preBlock.key, code)
}
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()
}
} }
} }

View File

@ -12,10 +12,6 @@ class ClickEvent {
const { container, eventCenter, contentState } = this.muya const { container, eventCenter, contentState } = this.muya
const handler = event => { const handler = event => {
const { target } = 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 // handler table | html toolbar click
const toolItem = getToolItem(target) const toolItem = getToolItem(target)
if (toolItem) { if (toolItem) {
@ -56,13 +52,7 @@ class ClickEvent {
if (target.tagName === 'INPUT' && target.classList.contains(CLASS_OR_ID['AG_TASK_LIST_ITEM_CHECKBOX'])) { if (target.tagName === 'INPUT' && target.classList.contains(CLASS_OR_ID['AG_TASK_LIST_ITEM_CHECKBOX'])) {
contentState.listItemCheckBoxClick(target) contentState.listItemCheckBoxClick(target)
} }
// is show format float box? contentState.clickHandler(event)
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 })
}
} }
eventCenter.attachDOMEvent(container, 'click', handler) 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 selection from '../selection'
import { findNearestParagraph } from '../selection/dom' import { findNearestParagraph } from '../selection/dom'
import { getParagraphReference } from '../utils' import { getParagraphReference } from '../utils'
import { checkEditLanguage } from '../codeMirror/language' import { checkEditLanguage } from '../prism/index'
import { checkEditEmoji } from '../ui/emojis' import { checkEditEmoji } from '../ui/emojis'
class Keyboard { class Keyboard {
@ -11,7 +11,7 @@ class Keyboard {
this._isEditChinese = false this._isEditChinese = false
this.shownFloat = new Set() this.shownFloat = new Set()
this.recordEditChinese() this.recordEditChinese()
this.dispatchUpdateState() this.dispatchEditorState()
this.keydownBinding() this.keydownBinding()
this.keyupBinding() this.keyupBinding()
this.inputBinding() this.inputBinding()
@ -39,32 +39,25 @@ class Keyboard {
eventCenter.attachDOMEvent(container, 'compositionstart', handler) eventCenter.attachDOMEvent(container, 'compositionstart', handler)
} }
dispatchUpdateState () { dispatchEditorState () {
const { container, eventCenter, contentState } = this.muya const { container, eventCenter, contentState } = this.muya
let timer = null let timer = null
const changeHandler = event => { 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 (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 (timer) clearTimeout(timer)
} timer = setTimeout(() => {
if (event.type === 'click' || event.type === 'keyup') { const selectionChanges = contentState.selectionChange()
if (timer) clearTimeout(timer) const { formats } = contentState.selectionFormats()
timer = setTimeout(() => { eventCenter.dispatch('selectionChange', selectionChanges)
const selectionChanges = contentState.selectionChange() eventCenter.dispatch('selectionFormats', formats)
const { formats } = contentState.selectionFormats() this.muya.dispatchChange()
eventCenter.dispatch('selectionChange', selectionChanges) })
eventCenter.dispatch('selectionFormats', formats)
this.muya.dispatchChange()
})
}
} }
eventCenter.attachDOMEvent(container, 'click', changeHandler) eventCenter.attachDOMEvent(container, 'click', changeHandler)
eventCenter.attachDOMEvent(container, 'keyup', changeHandler) eventCenter.attachDOMEvent(container, 'keyup', changeHandler)
eventCenter.attachDOMEvent(container, 'input', changeHandler)
} }
keydownBinding () { keydownBinding () {
@ -123,7 +116,7 @@ class Keyboard {
inputBinding () { inputBinding () {
const { container, eventCenter, contentState } = this.muya const { container, eventCenter, contentState } = this.muya
const inputHandler = _ => { const inputHandler = event => {
const node = selection.getSelectionStart() const node = selection.getSelectionStart()
const paragraph = findNearestParagraph(node) const paragraph = findNearestParagraph(node)
const selectionState = selection.exportSelection(paragraph) const selectionState = selection.exportSelection(paragraph)
@ -136,6 +129,12 @@ class Keyboard {
contentState.selectLanguage(paragraph, item.name) 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 && emojiNode &&
event.key !== EVENT_KEYS.Enter && event.key !== EVENT_KEYS.Enter &&
event.key !== EVENT_KEYS.ArrowDown && 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) const reference = getParagraphReference(emojiNode, paragraph.id)
eventCenter.dispatch('muya-emoji-picker', { eventCenter.dispatch('muya-emoji-picker', {
@ -169,7 +170,8 @@ class Keyboard {
} }
// is show format float box? // is show format float box?
const { start, end } = selection.getCursorRange() 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 reference = contentState.getPositionReference()
const { formats } = contentState.selectionFormats() const { formats } = contentState.selectionFormats()
eventCenter.dispatch('muya-format-picker', { reference, formats }) eventCenter.dispatch('muya-format-picker', { reference, formats })

View File

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

View File

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

View File

@ -1,6 +1,14 @@
import { CLASS_OR_ID } from '../../../config' import { CLASS_OR_ID } from '../../../config'
import { h } from '../snabbdom' 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) { export default function renderContainerBlock (block, cursor, activeBlocks, matches, useCache = false) {
let selector = this.getSelector(block, cursor, activeBlocks) let selector = this.getSelector(block, cursor, activeBlocks)
const data = { const data = {
@ -76,11 +84,19 @@ export default function renderContainerBlock (block, cursor, activeBlocks, match
if (block.type === 'ol') { if (block.type === 'ol') {
Object.assign(data.attrs, { start: block.start }) Object.assign(data.attrs, { start: block.start })
} }
if (block.type === 'pre' && /frontmatter|multiplemath/.test(block.functionType)) { if (block.type === 'code') {
const role = block.functionType === 'frontmatter' ? 'YAML' : 'MATH' const { lang } = block
const className = block.functionType === 'frontmatter' ? CLASS_OR_ID['AG_FRONT_MATTER'] : CLASS_OR_ID['AG_MULTIPLE_MATH'] if (lang) {
Object.assign(data.dataset, { role }) selector += `.language-${lang}`
selector += `.${className}` }
}
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))) 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 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 { tokenizer } from '../../parse'
import { snakeToCamel } from '../../../utils' import { snakeToCamel, sanitize, escapeHtml } from '../../../utils'
import { h, htmlToVNode } from '../snabbdom' import { h, htmlToVNode } from '../snabbdom'
const PRE_BLOCK_HASH = { const getHighlightHtml = (text, highlights) => {
'code': `.${CLASS_OR_ID['AG_CODE_BLOCK']}`, let code = ''
'html': `.${CLASS_OR_ID['AG_HTML_BLOCK']}`, let pos = 0
'frontmatter': `.${CLASS_OR_ID['AG_FRONT_MATTER']}` 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) { export default function renderLeafBlock (block, cursor, activeBlocks, matches, useCache = false) {
const { loadMathMap, refreshCodeBlock } = this const { loadMathMap } = this
let selector = this.getSelector(block, cursor, activeBlocks) let selector = this.getSelector(block, cursor, activeBlocks)
// highlight search key in block // highlight search key in block
const highlights = matches.filter(m => m.key === block.key) const highlights = matches.filter(m => m.key === block.key)
@ -20,17 +31,15 @@ export default function renderLeafBlock (block, cursor, activeBlocks, matches, u
type, type,
headingStyle, headingStyle,
align, align,
htmlContent,
icon, icon,
checked, checked,
key, key,
lang, lang,
functionType, functionType,
codeBlockStyle,
math,
editable editable
} = block } = block
const data = { const data = {
props: {},
attrs: {}, attrs: {},
dataset: {} dataset: {}
} }
@ -57,15 +66,17 @@ export default function renderLeafBlock (block, cursor, activeBlocks, matches, u
style: `text-align:${align}` style: `text-align:${align}`
}) })
} else if (type === 'div') { } else if (type === 'div') {
if (typeof htmlContent === 'string') { if (functionType === 'preview') {
selector += `.${CLASS_OR_ID['AG_HTML_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 // handle empty html bock
if (/<([a-z][a-z\d]*).*>\s*<\/\1>/.test(htmlContent)) { if (/<([a-z][a-z\d]*).*>\s*<\/\1>/.test(htmlContent)) {
children = htmlToVNode('<div class="ag-empty">&lt;Empty HTML Block&gt;</div>') children = htmlToVNode('<div class="ag-empty">&lt;Empty HTML Block&gt;</div>')
} else { } else {
children = htmlToVNode(htmlContent) 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` const key = `${math}_display_math`
selector += `.${CLASS_OR_ID['AG_MATH_PREVIEW']}` selector += `.${CLASS_OR_ID['AG_MATH_PREVIEW']}`
if (math === '') { if (math === '') {
@ -122,42 +133,32 @@ export default function renderLeafBlock (block, cursor, activeBlocks, matches, u
selector += `.${CLASS_OR_ID['AG_CHECKBOX_CHECKED']}` selector += `.${CLASS_OR_ID['AG_CHECKBOX_CHECKED']}`
} }
children = '' children = ''
} else if (type === 'pre') { } else if (type === 'span' && functionType === 'codeLine') {
selector += `.${CLASS_OR_ID['AG_CODEMIRROR_BLOCK']}` let code
selector += PRE_BLOCK_HASH[functionType] if (lang && lang === 'markup') {
Object.assign(data.attrs, { contenteditable: 'false' }) code = getHighlightHtml(escapeHtml(text), highlights)
data.hook = { } else {
prepatch (oldvnode, vnode) { code = getHighlightHtml(text, highlights)
// cheat snabbdom that the pre block is not changed!!!
if (!refreshCodeBlock) {
vnode.children = oldvnode.children
}
}
}
if (lang) {
Object.assign(data.dataset, {
lang
})
} }
if (codeBlockStyle) { selector += `.${CLASS_OR_ID['AG_CODE_LINE']}`
Object.assign(data.dataset, {
codeBlockStyle
})
}
if (/code|html/.test(functionType)) { if (lang && /\S/.test(code) && loadedCache.has(lang)) {
// do not set it to '' (empty string) const wrapper = document.createElement('div')
children = [] 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)) { } else if (type === 'span' && functionType === 'languageInput') {
if (functionType === 'frontmatter') { const html = getHighlightHtml(text, highlights)
selector += `.${CLASS_OR_ID['AG_FRONT_MATTER_LINE']}` selector += `.${CLASS_OR_ID['AG_LANGUAGE_INPUT']}`
} children = htmlToVNode(html)
if (functionType === 'multiplemath') {
selector += `.${CLASS_OR_ID['AG_MULTIPLE_MATH_LINE']}`
}
children = text
} }
return h(selector, data, children) 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') const snabbdom = require('snabbdom')
export const patch = snabbdom.init([ // Init patch function with chosen modules 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 require('snabbdom/modules/eventlisteners').default // attaches event listeners
]) ])
export const h = require('snabbdom/h').default // helper function for creating vnodes 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 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) { if (range.getClientRects) {
range.collapse(true) range.collapse(true)
const rects = range.getClientRects() const rects = range.getClientRects()
if (rects.length) { if (rects.length) {
const { left, top, x: rectX, y: rectY } = rects[0] const { left, top, x: rectX, y: rectY } = rects[0]
x = rectX || left x = rectX || left

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
// DOTO: Don't use Node API in editor folder, remove `path` @jocs // 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 axios from 'axios'
import createDOMPurify from 'dompurify' 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 => { export const getImageInfo = src => {
const EXT_REG = /\.(jpeg|jpg|png|gif|svg|webp)(?=\?|$)/i const EXT_REG = /\.(jpeg|jpg|png|gif|svg|webp)(?=\?|$)/i
// http[s] (domain or IPv4 or localhost or IPv6) [port] /not-white-space // http[s] (domain or IPv4 or localhost or IPv6) [port] /not-white-space

View File

@ -64,11 +64,6 @@ body {
caret-color: #efefef; caret-color: #efefef;
} }
#ag-editor-id>ul:first-child,
#ag-editor-id>ol:first-child {
margin-top: 30px;
}
.ag-float-box { .ag-float-box {
background: #303133; background: #303133;
border: 1px solid #303133; border: 1px solid #303133;
@ -350,26 +345,13 @@ table tr td:last-child {
margin-bottom: 0; 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 */ /* custom add */
code { span.ag-line code,
th code,
td code {
border: none; border: none;
padding: 2px 4px; padding: 2px 4px;
border-radius: 3px;
font-size: 90%; font-size: 90%;
color: #efefef; color: #efefef;
background-color: #606266; background-color: #606266;
@ -428,7 +410,6 @@ code {
border-color: #333; border-color: #333;
} }
#ag-editor-id pre.ag-code-block,
#ag-editor-id pre.ag-html-block { #ag-editor-id pre.ag-html-block {
font-size: 90%; font-size: 90%;
line-height: 1.6; line-height: 1.6;
@ -436,11 +417,134 @@ code {
color: #777777; 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 { .ag-color-dark {
color: #c6c6c6; 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-family: 'Open Sans';
font-style: normal; font-style: normal;
font-weight: 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-face {
font-family: 'Open Sans'; font-family: 'Open Sans';
font-style: italic; font-style: italic;
font-weight: normal; 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-face {
font-family: 'Open Sans'; font-family: 'Open Sans';
font-style: normal; font-style: normal;
font-weight: bold; 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-face {
font-family: 'Open Sans'; font-family: 'Open Sans';
font-style: italic; font-style: italic;
font-weight: bold; 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 { html, body {
@ -65,11 +65,6 @@ body {
caret-color: #000000; caret-color: #000000;
} }
#ag-editor-id>ul:first-child,
#ag-editor-id>ol:first-child {
margin-top: 30px;
}
.ag-gray { .ag-gray {
color: #C0C4CC; color: #C0C4CC;
text-decoration: none; text-decoration: none;
@ -322,32 +317,28 @@ table tr td:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.CodeMirror-gutters { span code,
border-right: 1px solid #ddd; td code,
} th code {
background-color: rgba(27, 31, 35, 0.05);
code,
tt {
border: 1px solid #ddd;
background-color: #f8f8f8;
border-radius: 3px; border-radius: 3px;
padding: 0; padding: 0;
font-family: Consolas, "Liberation Mono", Courier, monospace; font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
padding: 2px 4px 0px 4px; padding: 2px 4px 0px 4px;
font-size: 0.9em; font-size: 85%;
margin: 0;
padding: 0.2em 0.4em;
color: #24292e;
} }
:not(pre) > code[class*="language-"],
/* custom add */ pre {
code { font-size: 90%;
padding: 0.2em 0.4em; line-height: 1.6;
margin: 0; background: #f6f8fa;
font-size: 85%; border: 0;
background-color: rgba(27,31,35,0.05);
border-radius: 3px; border-radius: 3px;
border: none; color: #777777;
color: #24292e;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
} }
@media print { @media print {
@ -376,16 +367,6 @@ code {
border-bottom-color: #333333; 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 { #ag-editor-id pre.ag-html-block {
background: transparent; background: transparent;
padding: 0 .5rem; padding: 0 .5rem;
@ -403,3 +384,116 @@ p:not(.ag-active)[data-role="hr"]::before {
.fg-color-dark { .fg-color-dark {
color: #303133; 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 => { this.editor.on('selectionChange', changes => {
const { y } = changes.cursorCoords const { y } = changes.cursorCoords
if (this.typewriter) { if (this.typewriter) {
animatedScrollTo(container, container.scrollTop + y - STANDAR_Y, 100) animatedScrollTo(container, container.scrollTop + y - STANDAR_Y, 100)
} }

View File

@ -8,7 +8,7 @@
</template> </template>
<script> <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 { wordCount as getWordCount } from 'muya/lib/utils'
import { adjustCursor } from '../../util' import { adjustCursor } from '../../util'
import bus from '../../bus' import bus from '../../bus'