Support multiple lines math input (#294)

* change another way to render math

* open\save\edit multiple lines math block

* rewrite header label style

* inline code style update

* update dark theme style

* update change log

* typo error

* update webpack to v4

* update license

* fix unexpected to delete math preview block

* fix cursor error when change mode
This commit is contained in:
冉四夕 2018-05-26 00:58:16 +08:00 committed by GitHub
parent 1a7a3d5c06
commit 16bb1de82e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 4837 additions and 1632 deletions

View File

@ -51,7 +51,7 @@ function startRenderer () {
compiler.plugin('compilation', compilation => {
compilation.plugin('html-webpack-plugin-after-emit', (data, cb) => {
hotMiddleware.publish({ action: 'reload' })
cb()
cb && cb()
})
})

View File

@ -5,10 +5,10 @@ process.env.BABEL_ENV = 'main'
const path = require('path')
const { dependencies } = require('../package.json')
const webpack = require('webpack')
const proMode = process.env.NODE_ENV === 'production'
const BabiliWebpackPlugin = require('babili-webpack-plugin')
let mainConfig = {
const mainConfig = {
mode: 'development',
entry: {
main: path.join(__dirname, '../src/main/index.js')
},
@ -40,8 +40,8 @@ let mainConfig = {
]
},
node: {
__dirname: process.env.NODE_ENV !== 'production',
__filename: process.env.NODE_ENV !== 'production'
__dirname: !proMode,
__filename: !proMode
},
output: {
filename: '[name].js',
@ -60,7 +60,7 @@ let mainConfig = {
/**
* Adjust mainConfig for development settings
*/
if (process.env.NODE_ENV !== 'production') {
if (!proMode) {
mainConfig.plugins.push(
new webpack.DefinePlugin({
'__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
@ -71,12 +71,10 @@ if (process.env.NODE_ENV !== 'production') {
/**
* Adjust mainConfig for production settings
*/
if (process.env.NODE_ENV === 'production') {
if (proMode) {
mainConfig.mode = 'production'
mainConfig.plugins.push(
new BabiliWebpackPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
})
// new BabiliWebpackPlugin()
)
}

View File

@ -3,14 +3,14 @@
process.env.BABEL_ENV = 'renderer'
const path = require('path')
const { dependencies } = require('../package.json')
const webpack = require('webpack')
const BabiliWebpackPlugin = require('babili-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const { dependencies } = require('../package.json')
const proMode = process.env.NODE_ENV === 'production'
/**
* List of node_modules to include in webpack bundle
*
@ -18,9 +18,10 @@ const HtmlWebpackPlugin = require('html-webpack-plugin')
* that provide pure *.vue files that need compiling
* https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals
*/
let whiteListedModules = ['vue']
const whiteListedModules = ['vue']
let rendererConfig = {
const rendererConfig = {
mode: 'development',
devtool: '#cheap-module-eval-source-map',
entry: {
renderer: path.join(__dirname, '../src/renderer/main.js')
@ -43,10 +44,10 @@ let rendererConfig = {
},
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader'
})
use: [
proMode ? MiniCssExtractPlugin.loader : 'style-loader',
"css-loader"
]
},
{
test: /\.html$/,
@ -64,14 +65,7 @@ let rendererConfig = {
{
test: /\.vue$/,
use: {
loader: 'vue-loader',
options: {
extractCSS: process.env.NODE_ENV === 'production',
loaders: {
sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
scss: 'vue-style-loader!css-loader!sass-loader'
}
}
loader: 'vue-loader'
}
},
{
@ -109,7 +103,6 @@ let rendererConfig = {
__filename: process.env.NODE_ENV !== 'production'
},
plugins: [
new ExtractTextPlugin('styles.css'),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, '../src/index.ejs'),
@ -123,7 +116,8 @@ let rendererConfig = {
: false
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
new webpack.NoEmitOnErrorsPlugin(),
new VueLoaderPlugin()
],
output: {
filename: '[name].js',
@ -154,11 +148,16 @@ if (process.env.NODE_ENV !== 'production') {
/**
* Adjust rendererConfig for production settings
*/
if (process.env.NODE_ENV === 'production') {
if (proMode) {
rendererConfig.devtool = ''
rendererConfig.mode = 'production'
rendererConfig.plugins.push(
new BabiliWebpackPlugin(),
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: '[name].[hash].css',
chunkFilename: '[id].[hash].css'
}),
new CopyWebpackPlugin([
{
from: path.join(__dirname, '../static'),
@ -170,9 +169,6 @@ if (process.env.NODE_ENV === 'production') {
to: path.join(__dirname, '../dist/electron/codemirror/mode/[name]/[name].js')
}
]),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
new webpack.LoaderOptionsPlugin({
minimize: true
})

View File

@ -5,12 +5,15 @@ process.env.BABEL_ENV = 'web'
const path = require('path')
const webpack = require('webpack')
const BabiliWebpackPlugin = require('babili-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
let webConfig = {
const proMode = process.env.NODE_ENV === 'production'
const webConfig = {
mode: 'development',
devtool: '#cheap-module-eval-source-map',
entry: {
web: path.join(__dirname, '../src/renderer/main.js')
@ -30,10 +33,10 @@ let webConfig = {
},
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader'
})
use: [
proMode ? MiniCssExtractPlugin.loader : 'style-loader',
"css-loader"
]
},
{
test: /\.html$/,
@ -48,14 +51,7 @@ let webConfig = {
{
test: /\.vue$/,
use: {
loader: 'vue-loader',
options: {
extractCSS: true,
loaders: {
sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
scss: 'vue-style-loader!css-loader!sass-loader'
}
}
loader: 'vue-loader'
}
},
{
@ -81,7 +77,6 @@ let webConfig = {
]
},
plugins: [
new ExtractTextPlugin('styles.css'),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, '../src/index.ejs'),
@ -96,7 +91,8 @@ let webConfig = {
'process.env.IS_WEB': 'true'
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
new webpack.NoEmitOnErrorsPlugin(),
new VueLoaderPlugin()
],
output: {
filename: '[name].js',
@ -115,11 +111,17 @@ let webConfig = {
/**
* Adjust webConfig for production settings
*/
if (process.env.NODE_ENV === 'production') {
if (proMode) {
webConfig.devtool = ''
webConfig.mode ='production'
webConfig.plugins.push(
new BabiliWebpackPlugin(),
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: '[name].[hash].css',
chunkFilename: '[id].[hash].css'
}),
new CopyWebpackPlugin([
{
from: path.join(__dirname, '../static'),
@ -127,9 +129,6 @@ if (process.env.NODE_ENV === 'production') {
ignore: ['.*']
}
]),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
new webpack.LoaderOptionsPlugin({
minimize: true
})

View File

@ -1,4 +1,4 @@
### 0.11.37
### 0.11.38
**:cactus:Feature**
@ -11,6 +11,7 @@
- feature: Support `setext` heading but the default heading style is `atx`
- feature: User list item marker setting in preference file.
- feature: Select text from selected table (cell) only if you press Ctrl+A
- feature: Support Multiple lines math #242
**:butterfly:Optimization**

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017-2018 Jocs
Copyright (c) 2017-Present Jocs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,6 +0,0 @@
!macro customUnInstall
MessageBox MB_YESNO "Do you want to delete user settings?" /SD IDNO IDNO SkipRemoval
SetShellVarContext current
RMDir /r "$APPDATA\marktext"
SkipRemoval:
!macroend

View File

0
dist/web/.gitkeep vendored
View File

5543
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "marktext",
"version": "0.10.21",
"author": "Jocs <luoran1988@126.com>",
"author": "Jocs <ransixi@gmail.com>",
"description": "Next generation markdown editor",
"license": "MIT",
"main": "./dist/electron/main.js",
@ -105,61 +105,59 @@
"codemirror": "^5.36.0",
"css-tree": "^1.0.0-alpha.28",
"dompurify": "^1.0.3",
"electron-window-state": "^4.1.1",
"element-ui": "^2.3.3",
"element-ui": "^2.3.9",
"file-icons-js": "^1.0.3",
"fs-extra": "^5.0.0",
"fs-extra": "^6.0.1",
"fuzzaldrin": "^2.1.0",
"html-tags": "^2.0.0",
"katex": "^0.9.0",
"katex": "^0.10.0-alpha",
"mousetrap": "^1.6.1",
"parse5": "^4.0.0",
"parse5": "^5.0.0",
"snabbdom": "^0.7.1",
"snabbdom-to-html": "^5.1.1",
"snabbdom-virtualize": "^0.7.0",
"turndown": "^4.0.1",
"turndown-plugin-gfm": "^1.0.1",
"turndown": "^4.0.2",
"turndown-plugin-gfm": "^1.0.2",
"vue": "^2.5.16",
"vue-electron": "^1.0.6",
"vuex": "^2.3.1"
"vuex": "^3.0.1"
},
"devDependencies": {
"babel-core": "^6.26.3",
"babel-eslint": "^7.2.3",
"babel-loader": "^7.1.1",
"babel-plugin-component": "^1.1.0",
"babel-eslint": "^8.2.3",
"babel-loader": "^7.1.4",
"babel-plugin-component": "^1.1.1",
"babel-plugin-istanbul": "^4.1.6",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.0",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-0": "^6.24.1",
"babel-register": "^6.24.1",
"babili-webpack-plugin": "^0.1.2",
"cfonts": "^1.2.0",
"babel-register": "^6.26.0",
"cfonts": "^2.1.2",
"chai": "^4.0.0",
"chalk": "^2.1.0",
"copy-webpack-plugin": "^4.0.1",
"cross-env": "^5.0.5",
"css-loader": "^0.28.4",
"chalk": "^2.4.1",
"copy-webpack-plugin": "^4.5.1",
"cross-env": "^5.1.6",
"css-loader": "^0.28.11",
"del": "^3.0.0",
"devtron": "^1.4.0",
"electron": "^2.0.2",
"electron-builder": "^20.14.7",
"electron-debug": "^1.5.0",
"electron-devtools-installer": "^2.2.0",
"electron-updater": "^2.21.8",
"electron-devtools-installer": "^2.2.4",
"electron-updater": "^2.21.10",
"electron-window-state": "^4.1.1",
"eslint": "^4.19.1",
"eslint-config-standard": "^10.2.1",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.9.0",
"eslint-plugin-html": "^3.1.1",
"eslint-plugin-import": "^2.10.0",
"eslint-plugin-node": "^5.1.1",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^3.0.1",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^0.11.2",
"html-webpack-plugin": "^2.30.1",
"inject-loader": "^3.0.0",
"eslint-config-standard": "^11.0.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.0.0",
"eslint-plugin-html": "^4.0.3",
"eslint-plugin-import": "^2.12.0",
"eslint-plugin-node": "^6.0.1",
"eslint-plugin-promise": "^3.8.0",
"eslint-plugin-standard": "^3.1.0",
"file-loader": "^1.1.11",
"html-webpack-plugin": "^3.2.0",
"inject-loader": "^4.0.1",
"karma": "^1.3.0",
"karma-chai": "^0.1.0",
"karma-coverage": "^1.1.1",
@ -168,20 +166,22 @@
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "^0.0.31",
"karma-webpack": "^2.0.1",
"mini-css-extract-plugin": "^0.4.0",
"mocha": "^3.0.2",
"multispinner": "^0.2.1",
"node-loader": "^0.6.0",
"require-dir": "^0.3.0",
"spectron": "^3.7.1",
"style-loader": "^0.18.2",
"url-loader": "^0.5.9",
"require-dir": "^1.0.0",
"spectron": "^3.8.0",
"style-loader": "^0.21.0",
"url-loader": "^1.0.1",
"vue-html-loader": "^1.2.4",
"vue-loader": "^14.2.2",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.4.2",
"webpack": "^3.5.2",
"webpack-dev-server": "^2.7.1",
"webpack-hot-middleware": "^2.22.0",
"webpack-merge": "^4.1.0"
"vue-loader": "^15.2.0",
"vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.5.16",
"webpack": "^4.8.3",
"webpack-cli": "^2.1.4",
"webpack-dev-server": "^3.1.4",
"webpack-hot-middleware": "^2.22.2",
"webpack-merge": "^4.1.2"
}
}

View File

@ -2,9 +2,10 @@ import { generateKeyHash, genUpper2LowerKeyHash, getLongUniqueId } from './utils
import htmlTags from 'html-tags'
import voidHtmlTags from 'html-tags/void'
export const UNDO_DEPTH = 100
// [0.25, 0.5, 1, 2, 4, 8] <—?—> [256M, 500M/768M, 1G/1000M, 2G, 4G, 8G]
export const DEVICE_MEMORY = navigator.deviceMemory // Get the divice memory number
// Electron 2.0.2 not support yet! So give a default value 4
export const DEVICE_MEMORY = navigator.deviceMemory || 4 // Get the divice memory number(Chrome >= 63)
export const UNDO_DEPTH = DEVICE_MEMORY >= 4 ? 100 : 50
export const HAS_TEXT_BLOCK_REG = /^(h\d|span|th|td|hr|pre)/i
export const VOID_HTML_TAGS = voidHtmlTags
export const HTML_TAGS = htmlTags
@ -80,6 +81,7 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_HTML_ESCAPE',
'AG_FRONT_MATTER',
'AG_FRONT_MATTER_LINE',
'AG_MULTIPLE_MATH_LINE',
'AG_CODEMIRROR_BLOCK',
'AG_SHOW_PREVIEW',
'AG_HTML_PREVIEW',
@ -113,7 +115,11 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
'AG_MATH_TEXT',
'AG_MATH_RENDER',
'AG_MATH_ERROR',
'AG_MATH_EMPTY',
'AG_MATH_MARKER',
'AG_MATH_PREVIEW',
'AG_MULTIPLE_MATH_BLOCK',
'AG_MULTIPLE_MATH',
'AG_LOOSE_LIST_ITEM',
'AG_TIGHT_LIST_ITEM',
'AG_HTML_TAG',

View File

@ -89,7 +89,7 @@ const arrowCtrl = ContentState => {
return
}
if (block.type === 'pre' && block.functionType !== 'frontmatter') {
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
@ -205,8 +205,8 @@ const arrowCtrl = ContentState => {
}
if (
(preBlock && preBlock.type === 'pre' && preBlock.functionType !== 'frontmatter' && event.key === EVENT_KEYS.ArrowUp) ||
(preBlock && preBlock.type === 'pre' && preBlock.functionType !== 'frontmatter' && event.key === EVENT_KEYS.ArrowLeft && left === 0)
(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()
@ -222,8 +222,8 @@ const arrowCtrl = ContentState => {
return this.partialRender()
} else if (
(nextBlock && nextBlock.type === 'pre' && nextBlock.functionType !== 'frontmatter' && event.key === EVENT_KEYS.ArrowDown) ||
(nextBlock && nextBlock.type === 'pre' && nextBlock.functionType !== 'frontmatter' && event.key === EVENT_KEYS.ArrowRight && right === 0)
(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()

View File

@ -104,6 +104,7 @@ const backspaceCtrl = ContentState => {
const { start, end } = selection.getCursorRange()
const startBlock = this.getBlock(start.key)
const endBlock = this.getBlock(end.key)
// fix: #67 problem 1
if (startBlock.icon) return event.preventDefault()
// fix: unexpect remove all editor html. #67 problem 4
@ -131,11 +132,11 @@ const backspaceCtrl = ContentState => {
if (start.key !== end.key) {
event.preventDefault()
const { key, offset } = start
const startRemainText = startBlock.type === 'pre' && startBlock.functionType !== 'frontmatter'
const startRemainText = startBlock.type === 'pre' && /code|html/.test(startBlock.functionType)
? startBlock.text.substring(0, offset - 1)
: startBlock.text.substring(0, offset)
const endRemainText = endBlock.type === 'pre' && endBlock.functionType !== 'frontmatter'
const endRemainText = endBlock.type === 'pre' && /code|html/.test(endBlock.functionType)
? endBlock.text.substring(end.offset - 1)
: endBlock.text.substring(end.offset)
@ -183,7 +184,7 @@ const backspaceCtrl = ContentState => {
return tHeadHasContent || tBodyHasContent
}
if (block.type === 'pre' && block.functionType !== 'frontmatter') {
if (block.type === 'pre' && /code|html/.test(block.functionType)) {
const cm = this.codeBlocks.get(id)
// if event.preventDefault(), you can not use backspace in language input.
if (isCursorAtBegin(cm) && onlyHaveOneLine(cm)) {
@ -204,9 +205,10 @@ const backspaceCtrl = ContentState => {
this.partialRender()
}
} else if (
block.type === 'span' && block.functionType === 'frontmatter' &&
left === 0 && !preBlock
block.type === 'span' && /frontmatter|multiplemath/.test(block.functionType) &&
left === 0 && !block.preSibling
) {
const isMathLine = block.functionType === 'multiplemath'
event.preventDefault()
event.stopPropagation()
const { key } = block
@ -216,6 +218,9 @@ const backspaceCtrl = ContentState => {
delete line.functionType
this.appendChild(pBlock, line)
}
if (isMathLine) {
parent = this.getParent(parent)
}
this.insertBefore(pBlock, parent)
this.removeBlock(parent)
@ -326,7 +331,7 @@ const backspaceCtrl = ContentState => {
const { text } = block
const key = preBlock.key
const offset = preBlock.text.length
if (preBlock.type === 'pre' && preBlock.functionType !== 'frontmatter') {
if (preBlock.type === 'pre' && /code|html/.test(preBlock.functionType)) {
const cm = this.codeBlocks.get(key)
const value = cm.getValue() + text
cm.setValue(value)

View File

@ -52,7 +52,7 @@ const copyCutCtrl = ContentState => {
$(
`.${CLASS_OR_ID['AG_REMOVE']}, .${CLASS_OR_ID['AG_TOOL_BAR']},
.${CLASS_OR_ID['AG_MATH_RENDER']}, .${CLASS_OR_ID['AG_HTML_PREVIEW']},
.${CLASS_OR_ID['AG_COPY_REMOVE']}`
.${CLASS_OR_ID['AG_MATH_PREVIEW']}, .${CLASS_OR_ID['AG_COPY_REMOVE']}`
).remove()
$(`.${CLASS_OR_ID['AG_EMOJI_MARKER']}`).text(':')
$(`.${CLASS_OR_ID['AG_NOTEXT_LINK']}`).empty()
@ -102,6 +102,18 @@ const copyCutCtrl = ContentState => {
})
}
const mathBlock = $(`figure.ag-multiple-math-block`)
if (mathBlock.length > 0) {
mathBlock.each((i, hb) => {
const ele = $(hb)
const id = ele.attr('id')
const { math } = this.getBlock(id).children[1]
const pre = $('<pre class="multiple-math"></pre>')
pre.text(math)
ele.replaceWith(pre)
})
}
event.clipboardData.setData('text/html', $('body').html())
event.clipboardData.setData('text/plain', text)
}

View File

@ -150,7 +150,7 @@ const enterCtrl = ContentState => {
return
}
// handle cursor in code block
if (block.type === 'pre' && block.functionType !== 'frontmatter') {
if (block.type === 'pre' && block.functionType === 'code') {
return
}
@ -197,15 +197,13 @@ const enterCtrl = ContentState => {
// only cursor in `line block` can create `soft line break` and `hard line break`
if (
(event.shiftKey && block.type === 'span') ||
(block.type === 'span' && block.functionType === 'frontmatter')
(block.type === 'span' && /frontmatter|multiplemath/.test(block.functionType))
) {
const { text } = block
const newLineText = text.substring(start.offset)
block.text = text.substring(0, start.offset)
const newLine = this.createBlock('span', newLineText)
if (block.functionType === 'frontmatter') {
newLine.functionType = 'frontmatter'
}
newLine.functionType = block.functionType
this.insertAfter(newLine, block)
const { key } = newLine
const offset = 0
@ -374,6 +372,7 @@ const enterCtrl = ContentState => {
const blockNeedFocus = this.codeBlockUpdate(preParagraphBlock)
let tableNeedFocus = this.tableBlockUpdate(preParagraphBlock)
let htmlNeedFocus = this.updateHtmlBlock(preParagraphBlock)
let mathNeedFocus = this.updateMathBlock(preParagraphBlock)
let cursorBlock
switch (true) {
@ -386,6 +385,9 @@ const enterCtrl = ContentState => {
case !!htmlNeedFocus:
cursorBlock = htmlNeedFocus
break
case !!mathNeedFocus:
cursorBlock = mathNeedFocus
break
default:
cursorBlock = newBlock
break

View File

@ -8,8 +8,7 @@ const DOMPurify = createDOMPurify(window)
const htmlBlock = ContentState => {
ContentState.prototype.createToolBar = function (tools, toolBarType) {
const toolBar = this.createBlock('div')
toolBar.editable = false
const toolBar = this.createBlock('div', '', false)
toolBar.toolBarType = toolBarType
const ul = this.createBlock('ul')
@ -28,8 +27,7 @@ const htmlBlock = ContentState => {
ContentState.prototype.createCodeInHtml = function (code, selection) {
const codeContainer = this.createBlock('div')
codeContainer.functionType = 'html'
const preview = this.createBlock('div')
preview.editable = false
const preview = this.createBlock('div', '', false)
preview.htmlContent = DOMPurify.sanitize(escapeInBlockHtml(code), DOMPURIFY_CONFIG)
preview.functionType = 'preview'
const codePre = this.createBlock('pre')

View File

@ -51,7 +51,6 @@ class ContentState {
this.blocks = [ this.createBlockP() ]
this.stateRender = new StateRender(eventCenter)
this.codeBlocks = new Map()
this.loadMathMap = new Map()
this.renderRange = [ null, null ]
this.currentCursor = null
this.prevCursor = null
@ -122,7 +121,7 @@ class ContentState {
setCursor () {
const { start: { key } } = this.cursor
const block = this.getBlock(key)
if (block.type === 'pre' && block.functionType !== 'frontmatter') {
if (block.type === 'pre' && /code|html/.test(block.functionType)) {
const cm = this.codeBlocks.get(key)
const { selection } = block
if (selection) {
@ -156,7 +155,6 @@ class ContentState {
this.setNextRenderRange()
this.stateRender.render(blocks, cursor, activeBlocks, matches)
this.pre2CodeMirror(isRenderCursor)
this.renderMath()
if (isRenderCursor) this.setCursor()
}
@ -175,7 +173,6 @@ class ContentState {
this.setNextRenderRange()
this.stateRender.partialRender(needRenderBlocks, cursor, activeBlocks, matches, startKey, endKey)
this.pre2CodeMirror(true, [...new Set([cursorOutMostBlock, ...needRenderBlocks])])
this.renderMath([...new Set([cursorOutMostBlock, ...needRenderBlocks])])
this.setCursor()
}
@ -183,12 +180,13 @@ class ContentState {
* A block in Aganippe present a paragraph(block syntax in GFM) or a line in paragraph.
* a line block must in a `p block` or `pre block(frontmatter)` and `p block`'s children must be line blocks.
*/
createBlock (type = 'span', text = '') { // span type means it is a line block.
createBlock (type = 'span', text = '', editable = true) { // span type means it is a line block.
const key = getUniqueId()
return {
key,
type,
text,
editable,
parent: null,
preSibling: null,
nextSibling: null,
@ -310,7 +308,7 @@ class ContentState {
if (children.length) {
children.forEach(child => this.removeTextOrBlock(child))
}
} else {
} else if (block.editable) {
this.removeBlock(block)
}
}
@ -365,8 +363,8 @@ class ContentState {
if (!afterEnd) {
const parent = this.getParent(after)
if (parent) {
const isOnlyChild = this.isOnlyChild(after)
this.removeBlocks(before, parent, isOnlyChild, true)
const removeAfter = isRemoveAfter && this.isOnlyEditableChild(after)
this.removeBlocks(before, parent, removeAfter, true)
}
}
if (isRemoveAfter) {
@ -505,6 +503,13 @@ class ContentState {
return !block.nextSibling && !block.preSibling
}
isOnlyEditableChild (block) {
if (block.editable === false) return false
const parent = this.getParent(block)
if (!parent) throw new Error('isOnlyEditableChild method only apply for child block')
return parent.children.filter(child => child.editable).length === 1
}
getLastChild (block) {
if (block) {
const len = block.children.length

View File

@ -1,40 +1,69 @@
import katex from 'katex'
import { CLASS_OR_ID } from '../config'
import 'katex/dist/katex.min.css'
// import { CLASS_OR_ID } from '../config'
const LINE_BREAKS_REG = /\n/
const mathCtrl = ContentState => {
ContentState.prototype.renderMath = function (blocks) {
let selector = ''
if (blocks) {
selector = blocks.map(block => `#${block.key} .${CLASS_OR_ID['AG_MATH_RENDER']}`).join(', ')
ContentState.prototype.createMathBlock = function (value = '') {
const FUNCTION_TYPE = 'multiplemath'
const mathBlock = this.createBlock('figure')
const textArea = this.createBlock('pre')
const mathPreview = this.createBlock('div', '', false)
if (typeof value === 'string' && value) {
const lines = value.replace(/^\s+/, '').split(LINE_BREAKS_REG).map(line => this.createBlock('span', line))
for (const line of lines) {
line.functionType = FUNCTION_TYPE
this.appendChild(textArea, line)
}
} else {
selector = `.${CLASS_OR_ID['AG_MATH_RENDER']}`
const emptyLine = this.createBlock('span')
emptyLine.functionType = FUNCTION_TYPE
this.appendChild(textArea, emptyLine)
}
const mathEles = document.querySelectorAll(selector)
const { loadMathMap } = this
for (const math of mathEles) {
const content = math.getAttribute('data-math')
const type = math.getAttribute('data-type')
const displayMode = type === 'display_math'
const key = `${content}_${type}`
if (loadMathMap.has(key)) {
math.innerHTML = loadMathMap.get(key)
continue
}
try {
const html = katex.renderToString(content, {
displayMode
})
loadMathMap.set(key, html)
math.innerHTML = html
} catch (err) {
math.innerHTML = 'Invalid'
math.classList.add(CLASS_OR_ID['AG_MATH_ERROR'])
}
mathBlock.functionType = textArea.functionType = mathPreview.functionType = FUNCTION_TYPE
mathPreview.math = value
this.appendChild(mathBlock, textArea)
this.appendChild(mathBlock, mathPreview)
return mathBlock
}
ContentState.prototype.initMathBlock = function (block) { // p block
const FUNCTION_TYPE = 'multiplemath'
const textArea = this.createBlock('pre')
const emptyLine = this.createBlock('span')
textArea.functionType = emptyLine.functionType = FUNCTION_TYPE
this.appendChild(textArea, emptyLine)
block.type = 'figure'
block.functionType = FUNCTION_TYPE
block.children = []
const mathPreview = this.createBlock('div', '', false)
mathPreview.math = ''
mathPreview.functionType = FUNCTION_TYPE
this.appendChild(block, textArea)
this.appendChild(block, mathPreview)
return emptyLine
}
ContentState.prototype.handleMathBlockClick = function (mathFigure) {
const { id } = mathFigure
const mathBlock = this.getBlock(id)
const textAreaBlock = mathBlock.children[0]
const firstLine = textAreaBlock.children[0]
const { key } = firstLine
const offset = 0
this.cursor = {
start: { key, offset },
end: { key, offset }
}
this.partialRender()
}
ContentState.prototype.updateMathBlock = function (block) {
const { type } = block
if (type !== 'p') return false
const { text } = block.children[0]
return text.trim() === '$$' ? this.initMathBlock(block) : false
}
}

View File

@ -318,6 +318,27 @@ const paragraphCtrl = ContentState => {
}
}
ContentState.prototype.insertMathBlock = function () {
const { start, end } = selection.getCursorRange()
if (start.key !== end.key) return
let block = this.getBlock(start.key)
if (block.type === 'span') {
block = this.getParent(block)
}
const mathBlock = this.createMathBlock()
this.insertAfter(mathBlock, block)
if (block.type === 'p' && block.children.length === 1 && !block.children[0].text) {
this.removeBlock(block)
}
const cursorBlock = mathBlock.children[0].children[0]
const { key } = cursorBlock
const offset = 0
this.cursor = {
start: { key, offset },
end: { key, offset }
}
}
ContentState.prototype.updateParagraph = function (paraType) {
const { start, end } = selection.getCursorRange()
const block = this.getBlock(start.key)
@ -346,6 +367,10 @@ const paragraphCtrl = ContentState => {
this.handleQuoteMenu()
break
}
case 'mathblock': {
this.insertMathBlock()
break
}
case 'heading 1':
case 'heading 2':
case 'heading 3':

View File

@ -131,10 +131,15 @@ const pasteCtrl = ContentState => {
throw new Error('unknown paste type')
}
// step 3: set cursor and render
const cursorBlock = this.getBlock(key)
let cursorBlock = this.getBlock(key)
if (!cursorBlock) {
key = startBlock.key
offset = startBlock.text.length - cacheText.length
cursorBlock = startBlock
}
// TODO @Jocs duplicate with codes in updateCtrl.js
if (cursorBlock && cursorBlock.type === 'span' && cursorBlock.functionType === 'multiplemath') {
this.updateMathContent(cursorBlock)
}
this.cursor = {
start: {

View File

@ -284,6 +284,13 @@ const updateCtrl = ContentState => {
return null
}
ContentState.prototype.updateMathContent = function (block) {
const preBlock = this.getParent(block)
const mathPreview = this.getNextSibling(preBlock)
const math = preBlock.children.map(line => line.text).join('\n')
mathPreview.math = math
}
ContentState.prototype.updateState = function (event) {
const { floatBox } = this
const { start, end } = selection.getCursorRange()
@ -314,7 +321,7 @@ const updateCtrl = ContentState => {
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' && startBlock.functionType !== 'frontmatter') {
if (startBlock.type === 'pre' && /code|html/.test(startBlock.functionType)) {
event.preventDefault()
const startRemainText = startBlock.type === 'pre'
? startBlock.text.substring(0, oldStart.offset - 1)
@ -379,7 +386,7 @@ const updateCtrl = ContentState => {
}
}
if (block && block.type === 'pre' && block.functionType !== 'frontmatter') {
if (block && block.type === 'pre' && /code|html/.test(block.functionType)) {
if (block.key !== oldKey) {
this.cursor = lastCursor = { start, end }
if (event.type === 'click' && oldKey) {
@ -388,6 +395,7 @@ const updateCtrl = ContentState => {
}
return
}
// auto pair
if (block && block.text !== text) {
const BRACKET_HASH = {
@ -423,13 +431,16 @@ const updateCtrl = ContentState => {
block.text = text
}
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)
const inlineUpdatedBlock = this.isCollapse() && block.functionType !== 'frontmatter' && this.checkInlineUpdate(block)
const inlineUpdatedBlock = this.isCollapse() && !/frontmatter|multiplemath/.test(block.functionType) && this.checkInlineUpdate(block)
if (checkMarkedUpdate || inlineUpdatedBlock || needRender) {
this.partialRender()
}

View File

@ -10,12 +10,12 @@
}
}
h1.ag-active::before,
h2.ag-active::before,
h3.ag-active::before,
h4.ag-active::before,
h5.ag-active::before,
h6.ag-active::before {
#ag-editor-id > h1.ag-active::before,
#ag-editor-id > h2.ag-active::before,
#ag-editor-id > h3.ag-active::before,
#ag-editor-id > h4.ag-active::before,
#ag-editor-id > h5.ag-active::before,
#ag-editor-id > h6.ag-active::before {
content: attr(data-head);
width: 20px;
height: 20px;
@ -25,12 +25,10 @@ h6.ag-active::before {
position: absolute;
top: 0;
left: -25px;
border: 1px solid #C0C4CC;
border-radius: 3px;
font-size: 12px;
font-size: 14px;
color: #C0C4CC;
transform: scale(.7);
font-weight: 100;
font-weight: 400;
font-style: italic;
}
.ag-paragraph:empty::after,
@ -43,6 +41,10 @@ h6.ag-active::before {
white-space: pre-wrap;
}
.ag-gray {
font-family: monospace;
}
/* .ag-soft-line-break::after {
content: '↓';
opacity: .5;
@ -58,6 +60,7 @@ h6.ag-active::before {
color: #303133;
}
figure pre.ag-multiple-math,
div.ag-function-html pre.ag-html-block {
width: 0;
height: 0;
@ -67,7 +70,8 @@ div.ag-function-html pre.ag-html-block {
position: absolute;
}
div.ag-function-html.ag-active pre.ag-html-block {
div.ag-function-html.ag-active pre.ag-html-block,
figure.ag-active pre.ag-multiple-math {
position: static;
width: 100%;
height: auto;
@ -101,37 +105,38 @@ span.ag-html-tag {
span.ag-math {
position: relative;
color: purple;
letter-spacing: 0.1em;
font-family: monospace;
display: inline-block;
vertical-align: bottom;
}
.ag-math > .ag-math-render {
display: inline-block;
background: rgb(79, 79, 79);
padding: .3em 1em;
border-radius: 5px;
color: #fff;
padding: .5rem;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
color: #333;
position: absolute;
top: 30px;
left: 0;
z-index: 10;
}
.ag-math > .ag-math-error {
color: rgba(242, 134, 94, .7);
div.ag-math-empty {
color: #999;
font-size: 14px;
font-style: italic;
font-family: monospace;
}
.ag-math > .ag-math-render::before {
content: '';
width: 10px;
height: 10px;
display: inline-block;
position: absolute;
transform: rotate(45deg) translateX(-50%);
background: rgb(79, 79, 79);
left: 10px;
top: -1px;
div.ag-math-error,
span.ag-math > .ag-math-render.ag-math-error {
color: #e6a23c;
font-size: 14px;
font-style: italic;
font-family: monospace;
}
.ag-math > .ag-math-render .katex-display {
@ -151,9 +156,11 @@ span.ag-math {
}
.ag-hide.ag-math > .ag-math-render {
padding: 0;
top: 0;
position: relative;
padding: 0;
border: none;
box-shadow: none;
background: transparent;
}
@ -164,7 +171,7 @@ span.ag-math {
figure {
padding: 0;
margin: 0;
margin-top: 25px;
margin: 1rem 0;
position: relative;
}
.ag-tool-bar {
@ -221,29 +228,18 @@ figure.ag-active .ag-tool-bar {
display: block;
}
figure[data-role=HTML] {
margin-top: 0;
}
figure.ag-active[data-role=HTML]::before,
pre.ag-active[data-role=YAML]::before {
figure.ag-active[data-role=HTML]::before {
content: attr(data-role);
width: auto;
padding: 0 .3rem;
letter-spacing: .1rem;
height: 20px;
text-align: center;
line-height: 22px;
display: block;
position: absolute;
top: 0;
left: -45px;
border: 1px solid #C0C4CC;
border-radius: 3px;
font-size: 12px;
color: #C0C4CC;
transform: scale(.7);
font-weight: 300;
font-weight: 400;
font-style: italic;
}
table {
@ -340,12 +336,17 @@ p:not(.ag-active)[data-role="hr"] * {
color: transparent;
}
pre.ag-multiple-math,
pre.ag-front-matter {
position: relative;
background: #f7f7f7;
padding: 1rem;
background: #f6f8fa;
padding: .5rem;
border: 5px;
font-size: 15px;
font-size: 14px;
margin: 0;
}
pre.ag-front-matter {
margin: 1rem 0;
}
span.ag-front-matter-line:first-of-type:empty::after {
@ -353,6 +354,11 @@ span.ag-front-matter-line:first-of-type:empty::after {
color: #999;
}
span.ag-multiple-math-line:first-of-type:empty::after {
content: 'Input Mathematical Formula...';
color: #999;
}
figure,
pre.ag-html-block,
div.ag-function-html,
@ -365,22 +371,70 @@ pre.ag-code-block {
pre.ag-code-block {
margin: 1rem 0;
padding: 0 .5rem;
}
pre.ag-active.ag-front-matter::before,
pre.ag-active.ag-front-matter::after {
content: '---';
}
pre.ag-active.ag-multiple-math::before,
pre.ag-active.ag-multiple-math::after {
content: '$$';
}
pre.ag-active.ag-code-block::before,
pre.ag-active.ag-code-block::after {
content: '```';
}
pre.ag-active.ag-front-matter::before,
pre.ag-active.ag-front-matter::after,
pre.ag-active.ag-code-block::before,
pre.ag-active.ag-code-block::after,
pre.ag-active.ag-multiple-math::before,
pre.ag-active.ag-multiple-math::after {
color: #909399;
font-family: monospace;
position: absolute;
left: 0;
}
pre.ag-active.ag-front-matter::before,
pre.ag-active.ag-multiple-math::before,
pre.ag-active.ag-code-block::before {
top: -20px;
}
pre.ag-active.ag-front-matter::after,
pre.ag-active.ag-multiple-math::after,
pre.ag-active.ag-code-block::after {
bottom: -25px;
bottom: -23px;
}
span.ag-multiple-math-line {
color: purple;
font-family: monospace;
}
figure div.ag-math-preview {
width: 100%;
text-align: center;
}
figure.ag-active div.ag-math-preview {
position: absolute;
top: calc(100% + 8px);
left: 50%;
width: auto;
z-index: 1;
transform: translateX(-50%);
padding: .5rem;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
}
div.ag-html-preview {

View File

@ -80,6 +80,7 @@ class Aganippe {
this.dispatchTableToolBar()
this.dispatchCodeBlockClick()
this.htmlPreviewClick()
this.mathPreviewClick()
contentState.listenForPathChange()
@ -429,6 +430,21 @@ class Aganippe {
eventCenter.attachDOMEvent(container, 'click', handler)
}
mathPreviewClick () {
const { eventCenter, container } = this
const handler = event => {
const target = event.target
const mathFigure = isInElement(target, 'ag-multiple-math-block')
if (mathFigure && !mathFigure.classList.contains(CLASS_OR_ID['AG_ACTIVE'])) {
event.preventDefault()
event.stopPropagation()
this.contentState.handleMathBlockClick(mathFigure)
}
}
eventCenter.attachDOMEvent(container, 'click', handler)
}
listItemCheckBoxClick () {
const { container, eventCenter } = this
const handler = event => {

View File

@ -29,7 +29,8 @@ var block = {
table: noop,
paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,
text: /^[^\n]+/,
frontmatter: /^---\n([\s\S]+?)---(?:\n+|$)/
frontmatter: /^---\n([\s\S]+?)---(?:\n+|$)/,
multiplemath: /^\$\$\n([\s\S]+?)\n\$\$(?:\n+|$)/
};
block.checkbox = /^\[([ x])\] +/;
@ -195,16 +196,25 @@ Lexer.prototype.token = function(src, top, bq) {
continue;
}
// multiple line math
if (cap = this.rules.multiplemath.exec(src)) {
src = src.substring(cap[0].length)
this.tokens.push({
type: 'multiplemath',
text: cap[1]
})
}
// fences (gfm)
if (cap = this.rules.fences.exec(src)) {
src = src.substring(cap[0].length);
src = src.substring(cap[0].length)
this.tokens.push({
type: 'code',
codeBlockStyle: 'fenced',
lang: cap[2],
text: cap[3]
});
continue;
})
continue
}
// heading
@ -822,6 +832,10 @@ Renderer.prototype.frontmatter = function (text) {
return `<pre class="front-matter">\n${text}</pre>\n`
}
Renderer.prototype.multiplemath = function (text) {
return `<pre class="multiple-math">\n${text}</pre>\n`
}
Renderer.prototype.code = function (code, lang, escaped, codeBlockStyle) {
if (this.options.highlight) {
var out = this.options.highlight(code, lang);
@ -1073,13 +1087,17 @@ Parser.prototype.tok = function() {
return this.renderer.hr()
}
case 'heading': {
return this.renderer.heading(
this.inline.output(this.token.text),
this.token.depth,
this.token.text,
this.token.headingStyle
)
}
return this.renderer.heading(
this.inline.output(this.token.text),
this.token.depth,
this.token.text,
this.token.headingStyle
)
}
case 'multiplemath': {
const { text } = this.token
return this.renderer.multiplemath(text)
}
case 'code':
{
const { codeBlockStyle, text, lang, escaped } = this.token

View File

@ -101,7 +101,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top) => {
}
if (beginRules && pos === 0) {
const beginR = ['header', 'hr', 'code_fense', 'display_math']
const beginR = ['header', 'hr', 'code_fense', 'display_math', 'multiple_math']
for (const ruleName of beginR) {
const to = beginRules[ruleName].exec(src)
@ -488,6 +488,7 @@ export const generator = tokens => {
result += token.escapeCharacter
break
case 'tail_header':
case 'multiple_math':
result += token.marker
break
case 'hard_line_break':

View File

@ -9,6 +9,7 @@ class StateRender {
constructor (eventCenter) {
this.eventCenter = eventCenter
this.loadImageMap = new Map()
this.loadMathMap = new Map()
this.tokenCache = new Map()
this.container = null
}

View File

@ -24,6 +24,9 @@ export default function renderContainerBlock (block, cursor, activeBlocks, match
if (block.functionType === 'html') { // HTML Block
Object.assign(data.dataset, { role: block.functionType.toUpperCase() })
}
if (block.functionType === 'multiplemath') {
selector += `.${CLASS_OR_ID['AG_MULTIPLE_MATH_BLOCK']}`
}
}
// hanle list block
if (/ul|ol/.test(block.type) && block.listType) {
@ -72,9 +75,11 @@ export default function renderContainerBlock (block, cursor, activeBlocks, match
if (block.type === 'ol') {
Object.assign(data.attrs, { start: block.start })
}
if (block.type === 'pre' && block.functionType === 'frontmatter') {
Object.assign(data.dataset, { role: 'YAML' })
selector += `.${CLASS_OR_ID['AG_FRONT_MATTER']}`
if (block.type === 'pre' && /frontmatter|multiplemath/.test(block.functionType)) {
const role = block.functionType === 'frontmatter' ? 'YAML' : 'MATH'
const className = block.functionType === 'frontmatter' ? CLASS_OR_ID['AG_FRONT_MATTER'] : CLASS_OR_ID['AG_MULTIPLE_MATH']
Object.assign(data.dataset, { role })
selector += `.${className}`
}
return h(selector, data, block.children.map(child => this.renderBlock(child, cursor, activeBlocks, matches, useCache)))

View File

@ -1,3 +1,4 @@
import katex from 'katex'
import { CLASS_OR_ID, DEVICE_MEMORY } from '../../../config'
import { tokenizer } from '../../parse'
import { snakeToCamel } from '../../../utils'
@ -9,13 +10,26 @@ const PRE_BLOCK_HASH = {
'frontmatter': `.${CLASS_OR_ID['AG_FRONT_MATTER']}`
}
export default function renderLeafBlock (block, cursor, activeBlocks, matches, useCache = false) {
const { loadMathMap } = this
let selector = this.getSelector(block, cursor, activeBlocks)
// highlight search key in block
const highlights = matches.filter(m => m.key === block.key)
const { text, type, headingStyle, align, htmlContent, icon, checked, key, lang, functionType, codeBlockStyle } = block
const {
text,
type,
headingStyle,
align,
htmlContent,
icon,
checked,
key,
lang,
functionType,
codeBlockStyle,
math,
editable
} = block
const data = {
attrs: {},
dataset: {}
@ -33,16 +47,41 @@ export default function renderLeafBlock (block, cursor, activeBlocks, matches, u
children = tokens.reduce((acc, token) => [...acc, ...this[snakeToCamel(token.type)](h, cursor, block, token)], [])
}
if (editable === false) {
Object.assign(data.attrs, {
contenteditable: 'false'
})
}
if (/th|td/.test(type) && align) {
Object.assign(data.attrs, {
style: `text-align:${align}`
})
} else if (type === 'div' && htmlContent !== undefined) {
selector += `.${CLASS_OR_ID['AG_HTML_PREVIEW']}`
Object.assign(data.attrs, {
contenteditable: 'false'
})
children = htmlToVNode(htmlContent)
} else if (type === 'div') {
if (typeof htmlContent === 'string') {
selector += `.${CLASS_OR_ID['AG_HTML_PREVIEW']}`
children = htmlToVNode(htmlContent)
} else if (typeof math === 'string') {
const key = `${math}_display_math`
selector += `.${CLASS_OR_ID['AG_MATH_PREVIEW']}`
if (math === '') {
children = '< Empty Mathematical Formula >'
selector += `.${CLASS_OR_ID['AG_MATH_EMPTY']}`
} else if (loadMathMap.has(key)) {
children = loadMathMap.get(key)
} else {
try {
const html = katex.renderToString(math, {
displayMode: true
})
children = htmlToVNode(html)
loadMathMap.set(key, children)
} catch (err) {
children = '< Invalid Mathematical Formula >'
selector += `.${CLASS_OR_ID['AG_MATH_ERROR']}`
}
}
}
} else if (type === 'svg' && icon) {
selector += '.icon'
Object.assign(data.attrs, {
@ -98,12 +137,17 @@ export default function renderLeafBlock (block, cursor, activeBlocks, matches, u
})
}
if (functionType !== 'frontmatter') {
if (/code|html/.test(functionType)) {
// do not set it to '' (empty string)
children = []
}
} else if (type === 'span' && functionType === 'frontmatter') {
selector += `.${CLASS_OR_ID['AG_FRONT_MATTER_LINE']}`
} else if (type === 'span' && /frontmatter|multiplemath/.test(functionType)) {
if (functionType === 'frontmatter') {
selector += `.${CLASS_OR_ID['AG_FRONT_MATTER_LINE']}`
}
if (functionType === 'multiplemath') {
selector += `.${CLASS_OR_ID['AG_MULTIPLE_MATH_LINE']}`
}
children = text
}

View File

@ -1,4 +1,8 @@
import katex from 'katex'
import { CLASS_OR_ID } from '../../../config'
import { htmlToVNode } from '../snabbdom'
import 'katex/dist/katex.min.css'
export default function displayMath (h, cursor, block, token, outerClass) {
const className = this.getClassName(outerClass, block, token, cursor)
@ -11,14 +15,34 @@ export default function displayMath (h, cursor, block, token, outerClass) {
const { content: math, type } = token
const { loadMathMap } = this
const displayMode = type === 'display_math'
const key = `${math}_${type}`
let mathVnode = null
let previewSelector = `span.${CLASS_OR_ID['AG_MATH_RENDER']}`
if (loadMathMap.has(key)) {
mathVnode = loadMathMap.get(key)
} else {
try {
const html = katex.renderToString(math, {
displayMode
})
mathVnode = htmlToVNode(html)
loadMathMap.set(key, mathVnode)
} catch (err) {
mathVnode = '< Invalid Mathematical Formula >'
previewSelector += `.${CLASS_OR_ID['AG_MATH_ERROR']}`
}
}
return [
h(`span.${className}.${CLASS_OR_ID['AG_MATH_MARKER']}`, startMarker),
h(`span.${className}.${CLASS_OR_ID['AG_MATH']}`, [
h(`span.${CLASS_OR_ID['AG_MATH_TEXT']}`, content),
h(`span.${CLASS_OR_ID['AG_MATH_RENDER']}`, {
dataset: { math, type },
h(previewSelector, {
attrs: { contenteditable: 'false' }
}, 'Loading')
}, mathVnode)
]),
h(`span.${className}.${CLASS_OR_ID['AG_MATH_MARKER']}`, endMarker)
]

View File

@ -23,6 +23,7 @@ import del from './del'
import em from './em'
import strong from './strong'
import htmlEscape from './htmlEscape'
import multipleMath from './multipleMath'
export default {
backlashInToken,
@ -49,5 +50,6 @@ export default {
del,
em,
strong,
htmlEscape
htmlEscape,
multipleMath
}

View File

@ -0,0 +1,9 @@
import { CLASS_OR_ID } from '../../../config'
export default function multipleMath (h, cursor, block, token, outerClass) {
const { start, end } = token.range
const content = this.highlight(h, block, start, end, token)
return [
h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, content)
]
}

View File

@ -5,7 +5,8 @@ export const beginRules = {
'hr': /^(\*{3,}$|^\-{3,}$|^\_{3,}$)/,
'code_fense': /^(`{3,})([^`]*)$/,
'header': /(^\s{0,3}#{1,6}(\s{1,}|$))/,
'display_math': /^(\$\$)([^\$]*?[^\$\\])(\\*)\1$/
'display_math': /^(\$\$)([^\$]*?[^\$\\])(\\*)\1$/,
'multiple_math': /^(\$\$)$/
}
export const inlineRules = {

View File

@ -55,6 +55,8 @@ class ExportMarkdown {
result.push(this.normalizeTable(table, indent))
} else if (block.functionType === 'html') {
result.push(this.normalizeHTML(block, indent))
} else if (block.functionType === 'multiplemath') {
result.push(this.normalizeMultipleMath(block, indent))
}
break
@ -159,6 +161,16 @@ class ExportMarkdown {
return result.join('')
}
normalizeMultipleMath (block, /* figure */ indent) {
const result = []
result.push('$$\n')
for (const line of block.children[0].children) {
result.push(`${line.text}\n`)
}
result.push('$$\n')
return result.join('')
}
normalizeCodeBlock (block, indent) {
const result = []
const textList = block.text.split(LINE_BREAKS)

View File

@ -65,12 +65,15 @@ const importRegister = ContentState => {
return { lang, codeBlockStyle }
}
const isFrontMatter = node => {
const getPreFunctionType = node => {
let type = 'code'
const classAttr = node.attrs.filter(attr => attr.name === 'class')[0]
if (classAttr && classAttr.value) {
return /front-matter/.test(classAttr.value)
const { value } = classAttr
if (/front-matter/.test(value)) type = 'frontmatter'
if (/multiple-math/.test(value)) type = 'multiplemath'
}
return false
return type
}
const getRowColumnCount = childNodes => {
@ -219,17 +222,20 @@ const importRegister = ContentState => {
break
case 'pre':
const frontMatter = isFrontMatter(child)
if (frontMatter) {
const functionType = getPreFunctionType(child)
if (functionType === 'frontmatter') {
value = child.childNodes[0].value
block = this.createBlock('pre')
const lines = value.replace(/^\s+/, '').split(LINE_BREAKS_REG).map(line => this.createBlock('span', line))
for (const line of lines) {
line.functionType = 'frontmatter'
line.functionType = functionType
this.appendChild(block, line)
}
block.functionType = 'frontmatter'
} else {
block.functionType = functionType
} else if (functionType === 'multiplemath') {
value = child.childNodes[0].value
block = this.createMathBlock(value)
} else if (functionType === 'code') {
const codeNode = child.childNodes[0]
const { lang, codeBlockStyle } = getLangAndType(codeNode)
value = codeNode.childNodes[0].value
@ -343,19 +349,29 @@ const importRegister = ContentState => {
// set cursor
const travel = blocks => {
for (const block of blocks) {
const { key, text, children } = block
const { key, text, children, editable, type, functionType } = block
if (text) {
const offset = text.indexOf(CURSOR_DNA)
if (offset > -1) {
block.text = text.substring(0, offset) + text.substring(offset + CURSOR_DNA.length)
this.cursor = {
start: { key, offset },
end: { key, offset }
if (editable) {
this.cursor = {
start: { key, offset },
end: { key, offset }
}
// handle cursor in Math block, need to remove `CURSOR_DNA` in preview block
if (type === 'span' && functionType === 'multiplemath') {
const mathPreview = this.getNextSibling(this.getParent(block))
const { math } = mathPreview
const offset = math.indexOf(CURSOR_DNA)
if (offset > -1) {
mathPreview.math = math.substring(0, offset) + math.substring(offset + CURSOR_DNA.length)
}
}
return
}
return
}
}
if (children.length) {
} else if (children.length) {
travel(children)
}
}

View File

@ -15,6 +15,16 @@ export const usePluginAddRules = turndownService => {
}
})
// handle multiple lines math
turndownService.addRule('multiplemath', {
filter (node, options) {
return node.nodeName === 'PRE' && node.classList.contains('multiple-math')
},
replacement (content, node, options) {
return `$$\n${content}\n$$`
}
})
// handle `soft line break` and `hard line break`
// add `LINE_BREAK` to the end of soft line break and hard line break.
turndownService.addRule('lineBreak', {

View File

@ -68,6 +68,14 @@ const setCheckedMenuItem = affiliation => {
return item.id === 'frontMatterMenuItem'
} else if (b.functionType === 'code') {
return item.id === 'codeFencesMenuItem'
} else if (b.functionType === 'html') {
return false
} else if (b.functionType === 'multiplemath') {
return item.id === 'mathBlockMenuItem'
}
} else if (b.type === 'figure' && b.functionType) {
if (b.functionType === 'table') {
return item.id === 'tableMenuItem'
}
} else {
return b.type === MENU_ID_MAP[item.id]
@ -90,10 +98,15 @@ ipcMain.on('AGANI::selection-change', (e, { start, end, affiliation }) => {
setCheckedMenuItem(affiliation)
// handle disable
setParagraphMenuItemStatus(true)
if (
(/th|td/.test(start.type) && /th|td/.test(end.type)) ||
(start.type === 'span' && start.block.functionType === 'frontmatter') ||
(end.type === 'span' && end.block.functionType === 'frontmatter')
(end.type === 'span' && end.block.functionType === 'frontmatter') ||
(start.type === 'span' && start.block.functionType === 'multiplemath') ||
(end.type === 'span' && end.block.functionType === 'multiplemath') ||
(start.type === 'pre' && start.block.functionType === 'html') ||
(end.type === 'pre' && end.block.functionType === 'html')
) {
setParagraphMenuItemStatus(false)
} else if (start.key !== end.key) {

View File

@ -7,9 +7,6 @@
/* eslint-disable */
// Set environment for development
process.env.NODE_ENV = 'development'
// Install `electron-debug` with `devtron`
require('electron-debug')({ showDevTools: false })

View File

@ -93,6 +93,14 @@ export default {
click (menuItem, browserWindow) {
actions.paragraph(browserWindow, 'blockquote')
}
}, {
id: 'mathBlockMenuItem',
label: 'Math Block',
type: 'checkbox',
accelerator: 'Alt+CmdOrCtrl+M',
click (menuItem, browserWindow) {
actions.paragraph(browserWindow, 'mathblock')
}
}, {
type: 'separator'
}, {

View File

@ -67,9 +67,9 @@
const STANDAR_Y = 320
const PARAGRAPH_CMD = [
'ul-bullet', 'ul-task', 'ol-order', 'pre', 'blockquote', 'heading 1', 'heading 2', 'heading 3',
'heading 4', 'heading 5', 'heading 6', 'upgrade heading', 'degrade heading', 'paragraph', 'hr',
'loose-list-item', 'front-matter'
'ul-bullet', 'ul-task', 'ol-order', 'pre', 'blockquote', 'mathblock', 'heading 1', 'heading 2',
'heading 3', 'heading 4', 'heading 5', 'heading 6', 'upgrade heading', 'degrade heading',
'paragraph', 'hr', 'loose-list-item', 'front-matter'
]
export default {

View File

@ -109,8 +109,8 @@ export const adjustCursor = (cursor, preline, line, nextline) => {
}
}
// Need to adjust the cursor when cursor in the first or last line of code block.
if (/```[\S]*/.test(line)) {
// Need to adjust the cursor when cursor in the first or last line of code/math block.
if (/```[\S]*/.test(line) || /^\$\$$/.test(line)) {
if (typeof nextline === 'string' && /\S/.test(nextline)) {
newCursor.line += 1
newCursor.ch = 0

View File

@ -372,10 +372,30 @@ code {
}
#ag-editor-id pre.ag-html-block {
padding: .4rem 1rem;
padding: 0 .5rem;
margin-top: 0;
}
#ag-editor-id pre.ag-front-matter {
background: transparent;
border-bottom: 1px dashed #efefef;
}
#ag-editor-id pre.ag-multiple-math {
background: transparent;
border: 1px solid #909399;
}
#ag-editor-id span.ag-math-text,
#ag-editor-id pre.ag-multiple-math span.ag-multiple-math-line {
color: lightsalmon;
}
#ag-editor-id div.ag-math-preview {
background: #303133;
border-color: #333;
}
#ag-editor-id pre.ag-code-block,
#ag-editor-id pre.ag-html-block {
font-size: 90%;

View File

@ -48,7 +48,7 @@ body {
}
.ag-gray {
color: #E4E7ED;
color: #C0C4CC;
text-decoration: none;
}
@ -317,12 +317,13 @@ tt {
/* custom add */
code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(27,31,35,0.05);
border-radius: 3px;
border: none;
padding: 2px 4px;
font-size: 90%;
color: #c7254e;
background-color: #f9f2f4;
border-radius: 4px;
color: #24292e;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}
@ -355,12 +356,12 @@ code {
#ag-editor-id pre.ag-html-block {
background: transparent;
padding: .4rem 1rem;
padding: 0 .5rem;
margin-top: 0;
}
#ag-editor-id pre.ag-active.ag-html-block {
background: #F2F6FC;
background: #f6f8fa;
}
p:not(.ag-active)[data-role="hr"]::before {