mirror of
https://gitee.com/mafgwo/stackedit
synced 2024-11-15 19:22:27 +08:00
First commit
This commit is contained in:
commit
4622e4842c
14
.babelrc
Normal file
14
.babelrc
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"presets": [
|
||||
["env", { "modules": false }],
|
||||
"stage-2"
|
||||
],
|
||||
"plugins": ["transform-runtime"],
|
||||
"comments": false,
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": ["env", "stage-2"],
|
||||
"plugins": [ "istanbul" ]
|
||||
}
|
||||
}
|
||||
}
|
9
.editorconfig
Normal file
9
.editorconfig
Normal file
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
build/*.js
|
||||
config/*.js
|
||||
src/cledit/*.js
|
40
.eslintrc.js
Normal file
40
.eslintrc.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
// http://eslint.org/docs/user-guide/configuring
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: 'babel-eslint',
|
||||
parserOptions: {
|
||||
sourceType: 'module'
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
extends: 'airbnb-base',
|
||||
// required to lint *.vue files
|
||||
plugins: [
|
||||
'html'
|
||||
],
|
||||
// check if imports actually resolve
|
||||
'settings': {
|
||||
'import/resolver': {
|
||||
'webpack': {
|
||||
'config': 'build/webpack.base.conf.js'
|
||||
}
|
||||
}
|
||||
},
|
||||
// add your custom rules here
|
||||
'rules': {
|
||||
'no-param-reassign': [2, { 'props': false }],
|
||||
// don't require .vue extension when importing
|
||||
'import/extensions': ['error', 'always', {
|
||||
'js': 'never',
|
||||
'vue': 'never'
|
||||
}],
|
||||
// allow optionalDependencies
|
||||
'import/no-extraneous-dependencies': ['error', {
|
||||
'optionalDependencies': ['test/unit/index.js']
|
||||
}],
|
||||
// allow debugger during development
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
|
||||
}
|
||||
}
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
.DS_Store
|
||||
node_modules/
|
||||
dist/
|
||||
.history
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
8
.postcssrc.js
Normal file
8
.postcssrc.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||
|
||||
module.exports = {
|
||||
"plugins": {
|
||||
// to edit target browsers: use "browserlist" field in package.json
|
||||
"autoprefixer": {}
|
||||
}
|
||||
}
|
7
.stylelintrc
Normal file
7
.stylelintrc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"processors": ["stylelint-processor-html"],
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"no-empty-source": null
|
||||
}
|
||||
}
|
21
README.md
Normal file
21
README.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# my-project
|
||||
|
||||
> A Vue.js project
|
||||
|
||||
## Build Setup
|
||||
|
||||
``` bash
|
||||
# install dependencies
|
||||
npm install
|
||||
|
||||
# serve with hot reload at localhost:8080
|
||||
npm run dev
|
||||
|
||||
# build for production with minification
|
||||
npm run build
|
||||
|
||||
# build for production and view the bundle analyzer report
|
||||
npm run build --report
|
||||
```
|
||||
|
||||
For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
|
35
build/build.js
Normal file
35
build/build.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
require('./check-versions')()
|
||||
|
||||
process.env.NODE_ENV = 'production'
|
||||
|
||||
var ora = require('ora')
|
||||
var rm = require('rimraf')
|
||||
var path = require('path')
|
||||
var chalk = require('chalk')
|
||||
var webpack = require('webpack')
|
||||
var config = require('../config')
|
||||
var webpackConfig = require('./webpack.prod.conf')
|
||||
|
||||
var spinner = ora('building for production...')
|
||||
spinner.start()
|
||||
|
||||
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
|
||||
if (err) throw err
|
||||
webpack(webpackConfig, function (err, stats) {
|
||||
spinner.stop()
|
||||
if (err) throw err
|
||||
process.stdout.write(stats.toString({
|
||||
colors: true,
|
||||
modules: false,
|
||||
children: false,
|
||||
chunks: false,
|
||||
chunkModules: false
|
||||
}) + '\n\n')
|
||||
|
||||
console.log(chalk.cyan(' Build complete.\n'))
|
||||
console.log(chalk.yellow(
|
||||
' Tip: built files are meant to be served over an HTTP server.\n' +
|
||||
' Opening index.html over file:// won\'t work.\n'
|
||||
))
|
||||
})
|
||||
})
|
48
build/check-versions.js
Normal file
48
build/check-versions.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
var chalk = require('chalk')
|
||||
var semver = require('semver')
|
||||
var packageConfig = require('../package.json')
|
||||
var shell = require('shelljs')
|
||||
function exec (cmd) {
|
||||
return require('child_process').execSync(cmd).toString().trim()
|
||||
}
|
||||
|
||||
var versionRequirements = [
|
||||
{
|
||||
name: 'node',
|
||||
currentVersion: semver.clean(process.version),
|
||||
versionRequirement: packageConfig.engines.node
|
||||
},
|
||||
]
|
||||
|
||||
if (shell.which('npm')) {
|
||||
versionRequirements.push({
|
||||
name: 'npm',
|
||||
currentVersion: exec('npm --version'),
|
||||
versionRequirement: packageConfig.engines.npm
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
var warnings = []
|
||||
for (var i = 0; i < versionRequirements.length; i++) {
|
||||
var mod = versionRequirements[i]
|
||||
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
||||
warnings.push(mod.name + ': ' +
|
||||
chalk.red(mod.currentVersion) + ' should be ' +
|
||||
chalk.green(mod.versionRequirement)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length) {
|
||||
console.log('')
|
||||
console.log(chalk.yellow('To use this template, you must update following to modules:'))
|
||||
console.log()
|
||||
for (var i = 0; i < warnings.length; i++) {
|
||||
var warning = warnings[i]
|
||||
console.log(' ' + warning)
|
||||
}
|
||||
console.log()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
9
build/dev-client.js
Normal file
9
build/dev-client.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
/* eslint-disable */
|
||||
require('eventsource-polyfill')
|
||||
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
|
||||
|
||||
hotClient.subscribe(function (event) {
|
||||
if (event.action === 'reload') {
|
||||
window.location.reload()
|
||||
}
|
||||
})
|
89
build/dev-server.js
Normal file
89
build/dev-server.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
require('./check-versions')()
|
||||
|
||||
var config = require('../config')
|
||||
if (!process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
|
||||
}
|
||||
|
||||
var opn = require('opn')
|
||||
var path = require('path')
|
||||
var express = require('express')
|
||||
var webpack = require('webpack')
|
||||
var proxyMiddleware = require('http-proxy-middleware')
|
||||
var webpackConfig = require('./webpack.dev.conf')
|
||||
|
||||
// default port where dev server listens for incoming traffic
|
||||
var port = process.env.PORT || config.dev.port
|
||||
// automatically open browser, if not set will be false
|
||||
var autoOpenBrowser = !!config.dev.autoOpenBrowser
|
||||
// Define HTTP proxies to your custom API backend
|
||||
// https://github.com/chimurai/http-proxy-middleware
|
||||
var proxyTable = config.dev.proxyTable
|
||||
|
||||
var app = express()
|
||||
var compiler = webpack(webpackConfig)
|
||||
|
||||
var devMiddleware = require('webpack-dev-middleware')(compiler, {
|
||||
publicPath: webpackConfig.output.publicPath,
|
||||
quiet: true
|
||||
})
|
||||
|
||||
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
|
||||
log: () => {}
|
||||
})
|
||||
// force page reload when html-webpack-plugin template changes
|
||||
compiler.plugin('compilation', function (compilation) {
|
||||
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
|
||||
hotMiddleware.publish({ action: 'reload' })
|
||||
cb()
|
||||
})
|
||||
})
|
||||
|
||||
// proxy api requests
|
||||
Object.keys(proxyTable).forEach(function (context) {
|
||||
var options = proxyTable[context]
|
||||
if (typeof options === 'string') {
|
||||
options = { target: options }
|
||||
}
|
||||
app.use(proxyMiddleware(options.filter || context, options))
|
||||
})
|
||||
|
||||
// handle fallback for HTML5 history API
|
||||
app.use(require('connect-history-api-fallback')())
|
||||
|
||||
// serve webpack bundle output
|
||||
app.use(devMiddleware)
|
||||
|
||||
// enable hot-reload and state-preserving
|
||||
// compilation error display
|
||||
app.use(hotMiddleware)
|
||||
|
||||
// serve pure static assets
|
||||
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
|
||||
app.use(staticPath, express.static('./static'))
|
||||
|
||||
var uri = 'http://localhost:' + port
|
||||
|
||||
var _resolve
|
||||
var readyPromise = new Promise(resolve => {
|
||||
_resolve = resolve
|
||||
})
|
||||
|
||||
console.log('> Starting dev server...')
|
||||
devMiddleware.waitUntilValid(() => {
|
||||
console.log('> Listening at ' + uri + '\n')
|
||||
// when env is testing, don't need open it
|
||||
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
|
||||
opn(uri)
|
||||
}
|
||||
_resolve()
|
||||
})
|
||||
|
||||
var server = app.listen(port)
|
||||
|
||||
module.exports = {
|
||||
ready: readyPromise,
|
||||
close: () => {
|
||||
server.close()
|
||||
}
|
||||
}
|
71
build/utils.js
Normal file
71
build/utils.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
var path = require('path')
|
||||
var config = require('../config')
|
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
|
||||
exports.assetsPath = function (_path) {
|
||||
var assetsSubDirectory = process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsSubDirectory
|
||||
: config.dev.assetsSubDirectory
|
||||
return path.posix.join(assetsSubDirectory, _path)
|
||||
}
|
||||
|
||||
exports.cssLoaders = function (options) {
|
||||
options = options || {}
|
||||
|
||||
var cssLoader = {
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
minimize: process.env.NODE_ENV === 'production',
|
||||
sourceMap: options.sourceMap
|
||||
}
|
||||
}
|
||||
|
||||
// generate loader string to be used with extract text plugin
|
||||
function generateLoaders (loader, loaderOptions) {
|
||||
var loaders = [cssLoader]
|
||||
if (loader) {
|
||||
loaders.push({
|
||||
loader: loader + '-loader',
|
||||
options: Object.assign({}, loaderOptions, {
|
||||
sourceMap: options.sourceMap
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Extract CSS when that option is specified
|
||||
// (which is the case during production build)
|
||||
if (options.extract) {
|
||||
return ExtractTextPlugin.extract({
|
||||
use: loaders,
|
||||
fallback: 'vue-style-loader'
|
||||
})
|
||||
} else {
|
||||
return ['vue-style-loader'].concat(loaders)
|
||||
}
|
||||
}
|
||||
|
||||
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
|
||||
return {
|
||||
css: generateLoaders(),
|
||||
postcss: generateLoaders(),
|
||||
less: generateLoaders('less'),
|
||||
sass: generateLoaders('sass', { indentedSyntax: true }),
|
||||
scss: generateLoaders('sass'),
|
||||
stylus: generateLoaders('stylus'),
|
||||
styl: generateLoaders('stylus')
|
||||
}
|
||||
}
|
||||
|
||||
// Generate loaders for standalone style files (outside of .vue)
|
||||
exports.styleLoaders = function (options) {
|
||||
var output = []
|
||||
var loaders = exports.cssLoaders(options)
|
||||
for (var extension in loaders) {
|
||||
var loader = loaders[extension]
|
||||
output.push({
|
||||
test: new RegExp('\\.' + extension + '$'),
|
||||
use: loader
|
||||
})
|
||||
}
|
||||
return output
|
||||
}
|
12
build/vue-loader.conf.js
Normal file
12
build/vue-loader.conf.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
var utils = require('./utils')
|
||||
var config = require('../config')
|
||||
var isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
module.exports = {
|
||||
loaders: utils.cssLoaders({
|
||||
sourceMap: isProduction
|
||||
? config.build.productionSourceMap
|
||||
: config.dev.cssSourceMap,
|
||||
extract: isProduction
|
||||
})
|
||||
}
|
76
build/webpack.base.conf.js
Normal file
76
build/webpack.base.conf.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
var path = require('path')
|
||||
var utils = require('./utils')
|
||||
var config = require('../config')
|
||||
var vueLoaderConfig = require('./vue-loader.conf')
|
||||
var StylelintPlugin = require('stylelint-webpack-plugin')
|
||||
|
||||
function resolve (dir) {
|
||||
return path.join(__dirname, '..', dir)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
app: './src/'
|
||||
},
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
filename: '[name].js',
|
||||
publicPath: process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsPublicPath
|
||||
: config.dev.assetsPublicPath
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.vue', '.json'],
|
||||
alias: {
|
||||
'@': resolve('src')
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|vue)$/,
|
||||
loader: 'eslint-loader',
|
||||
enforce: 'pre',
|
||||
include: [resolve('src'), resolve('test')],
|
||||
options: {
|
||||
formatter: require('eslint-friendly-formatter')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: vueLoaderConfig
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
include: [resolve('src'), resolve('test')]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('img/[name].[hash:7].[ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.md$/,
|
||||
loader: 'raw-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new StylelintPlugin({
|
||||
files: ['**/*.vue', '**/*.scss']
|
||||
})
|
||||
]
|
||||
}
|
35
build/webpack.dev.conf.js
Normal file
35
build/webpack.dev.conf.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
var utils = require('./utils')
|
||||
var webpack = require('webpack')
|
||||
var config = require('../config')
|
||||
var merge = require('webpack-merge')
|
||||
var baseWebpackConfig = require('./webpack.base.conf')
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
|
||||
|
||||
// add hot-reload related code to entry chunks
|
||||
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
|
||||
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
|
||||
})
|
||||
|
||||
module.exports = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
|
||||
},
|
||||
// cheap-module-eval-source-map is faster for development
|
||||
devtool: 'source-map',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': config.dev.env
|
||||
}),
|
||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
// https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'index.html',
|
||||
template: 'index.html',
|
||||
inject: true
|
||||
}),
|
||||
new FriendlyErrorsPlugin()
|
||||
]
|
||||
})
|
120
build/webpack.prod.conf.js
Normal file
120
build/webpack.prod.conf.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
var path = require('path')
|
||||
var utils = require('./utils')
|
||||
var webpack = require('webpack')
|
||||
var config = require('../config')
|
||||
var merge = require('webpack-merge')
|
||||
var baseWebpackConfig = require('./webpack.base.conf')
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
|
||||
|
||||
var env = config.build.env
|
||||
|
||||
var webpackConfig = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
rules: utils.styleLoaders({
|
||||
sourceMap: config.build.productionSourceMap,
|
||||
extract: true
|
||||
})
|
||||
},
|
||||
devtool: config.build.productionSourceMap ? '#source-map' : false,
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
filename: utils.assetsPath('js/[name].[chunkhash].js'),
|
||||
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
|
||||
},
|
||||
plugins: [
|
||||
// http://vuejs.github.io/vue-loader/en/workflow/production.html
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': env
|
||||
}),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false
|
||||
},
|
||||
sourceMap: true
|
||||
}),
|
||||
// extract css into its own file
|
||||
new ExtractTextPlugin({
|
||||
filename: utils.assetsPath('css/[name].[contenthash].css')
|
||||
}),
|
||||
// Compress extracted CSS. We are using this plugin so that possible
|
||||
// duplicated CSS from different components can be deduped.
|
||||
new OptimizeCSSPlugin({
|
||||
cssProcessorOptions: {
|
||||
safe: true
|
||||
}
|
||||
}),
|
||||
// generate dist index.html with correct asset hash for caching.
|
||||
// you can customize output by editing /index.html
|
||||
// see https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: config.build.index,
|
||||
template: 'index.html',
|
||||
inject: true,
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeAttributeQuotes: true
|
||||
// more options:
|
||||
// https://github.com/kangax/html-minifier#options-quick-reference
|
||||
},
|
||||
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
|
||||
chunksSortMode: 'dependency'
|
||||
}),
|
||||
// split vendor js into its own file
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'vendor',
|
||||
minChunks: function (module, count) {
|
||||
// any required modules inside node_modules are extracted to vendor
|
||||
return (
|
||||
module.resource &&
|
||||
/\.js$/.test(module.resource) &&
|
||||
module.resource.indexOf(
|
||||
path.join(__dirname, '../node_modules')
|
||||
) === 0
|
||||
)
|
||||
}
|
||||
}),
|
||||
// extract webpack runtime and module manifest to its own file in order to
|
||||
// prevent vendor hash from being updated whenever app bundle is updated
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'manifest',
|
||||
chunks: ['vendor']
|
||||
}),
|
||||
// copy custom static assets
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: path.resolve(__dirname, '../static'),
|
||||
to: config.build.assetsSubDirectory,
|
||||
ignore: ['.*']
|
||||
}
|
||||
])
|
||||
]
|
||||
})
|
||||
|
||||
if (config.build.productionGzip) {
|
||||
var CompressionWebpackPlugin = require('compression-webpack-plugin')
|
||||
|
||||
webpackConfig.plugins.push(
|
||||
new CompressionWebpackPlugin({
|
||||
asset: '[path].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
test: new RegExp(
|
||||
'\\.(' +
|
||||
config.build.productionGzipExtensions.join('|') +
|
||||
')$'
|
||||
),
|
||||
threshold: 10240,
|
||||
minRatio: 0.8
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (config.build.bundleAnalyzerReport) {
|
||||
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
|
||||
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
|
||||
}
|
||||
|
||||
module.exports = webpackConfig
|
6
config/dev.env.js
Normal file
6
config/dev.env.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
var merge = require('webpack-merge')
|
||||
var prodEnv = require('./prod.env')
|
||||
|
||||
module.exports = merge(prodEnv, {
|
||||
NODE_ENV: '"development"'
|
||||
})
|
38
config/index.js
Normal file
38
config/index.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
// see http://vuejs-templates.github.io/webpack for documentation.
|
||||
var path = require('path')
|
||||
|
||||
module.exports = {
|
||||
build: {
|
||||
env: require('./prod.env'),
|
||||
index: path.resolve(__dirname, '../dist/index.html'),
|
||||
assetsRoot: path.resolve(__dirname, '../dist'),
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
productionSourceMap: true,
|
||||
// Gzip off by default as many popular static hosts such as
|
||||
// Surge or Netlify already gzip all static assets for you.
|
||||
// Before setting to `true`, make sure to:
|
||||
// npm install --save-dev compression-webpack-plugin
|
||||
productionGzip: false,
|
||||
productionGzipExtensions: ['js', 'css'],
|
||||
// Run the build command with an extra argument to
|
||||
// View the bundle analyzer report after build finishes:
|
||||
// `npm run build --report`
|
||||
// Set to `true` or `false` to always turn it on or off
|
||||
bundleAnalyzerReport: process.env.npm_config_report
|
||||
},
|
||||
dev: {
|
||||
env: require('./dev.env'),
|
||||
port: 8080,
|
||||
autoOpenBrowser: true,
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
proxyTable: {},
|
||||
// CSS Sourcemaps off by default because relative paths are "buggy"
|
||||
// with this option, according to the CSS-Loader README
|
||||
// (https://github.com/webpack/css-loader#sourcemaps)
|
||||
// In our experience, they generally work as expected,
|
||||
// just be aware of this issue when enabling this option.
|
||||
cssSourceMap: false
|
||||
}
|
||||
}
|
3
config/prod.env.js
Normal file
3
config/prod.env.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
11
index.html
Normal file
11
index.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>my-project</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
89
package.json
Normal file
89
package.json
Normal file
|
@ -0,0 +1,89 @@
|
|||
{
|
||||
"name": "my-project",
|
||||
"version": "1.0.0",
|
||||
"description": "A Vue.js project",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "node build/dev-server.js",
|
||||
"start": "node build/dev-server.js",
|
||||
"build": "node build/build.js",
|
||||
"lint": "eslint --ext .js,.vue src"
|
||||
},
|
||||
"dependencies": {
|
||||
"bezier-easing": "^1.1.0",
|
||||
"clunderscore": "^1.0.3",
|
||||
"diff-match-patch": "^1.0.0",
|
||||
"markdown-it": "^8.3.1",
|
||||
"markdown-it-abbr": "^1.0.4",
|
||||
"markdown-it-deflist": "^2.0.2",
|
||||
"markdown-it-emoji": "^1.3.0",
|
||||
"markdown-it-footnote": "^3.0.1",
|
||||
"markdown-it-mathjax": "^2.0.0",
|
||||
"markdown-it-pandoc-renderer": "1.1.3",
|
||||
"markdown-it-sub": "^1.0.0",
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"normalize-scss": "^7.0.0",
|
||||
"prismjs": "^1.6.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"vue": "^2.3.3",
|
||||
"vuex": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^6.7.2",
|
||||
"babel-core": "^6.22.1",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-loader": "^6.2.10",
|
||||
"babel-plugin-transform-runtime": "^6.22.0",
|
||||
"babel-preset-env": "^1.3.2",
|
||||
"babel-preset-stage-2": "^6.22.0",
|
||||
"babel-register": "^6.22.0",
|
||||
"chalk": "^1.1.3",
|
||||
"connect-history-api-fallback": "^1.3.0",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"css-loader": "^0.28.4",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-config-airbnb-base": "^11.1.3",
|
||||
"eslint-friendly-formatter": "^2.0.7",
|
||||
"eslint-import-resolver-webpack": "^0.8.1",
|
||||
"eslint-loader": "^1.7.1",
|
||||
"eslint-plugin-html": "^2.0.0",
|
||||
"eslint-plugin-import": "^2.2.0",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"express": "^4.14.1",
|
||||
"extract-text-webpack-plugin": "^2.0.0",
|
||||
"file-loader": "^0.11.1",
|
||||
"friendly-errors-webpack-plugin": "^1.1.3",
|
||||
"html-webpack-plugin": "^2.28.0",
|
||||
"http-proxy-middleware": "^0.17.3",
|
||||
"node-sass": "^4.5.3",
|
||||
"opn": "^4.0.2",
|
||||
"optimize-css-assets-webpack-plugin": "^1.3.0",
|
||||
"ora": "^1.2.0",
|
||||
"rimraf": "^2.6.0",
|
||||
"sass-loader": "^6.0.5",
|
||||
"semver": "^5.3.0",
|
||||
"shelljs": "^0.7.6",
|
||||
"stylelint-config-standard": "^16.0.0",
|
||||
"stylelint-processor-html": "^1.0.0",
|
||||
"stylelint-webpack-plugin": "^0.7.0",
|
||||
"url-loader": "^0.5.8",
|
||||
"vue-loader": "^12.1.0",
|
||||
"vue-style-loader": "^3.0.1",
|
||||
"vue-template-compiler": "^2.3.3",
|
||||
"webpack": "^2.6.1",
|
||||
"webpack-bundle-analyzer": "^2.2.1",
|
||||
"webpack-dev-middleware": "^1.10.0",
|
||||
"webpack-hot-middleware": "^2.18.0",
|
||||
"webpack-merge": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
]
|
||||
}
|
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
482
src/cledit/cldiffutils.js
Normal file
482
src/cledit/cldiffutils.js
Normal file
|
@ -0,0 +1,482 @@
|
|||
import 'clunderscore';
|
||||
import DiffMatchPatch from 'diff-match-patch';
|
||||
|
||||
var clDiffUtils = {
|
||||
cloneObject: cloneObject,
|
||||
offsetToPatch: offsetToPatch,
|
||||
patchToOffset: patchToOffset,
|
||||
serializeObject: serializeObject,
|
||||
flattenContent: flattenContent,
|
||||
makePatchableText: makePatchableText,
|
||||
restoreDiscussionOffsets: restoreDiscussionOffsets,
|
||||
makeContentChange: makeContentChange,
|
||||
applyContentChanges: applyContentChanges,
|
||||
getTextPatches: getTextPatches,
|
||||
getObjectPatches: getObjectPatches,
|
||||
quickPatch: quickPatch,
|
||||
mergeObjects: mergeObjects,
|
||||
mergeFlattenContent: mergeFlattenContent
|
||||
}
|
||||
|
||||
var marker = '\uF111\uF222\uF333\uF444'
|
||||
var DIFF_DELETE = -1
|
||||
var DIFF_INSERT = 1
|
||||
var DIFF_EQUAL = 0
|
||||
var diffMatchPatch = new DiffMatchPatch() // eslint-disable-line new-cap
|
||||
var diffMatchPatchStrict = new DiffMatchPatch() // eslint-disable-line new-cap
|
||||
diffMatchPatchStrict.Match_Threshold = 0
|
||||
diffMatchPatchStrict.Patch_DeleteThreshold = 0
|
||||
var diffMatchPatchPermissive = new DiffMatchPatch() // eslint-disable-line new-cap
|
||||
diffMatchPatchPermissive.Match_Distance = 999999999
|
||||
|
||||
function cloneObject (obj) {
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
function offsetToPatch (text, offset) {
|
||||
var patch = diffMatchPatchPermissive.patch_make(text, [
|
||||
[0, text.slice(0, offset)],
|
||||
[1, marker],
|
||||
[0, text.slice(offset)]
|
||||
])[0]
|
||||
var diffs = patch.diffs.cl_map(function (diff) {
|
||||
if (!diff[0]) {
|
||||
return diff[1]
|
||||
} else if (diff[1] === marker) {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
return {
|
||||
diffs: diffs,
|
||||
length: patch.length1,
|
||||
start: patch.start1
|
||||
}
|
||||
}
|
||||
|
||||
function patchToOffset (text, patch) {
|
||||
var markersLength = 0
|
||||
var diffs = patch.diffs.cl_map(function (diff) {
|
||||
if (!diff) {
|
||||
markersLength += marker.length
|
||||
return [1, marker]
|
||||
} else {
|
||||
return [0, diff]
|
||||
}
|
||||
})
|
||||
return diffMatchPatchPermissive.patch_apply([{
|
||||
diffs: diffs,
|
||||
length1: patch.length,
|
||||
length2: patch.length + markersLength,
|
||||
start1: patch.start,
|
||||
start2: patch.start
|
||||
}], text)[0].indexOf(marker)
|
||||
}
|
||||
|
||||
function flattenObject (obj) {
|
||||
return obj.cl_reduce(function (result, value, key) {
|
||||
result[key] = value[1]
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
||||
function flattenContent (content) {
|
||||
var result = ({}).cl_extend(content)
|
||||
result.properties = flattenObject(content.properties)
|
||||
result.discussions = flattenObject(content.discussions)
|
||||
result.comments = flattenObject(content.comments)
|
||||
result.text = content.text.cl_reduce(function (text, item) {
|
||||
switch (item.type) {
|
||||
case 'discussion':
|
||||
if (result.discussions[item.id]) {
|
||||
result.discussions[item.id][item.name] = text.length
|
||||
}
|
||||
return text
|
||||
default:
|
||||
return text + item[1]
|
||||
}
|
||||
}, '')
|
||||
return result
|
||||
}
|
||||
|
||||
function getTextPatches (oldText, newText) {
|
||||
var diffs = diffMatchPatch.diff_main(oldText, newText)
|
||||
diffMatchPatch.diff_cleanupEfficiency(diffs)
|
||||
var patches = []
|
||||
var startOffset = 0
|
||||
diffs.cl_each(function (change) {
|
||||
var changeType = change[0]
|
||||
var changeText = change[1]
|
||||
switch (changeType) {
|
||||
case DIFF_EQUAL:
|
||||
startOffset += changeText.length
|
||||
break
|
||||
case DIFF_DELETE:
|
||||
changeText && patches.push({
|
||||
o: startOffset,
|
||||
d: changeText
|
||||
})
|
||||
break
|
||||
case DIFF_INSERT:
|
||||
changeText && patches.push({
|
||||
o: startOffset,
|
||||
a: changeText
|
||||
})
|
||||
startOffset += changeText.length
|
||||
break
|
||||
}
|
||||
})
|
||||
return patches.length ? patches : undefined
|
||||
}
|
||||
|
||||
function getObjectPatches (oldObject, newObjects) {
|
||||
var valueHash = Object.create(null)
|
||||
var valueArray = []
|
||||
oldObject = hashObject(oldObject, valueHash, valueArray)
|
||||
newObjects = hashObject(newObjects, valueHash, valueArray)
|
||||
var diffs = diffMatchPatch.diff_main(oldObject, newObjects)
|
||||
var patches = []
|
||||
diffs.cl_each(function (change) {
|
||||
var changeType = change[0]
|
||||
var changeHash = change[1]
|
||||
if (changeType === DIFF_EQUAL) {
|
||||
return
|
||||
}
|
||||
changeHash.split('').cl_each(function (objHash) {
|
||||
var obj = valueArray[objHash.charCodeAt(0)]
|
||||
var patch = {
|
||||
k: obj[0]
|
||||
}
|
||||
patch[changeType === DIFF_DELETE ? 'd' : 'a'] = obj[1]
|
||||
patches.push(patch)
|
||||
})
|
||||
})
|
||||
return patches.length ? patches : undefined
|
||||
}
|
||||
|
||||
function makePatchableText (content, markerKeys, markerIdxMap) {
|
||||
var markers = []
|
||||
// Sort keys to have predictable marker positions, in case of same offset
|
||||
var discussionKeys = Object.keys(content.discussions).sort()
|
||||
discussionKeys.cl_each(function (discussionId) {
|
||||
function addMarker (offsetName) {
|
||||
var markerKey = discussionId + offsetName
|
||||
if (discussion[offsetName] !== undefined) {
|
||||
var idx = markerIdxMap[markerKey]
|
||||
if (idx === undefined) {
|
||||
idx = markerKeys.length
|
||||
markerIdxMap[markerKey] = idx
|
||||
markerKeys.push({
|
||||
id: discussionId,
|
||||
offsetName: offsetName
|
||||
})
|
||||
}
|
||||
markers.push({
|
||||
idx: idx,
|
||||
offset: discussion[offsetName]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var discussion = content.discussions[discussionId]
|
||||
if (discussion.offset0 === discussion.offset1) {
|
||||
// Remove discussion offsets if markers are at the same position
|
||||
discussion.offset0 = discussion.offset1 = undefined
|
||||
} else {
|
||||
addMarker('offset0')
|
||||
addMarker('offset1')
|
||||
}
|
||||
})
|
||||
|
||||
var lastOffset = 0
|
||||
var result = ''
|
||||
markers
|
||||
.sort(function (marker1, marker2) {
|
||||
return marker1.offset - marker2.offset
|
||||
})
|
||||
.cl_each(function (marker) {
|
||||
result +=
|
||||
content.text.slice(lastOffset, marker.offset) +
|
||||
String.fromCharCode(0xe000 + marker.idx) // Use a character from the private use area
|
||||
lastOffset = marker.offset
|
||||
})
|
||||
return result + content.text.slice(lastOffset)
|
||||
}
|
||||
|
||||
function stripDiscussionOffsets (objectMap) {
|
||||
return objectMap.cl_reduce(function (result, object, id) {
|
||||
result[id] = {
|
||||
text: object.text
|
||||
}
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
||||
function restoreDiscussionOffsets (content, markerKeys) {
|
||||
var len = content.text.length
|
||||
var maxIdx = markerKeys.length
|
||||
for (var i = 0; i < len; i++) {
|
||||
var idx = content.text.charCodeAt(i) - 0xe000
|
||||
if (idx >= 0 && idx < maxIdx) {
|
||||
var markerKey = markerKeys[idx]
|
||||
content.text = content.text.slice(0, i) + content.text.slice(i + 1)
|
||||
var discussion = content.discussions[markerKey.id]
|
||||
if (discussion) {
|
||||
discussion[markerKey.offsetName] = i
|
||||
}
|
||||
i-- // We just removed the current character, we may have multiple markers with same offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeContentChange (oldContent, newContent) {
|
||||
var markerKeys = []
|
||||
var markerIdxMap = Object.create(null)
|
||||
var oldText = makePatchableText(oldContent, markerKeys, markerIdxMap)
|
||||
var newText = makePatchableText(newContent, markerKeys, markerIdxMap)
|
||||
var textPatches = getTextPatches(oldText, newText)
|
||||
textPatches && textPatches.cl_each(function (patch) {
|
||||
// If markers are present, replace changeText with an array of text and markers
|
||||
var changeText = patch.a || patch.d
|
||||
var textItems = []
|
||||
var lastItem = ''
|
||||
var len = changeText.length
|
||||
var maxIdx = markerKeys.length
|
||||
for (var i = 0; i < len; i++) {
|
||||
var idx = changeText.charCodeAt(i) - 0xe000
|
||||
if (idx >= 0 && idx < maxIdx) {
|
||||
var markerKey = markerKeys[idx]
|
||||
lastItem.length && textItems.push(lastItem)
|
||||
textItems.push({
|
||||
type: 'discussion',
|
||||
name: markerKey.offsetName,
|
||||
id: markerKey.id
|
||||
})
|
||||
lastItem = ''
|
||||
} else {
|
||||
lastItem += changeText[i]
|
||||
}
|
||||
}
|
||||
if (textItems.length) {
|
||||
lastItem.length && textItems.push(lastItem)
|
||||
if (patch.a) {
|
||||
patch.a = textItems
|
||||
} else {
|
||||
patch.d = textItems
|
||||
}
|
||||
}
|
||||
})
|
||||
var propertiesPatches = getObjectPatches(oldContent.properties, newContent.properties)
|
||||
var discussionsPatches = getObjectPatches(
|
||||
stripDiscussionOffsets(oldContent.discussions),
|
||||
stripDiscussionOffsets(newContent.discussions)
|
||||
)
|
||||
var commentsPatches = getObjectPatches(oldContent.comments, newContent.comments)
|
||||
if (textPatches || propertiesPatches || discussionsPatches || commentsPatches) {
|
||||
return {
|
||||
text: textPatches,
|
||||
properties: propertiesPatches,
|
||||
discussions: discussionsPatches,
|
||||
comments: commentsPatches
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyContentChanges (content, contentChanges, isBackward) {
|
||||
function applyObjectPatches (obj, patches) {
|
||||
if (patches) {
|
||||
patches.cl_each(function (patch) {
|
||||
if (!patch.a ^ !isBackward) {
|
||||
obj[patch.k] = patch.a || patch.d
|
||||
} else {
|
||||
delete obj[patch.k]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var markerKeys = []
|
||||
var markerIdxMap = Object.create(null)
|
||||
var result = {
|
||||
text: makePatchableText(content, markerKeys, markerIdxMap),
|
||||
properties: cloneObject(content.properties),
|
||||
discussions: stripDiscussionOffsets(content.discussions),
|
||||
comments: cloneObject(content.comments)
|
||||
}
|
||||
|
||||
contentChanges.cl_each(function (contentChange) {
|
||||
var textPatches = contentChange.text || []
|
||||
if (isBackward) {
|
||||
textPatches = textPatches.slice().reverse()
|
||||
}
|
||||
result.text = textPatches.cl_reduce(function (text, patch) {
|
||||
var isAdd = !patch.a ^ !isBackward
|
||||
var textChanges = patch.a || patch.d || ''
|
||||
// When no marker is present, textChanges is a string
|
||||
if (typeof textChanges === 'string') {
|
||||
textChanges = [textChanges]
|
||||
}
|
||||
var textChange = textChanges.cl_map(function (textChange) {
|
||||
if (!textChange.type) {
|
||||
// textChange is a string
|
||||
return textChange
|
||||
}
|
||||
// textChange is a marker
|
||||
var markerKey = textChange.id + textChange.name
|
||||
var idx = markerIdxMap[markerKey]
|
||||
if (idx === undefined) {
|
||||
idx = markerKeys.length
|
||||
markerIdxMap[markerKey] = idx
|
||||
markerKeys.push({
|
||||
id: textChange.id,
|
||||
offsetName: textChange.name
|
||||
})
|
||||
}
|
||||
return String.fromCharCode(0xe000 + idx)
|
||||
}).join('')
|
||||
if (!textChange) {
|
||||
return text
|
||||
} else if (isAdd) {
|
||||
return text.slice(0, patch.o).concat(textChange).concat(text.slice(patch.o))
|
||||
} else {
|
||||
return text.slice(0, patch.o).concat(text.slice(patch.o + textChange.length))
|
||||
}
|
||||
}, result.text)
|
||||
|
||||
applyObjectPatches(result.properties, contentChange.properties)
|
||||
applyObjectPatches(result.discussions, contentChange.discussions)
|
||||
applyObjectPatches(result.comments, contentChange.comments)
|
||||
})
|
||||
|
||||
restoreDiscussionOffsets(result, markerKeys)
|
||||
return result
|
||||
}
|
||||
|
||||
function serializeObject (obj) {
|
||||
return JSON.stringify(obj, function (key, value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
? Object.keys(value).sort().cl_reduce(function (sorted, key) {
|
||||
sorted[key] = value[key]
|
||||
return sorted
|
||||
}, {})
|
||||
: value
|
||||
})
|
||||
}
|
||||
|
||||
function hashArray (arr, valueHash, valueArray) {
|
||||
var hash = []
|
||||
arr.cl_each(function (obj) {
|
||||
var serializedObj = serializeObject(obj)
|
||||
var objHash = valueHash[serializedObj]
|
||||
if (objHash === undefined) {
|
||||
objHash = valueArray.length
|
||||
valueArray.push(obj)
|
||||
valueHash[serializedObj] = objHash
|
||||
}
|
||||
hash.push(objHash)
|
||||
})
|
||||
return String.fromCharCode.apply(null, hash)
|
||||
}
|
||||
|
||||
function hashObject (obj, valueHash, valueArray) {
|
||||
return hashArray(Object.keys(obj || {}).sort().cl_map(function (key) {
|
||||
return [key, obj[key]]
|
||||
}), valueHash, valueArray)
|
||||
}
|
||||
|
||||
function mergeText (oldText, newText, serverText) {
|
||||
var diffs = diffMatchPatch.diff_main(oldText, newText)
|
||||
diffMatchPatch.diff_cleanupSemantic(diffs)
|
||||
var patches = diffMatchPatch.patch_make(oldText, diffs)
|
||||
var patchResult = diffMatchPatch.patch_apply(patches, serverText)
|
||||
if (!patchResult[1]
|
||||
.cl_some(function (changeApplied) {
|
||||
return !changeApplied
|
||||
})) {
|
||||
return patchResult[0]
|
||||
}
|
||||
|
||||
diffs = diffMatchPatchStrict.diff_main(patchResult[0], newText)
|
||||
diffMatchPatch.diff_cleanupSemantic(diffs)
|
||||
return diffs.cl_map(function (diff) {
|
||||
return diff[1]
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function quickPatch (oldStr, newStr, destStr, strict) {
|
||||
var dmp = strict ? diffMatchPatchStrict : diffMatchPatch
|
||||
var diffs = dmp.diff_main(oldStr, newStr)
|
||||
var patches = dmp.patch_make(oldStr, diffs)
|
||||
var patchResult = dmp.patch_apply(patches, destStr)
|
||||
return patchResult[0]
|
||||
}
|
||||
|
||||
function mergeObjects (oldObject, newObject, serverObject) {
|
||||
var mergedObject = ({}).cl_extend(newObject).cl_extend(serverObject)
|
||||
mergedObject.cl_each(function (value, key) {
|
||||
if (!oldObject[key]) {
|
||||
return // There might be conflict, keep the server value
|
||||
}
|
||||
var newValue = newObject[key] && serializeObject(newObject[key])
|
||||
var serverValue = serverObject[key] && serializeObject(serverObject[key])
|
||||
if (newValue === serverValue) {
|
||||
return // no conflict
|
||||
}
|
||||
var oldValue = serializeObject(oldObject[key])
|
||||
if (oldValue !== newValue && !serverValue) {
|
||||
return // Removed on server but changed on client
|
||||
}
|
||||
if (oldValue !== serverValue && !newValue) {
|
||||
return // Removed on client but changed on server
|
||||
}
|
||||
if (oldValue !== newValue && oldValue === serverValue) {
|
||||
// Take the client value
|
||||
if (!newValue) {
|
||||
delete mergedObject[key]
|
||||
} else {
|
||||
mergedObject[key] = newObject[key]
|
||||
}
|
||||
} else if (oldValue !== serverValue && oldValue === newValue) {
|
||||
// Take the server value
|
||||
if (!serverValue) {
|
||||
delete mergedObject[key]
|
||||
}
|
||||
}
|
||||
// Take the server value otherwise
|
||||
})
|
||||
return cloneObject(mergedObject)
|
||||
}
|
||||
|
||||
function mergeFlattenContent (oldContent, newContent, serverContent) {
|
||||
var markerKeys = []
|
||||
var markerIdxMap = Object.create(null)
|
||||
var oldText = makePatchableText(oldContent, markerKeys, markerIdxMap)
|
||||
var serverText = makePatchableText(serverContent, markerKeys, markerIdxMap)
|
||||
var localText = makePatchableText(newContent, markerKeys, markerIdxMap)
|
||||
var isServerTextChanges = oldText !== serverText
|
||||
var isTextSynchronized = serverText === localText
|
||||
|
||||
var result = {
|
||||
text: isTextSynchronized || !isServerTextChanges
|
||||
? localText
|
||||
: mergeText(oldText, serverText, localText),
|
||||
properties: mergeObjects(
|
||||
oldContent.properties,
|
||||
newContent.properties,
|
||||
serverContent.properties
|
||||
),
|
||||
discussions: mergeObjects(
|
||||
stripDiscussionOffsets(oldContent.discussions),
|
||||
stripDiscussionOffsets(newContent.discussions),
|
||||
stripDiscussionOffsets(serverContent.discussions)
|
||||
),
|
||||
comments: mergeObjects(
|
||||
oldContent.comments,
|
||||
newContent.comments,
|
||||
serverContent.comments
|
||||
)
|
||||
}
|
||||
restoreDiscussionOffsets(result, markerKeys)
|
||||
return result
|
||||
}
|
||||
|
||||
export default clDiffUtils;
|
11
src/cledit/cledit.js
Normal file
11
src/cledit/cledit.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import 'clunderscore';
|
||||
import cledit from './cleditCore';
|
||||
import './cleditHighlighter';
|
||||
import './cleditKeystroke';
|
||||
import './cleditMarker';
|
||||
import './cleditSelectionMgr';
|
||||
import './cleditUndoMgr';
|
||||
import './cleditUtils';
|
||||
import './cleditWatcher';
|
||||
|
||||
export default cledit;
|
368
src/cledit/cleditCore.js
Normal file
368
src/cledit/cleditCore.js
Normal file
|
@ -0,0 +1,368 @@
|
|||
var DiffMatchPatch = require('diff-match-patch');
|
||||
|
||||
function cledit(contentElt, scrollElt, windowParam) {
|
||||
scrollElt = scrollElt || contentElt
|
||||
var editor = {
|
||||
$contentElt: contentElt,
|
||||
$scrollElt: scrollElt,
|
||||
$window: windowParam || window,
|
||||
$keystrokes: [],
|
||||
$markers: {}
|
||||
}
|
||||
editor.$document = editor.$window.document
|
||||
cledit.Utils.createEventHooks(editor)
|
||||
var debounce = cledit.Utils.debounce
|
||||
|
||||
editor.toggleEditable = function (isEditable) {
|
||||
if (isEditable === undefined) {
|
||||
isEditable = !contentElt.contentEditable
|
||||
}
|
||||
contentElt.contentEditable = isEditable
|
||||
}
|
||||
editor.toggleEditable(true)
|
||||
|
||||
function getTextContent() {
|
||||
var textContent = contentElt.textContent.replace(/\r[\n\u0085]?|[\u2424\u2028\u0085]/g, '\n') // Markdown-it sanitization (Mac/DOS to Unix)
|
||||
if (textContent.slice(-1) !== '\n') {
|
||||
textContent += '\n'
|
||||
}
|
||||
return textContent
|
||||
}
|
||||
|
||||
var lastTextContent = getTextContent()
|
||||
var highlighter = new cledit.Highlighter(editor)
|
||||
|
||||
var sectionList
|
||||
|
||||
function parseSections(content, isInit) {
|
||||
sectionList = highlighter.parseSections(content, isInit)
|
||||
editor.$allElements = Array.prototype.slice.call(contentElt.querySelectorAll('.cledit-section *'))
|
||||
return sectionList
|
||||
}
|
||||
|
||||
// Used to detect editor changes
|
||||
var watcher = new cledit.Watcher(editor, checkContentChange)
|
||||
watcher.startWatching()
|
||||
|
||||
/* eslint-disable new-cap */
|
||||
var diffMatchPatch = new DiffMatchPatch()
|
||||
/* eslint-enable new-cap */
|
||||
var selectionMgr = new cledit.SelectionMgr(editor)
|
||||
|
||||
function adjustCursorPosition(force) {
|
||||
selectionMgr.saveSelectionState(true, true, force)
|
||||
}
|
||||
|
||||
function replaceContent(selectionStart, selectionEnd, replacement) {
|
||||
var min = Math.min(selectionStart, selectionEnd)
|
||||
var max = Math.max(selectionStart, selectionEnd)
|
||||
var range = selectionMgr.createRange(min, max)
|
||||
var rangeText = '' + range
|
||||
// Range can contain a br element, which is not taken into account in rangeText
|
||||
if (rangeText.length === max - min && rangeText === replacement) {
|
||||
return
|
||||
}
|
||||
range.deleteContents()
|
||||
range.insertNode(editor.$document.createTextNode(replacement))
|
||||
return range
|
||||
}
|
||||
|
||||
var ignoreUndo = false
|
||||
var noContentFix = false
|
||||
|
||||
function setContent(value, noUndo, maxStartOffset) {
|
||||
var textContent = getTextContent()
|
||||
maxStartOffset = maxStartOffset !== undefined && maxStartOffset < textContent.length ? maxStartOffset : textContent.length - 1
|
||||
var startOffset = Math.min(
|
||||
diffMatchPatch.diff_commonPrefix(textContent, value),
|
||||
maxStartOffset
|
||||
)
|
||||
var endOffset = Math.min(
|
||||
diffMatchPatch.diff_commonSuffix(textContent, value),
|
||||
textContent.length - startOffset,
|
||||
value.length - startOffset
|
||||
)
|
||||
var replacement = value.substring(startOffset, value.length - endOffset)
|
||||
var range = replaceContent(startOffset, textContent.length - endOffset, replacement)
|
||||
if (range) {
|
||||
ignoreUndo = noUndo
|
||||
noContentFix = true
|
||||
}
|
||||
return {
|
||||
start: startOffset,
|
||||
end: value.length - endOffset,
|
||||
range: range
|
||||
}
|
||||
}
|
||||
|
||||
function replace(selectionStart, selectionEnd, replacement) {
|
||||
undoMgr.setDefaultMode('single')
|
||||
replaceContent(selectionStart, selectionEnd, replacement)
|
||||
var endOffset = selectionStart + replacement.length
|
||||
selectionMgr.setSelectionStartEnd(endOffset, endOffset)
|
||||
selectionMgr.updateCursorCoordinates(true)
|
||||
}
|
||||
|
||||
function replaceAll(search, replacement) {
|
||||
undoMgr.setDefaultMode('single')
|
||||
var textContent = getTextContent()
|
||||
var value = textContent.replace(search, replacement)
|
||||
if (value !== textContent) {
|
||||
var offset = editor.setContent(value)
|
||||
selectionMgr.setSelectionStartEnd(offset.end, offset.end)
|
||||
selectionMgr.updateCursorCoordinates(true)
|
||||
}
|
||||
}
|
||||
|
||||
function focus() {
|
||||
selectionMgr.restoreSelection()
|
||||
}
|
||||
|
||||
var undoMgr = new cledit.UndoMgr(editor)
|
||||
|
||||
function addMarker(marker) {
|
||||
editor.$markers[marker.id] = marker
|
||||
}
|
||||
|
||||
function removeMarker(marker) {
|
||||
delete editor.$markers[marker.id]
|
||||
}
|
||||
|
||||
var triggerSpellCheck = debounce(function () {
|
||||
var selection = editor.$window.getSelection()
|
||||
if (!selectionMgr.hasFocus || highlighter.isComposing || selectionMgr.selectionStart !== selectionMgr.selectionEnd || !selection.modify) {
|
||||
return
|
||||
}
|
||||
// Hack for Chrome to trigger the spell checker
|
||||
if (selectionMgr.selectionStart) {
|
||||
selection.modify('move', 'backward', 'character')
|
||||
selection.modify('move', 'forward', 'character')
|
||||
} else {
|
||||
selection.modify('move', 'forward', 'character')
|
||||
selection.modify('move', 'backward', 'character')
|
||||
}
|
||||
}, 10)
|
||||
|
||||
function checkContentChange(mutations) {
|
||||
watcher.noWatch(function () {
|
||||
var removedSections = []
|
||||
var modifiedSections = []
|
||||
|
||||
function markModifiedSection(node) {
|
||||
while (node && node !== contentElt) {
|
||||
if (node.section) {
|
||||
var array = node.parentNode ? modifiedSections : removedSections
|
||||
return array.indexOf(node.section) === -1 && array.push(node.section)
|
||||
}
|
||||
node = node.parentNode
|
||||
}
|
||||
}
|
||||
|
||||
mutations.cl_each(function (mutation) {
|
||||
markModifiedSection(mutation.target)
|
||||
mutation.addedNodes.cl_each(markModifiedSection)
|
||||
mutation.removedNodes.cl_each(markModifiedSection)
|
||||
})
|
||||
highlighter.fixContent(modifiedSections, removedSections, noContentFix)
|
||||
noContentFix = false
|
||||
})
|
||||
|
||||
var newTextContent = getTextContent()
|
||||
var diffs = diffMatchPatch.diff_main(lastTextContent, newTextContent)
|
||||
editor.$markers.cl_each(function (marker) {
|
||||
marker.adjustOffset(diffs)
|
||||
})
|
||||
|
||||
selectionMgr.saveSelectionState()
|
||||
var sectionList = parseSections(newTextContent)
|
||||
editor.$trigger('contentChanged', newTextContent, diffs, sectionList)
|
||||
if (!ignoreUndo) {
|
||||
undoMgr.addDiffs(lastTextContent, newTextContent, diffs)
|
||||
undoMgr.setDefaultMode('typing')
|
||||
undoMgr.saveState()
|
||||
}
|
||||
ignoreUndo = false
|
||||
lastTextContent = newTextContent
|
||||
triggerSpellCheck()
|
||||
}
|
||||
|
||||
function setSelection(start, end) {
|
||||
end = end === undefined ? start : end
|
||||
selectionMgr.setSelectionStartEnd(start, end)
|
||||
selectionMgr.updateCursorCoordinates()
|
||||
}
|
||||
|
||||
function keydownHandler(handler) {
|
||||
return function (evt) {
|
||||
if (
|
||||
evt.which !== 17 && // Ctrl
|
||||
evt.which !== 91 && // Cmd
|
||||
evt.which !== 18 && // Alt
|
||||
evt.which !== 16 // Shift
|
||||
) {
|
||||
handler(evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tryDestroy() {
|
||||
if (!editor.$window.document.contains(contentElt)) {
|
||||
watcher.stopWatching()
|
||||
editor.$window.removeEventListener('keydown', windowKeydownListener)
|
||||
editor.$window.removeEventListener('mouseup', windowMouseupListener)
|
||||
editor.$trigger('destroy')
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// In case of Ctrl/Cmd+A outside the editor element
|
||||
function windowKeydownListener(evt) {
|
||||
if (!tryDestroy()) {
|
||||
keydownHandler(function () {
|
||||
adjustCursorPosition()
|
||||
})(evt)
|
||||
}
|
||||
}
|
||||
editor.$window.addEventListener('keydown', windowKeydownListener, false)
|
||||
|
||||
// Mouseup can happen outside the editor element
|
||||
function windowMouseupListener() {
|
||||
if (!tryDestroy()) {
|
||||
selectionMgr.saveSelectionState(true, false)
|
||||
}
|
||||
}
|
||||
editor.$window.addEventListener('mouseup', windowMouseupListener)
|
||||
// This can also provoke selection changes and does not fire mouseup event on Chrome/OSX
|
||||
contentElt.addEventListener('contextmenu', selectionMgr.saveSelectionState.cl_bind(selectionMgr, true, false))
|
||||
|
||||
contentElt.addEventListener('keydown', keydownHandler(function (evt) {
|
||||
selectionMgr.saveSelectionState()
|
||||
adjustCursorPosition()
|
||||
|
||||
// Perform keystroke
|
||||
var textContent = getTextContent()
|
||||
var min = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd)
|
||||
var max = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd)
|
||||
var state = {
|
||||
before: textContent.slice(0, min),
|
||||
after: textContent.slice(max),
|
||||
selection: textContent.slice(min, max),
|
||||
isBackwardSelection: selectionMgr.selectionStart > selectionMgr.selectionEnd
|
||||
}
|
||||
editor.$keystrokes.cl_some(function (keystroke) {
|
||||
if (keystroke.handler(evt, state, editor)) {
|
||||
editor.setContent(state.before + state.selection + state.after, false, min)
|
||||
min = state.before.length
|
||||
max = min + state.selection.length
|
||||
selectionMgr.setSelectionStartEnd(
|
||||
state.isBackwardSelection ? max : min,
|
||||
state.isBackwardSelection ? min : max
|
||||
)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}), false)
|
||||
|
||||
contentElt.addEventListener('compositionstart', function () {
|
||||
highlighter.isComposing++
|
||||
}, false)
|
||||
|
||||
contentElt.addEventListener('compositionend', function () {
|
||||
setTimeout(function () {
|
||||
highlighter.isComposing && highlighter.isComposing--
|
||||
}, 0)
|
||||
}, false)
|
||||
|
||||
contentElt.addEventListener('paste', function (evt) {
|
||||
undoMgr.setCurrentMode('single')
|
||||
evt.preventDefault()
|
||||
var data
|
||||
var clipboardData = evt.clipboardData
|
||||
if (clipboardData) {
|
||||
data = clipboardData.getData('text/plain')
|
||||
} else {
|
||||
clipboardData = editor.$window.clipboardData
|
||||
data = clipboardData && clipboardData.getData('Text')
|
||||
}
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
replace(selectionMgr.selectionStart, selectionMgr.selectionEnd, data)
|
||||
adjustCursorPosition()
|
||||
}, false)
|
||||
|
||||
contentElt.addEventListener('cut', function () {
|
||||
undoMgr.setCurrentMode('single')
|
||||
adjustCursorPosition()
|
||||
}, false)
|
||||
|
||||
contentElt.addEventListener('focus', function () {
|
||||
selectionMgr.hasFocus = true
|
||||
editor.$trigger('focus')
|
||||
}, false)
|
||||
|
||||
contentElt.addEventListener('blur', function () {
|
||||
selectionMgr.hasFocus = false
|
||||
editor.$trigger('blur')
|
||||
}, false)
|
||||
|
||||
function addKeystroke(keystrokes) {
|
||||
if (!Array.isArray(keystrokes)) {
|
||||
keystrokes = [keystrokes]
|
||||
}
|
||||
editor.$keystrokes = editor.$keystrokes.concat(keystrokes).sort(function (keystroke1, keystroke2) {
|
||||
return keystroke1.priority - keystroke2.priority
|
||||
})
|
||||
}
|
||||
addKeystroke(cledit.defaultKeystrokes)
|
||||
|
||||
editor.selectionMgr = selectionMgr
|
||||
editor.undoMgr = undoMgr
|
||||
editor.highlighter = highlighter
|
||||
editor.watcher = watcher
|
||||
editor.adjustCursorPosition = adjustCursorPosition
|
||||
editor.setContent = setContent
|
||||
editor.replace = replace
|
||||
editor.replaceAll = replaceAll
|
||||
editor.getContent = getTextContent
|
||||
editor.focus = focus
|
||||
editor.setSelection = setSelection
|
||||
editor.addKeystroke = addKeystroke
|
||||
editor.addMarker = addMarker
|
||||
editor.removeMarker = removeMarker
|
||||
|
||||
editor.init = function (options) {
|
||||
options = ({
|
||||
cursorFocusRatio: 0.2,
|
||||
sectionHighlighter: function (section) {
|
||||
return section.text.replace(/&/g, '&').replace(/</g, '<').replace(/\u00a0/g, ' ')
|
||||
},
|
||||
sectionDelimiter: ''
|
||||
}).cl_extend(options || {})
|
||||
editor.options = options
|
||||
|
||||
if (options.content !== undefined) {
|
||||
lastTextContent = options.content.toString()
|
||||
if (lastTextContent.slice(-1) !== '\n') {
|
||||
lastTextContent += '\n'
|
||||
}
|
||||
}
|
||||
|
||||
var sectionList = parseSections(lastTextContent, true)
|
||||
editor.$trigger('contentChanged', lastTextContent, [0, lastTextContent], sectionList)
|
||||
if (options.selectionStart !== undefined && options.selectionEnd !== undefined) {
|
||||
editor.setSelection(options.selectionStart, options.selectionEnd)
|
||||
} else {
|
||||
selectionMgr.saveSelectionState()
|
||||
}
|
||||
undoMgr.init(options)
|
||||
|
||||
if (options.scrollTop !== undefined) {
|
||||
scrollElt.scrollTop = options.scrollTop
|
||||
}
|
||||
}
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
module.exports = cledit
|
189
src/cledit/cleditHighlighter.js
Normal file
189
src/cledit/cleditHighlighter.js
Normal file
|
@ -0,0 +1,189 @@
|
|||
var cledit = require('./cleditCore')
|
||||
|
||||
var styleElts = []
|
||||
|
||||
function createStyleSheet(document) {
|
||||
var styleElt = document.createElement('style')
|
||||
styleElt.type = 'text/css'
|
||||
styleElt.innerHTML = '.cledit-section * { display: inline; }'
|
||||
document.head.appendChild(styleElt)
|
||||
styleElts.push(styleElt)
|
||||
}
|
||||
|
||||
function Highlighter(editor) {
|
||||
var self = this
|
||||
cledit.Utils.createEventHooks(this)
|
||||
|
||||
styleElts.cl_some(function (styleElt) {
|
||||
return editor.$document.head.contains(styleElt)
|
||||
}) || createStyleSheet(editor.$document)
|
||||
|
||||
var contentElt = editor.$contentElt
|
||||
this.isComposing = 0
|
||||
|
||||
var sectionList = []
|
||||
var insertBeforeSection
|
||||
var useBr = cledit.Utils.isWebkit
|
||||
var trailingNodeTag = 'div'
|
||||
var hiddenLfInnerHtml = '<br><span class="hd-lf" style="display: none">\n</span>'
|
||||
|
||||
var lfHtml = '<span class="lf">' + (useBr ? hiddenLfInnerHtml : '\n') + '</span>'
|
||||
|
||||
this.fixContent = function (modifiedSections, removedSections, noContentFix) {
|
||||
modifiedSections.cl_each(function (section) {
|
||||
section.forceHighlighting = true
|
||||
if (!noContentFix) {
|
||||
if (useBr) {
|
||||
section.elt.getElementsByClassName('hd-lf').cl_each(function (lfElt) {
|
||||
lfElt.parentNode.removeChild(lfElt)
|
||||
})
|
||||
section.elt.getElementsByTagName('br').cl_each(function (brElt) {
|
||||
brElt.parentNode.replaceChild(editor.$document.createTextNode('\n'), brElt)
|
||||
})
|
||||
}
|
||||
if (section.elt.textContent.slice(-1) !== '\n') {
|
||||
section.elt.appendChild(editor.$document.createTextNode('\n'))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.addTrailingNode = function () {
|
||||
this.trailingNode = editor.$document.createElement(trailingNodeTag)
|
||||
contentElt.appendChild(this.trailingNode)
|
||||
}
|
||||
|
||||
function Section(text) {
|
||||
this.text = text.text === undefined ? text : text.text
|
||||
this.data = text.data
|
||||
}
|
||||
|
||||
Section.prototype.setElement = function (elt) {
|
||||
this.elt = elt
|
||||
elt.section = this
|
||||
}
|
||||
|
||||
this.parseSections = function (content, isInit) {
|
||||
if (this.isComposing) {
|
||||
return sectionList
|
||||
}
|
||||
|
||||
var newSectionList = editor.options.sectionParser ? editor.options.sectionParser(content) : [content]
|
||||
newSectionList = newSectionList.cl_map(function (sectionText) {
|
||||
return new Section(sectionText)
|
||||
})
|
||||
|
||||
var modifiedSections = []
|
||||
var sectionsToRemove = []
|
||||
insertBeforeSection = undefined
|
||||
|
||||
if (isInit) {
|
||||
// Render everything if isInit
|
||||
sectionsToRemove = sectionList
|
||||
sectionList = newSectionList
|
||||
modifiedSections = newSectionList
|
||||
} else {
|
||||
// Find modified section starting from top
|
||||
var leftIndex = sectionList.length
|
||||
sectionList.cl_some(function (section, index) {
|
||||
var newSection = newSectionList[index]
|
||||
if (index >= newSectionList.length ||
|
||||
section.forceHighlighting ||
|
||||
// Check text modification
|
||||
section.text !== newSection.text ||
|
||||
// Check that section has not been detached or moved
|
||||
section.elt.parentNode !== contentElt ||
|
||||
// Check also the content since nodes can be injected in sections via copy/paste
|
||||
section.elt.textContent !== newSection.text) {
|
||||
leftIndex = index
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// Find modified section starting from bottom
|
||||
var rightIndex = -sectionList.length
|
||||
sectionList.slice().reverse().cl_some(function (section, index) {
|
||||
var newSection = newSectionList[newSectionList.length - index - 1]
|
||||
if (index >= newSectionList.length ||
|
||||
section.forceHighlighting ||
|
||||
// Check modified
|
||||
section.text !== newSection.text ||
|
||||
// Check that section has not been detached or moved
|
||||
section.elt.parentNode !== contentElt ||
|
||||
// Check also the content since nodes can be injected in sections via copy/paste
|
||||
section.elt.textContent !== newSection.text) {
|
||||
rightIndex = -index
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
if (leftIndex - rightIndex > sectionList.length) {
|
||||
// Prevent overlap
|
||||
rightIndex = leftIndex - sectionList.length
|
||||
}
|
||||
|
||||
var leftSections = sectionList.slice(0, leftIndex)
|
||||
modifiedSections = newSectionList.slice(leftIndex, newSectionList.length + rightIndex)
|
||||
var rightSections = sectionList.slice(sectionList.length + rightIndex, sectionList.length)
|
||||
insertBeforeSection = rightSections[0]
|
||||
sectionsToRemove = sectionList.slice(leftIndex, sectionList.length + rightIndex)
|
||||
sectionList = leftSections.concat(modifiedSections).concat(rightSections)
|
||||
}
|
||||
|
||||
var newSectionEltList = editor.$document.createDocumentFragment()
|
||||
modifiedSections.cl_each(function (section) {
|
||||
section.forceHighlighting = false
|
||||
highlight(section)
|
||||
newSectionEltList.appendChild(section.elt)
|
||||
})
|
||||
editor.watcher.noWatch(function () {
|
||||
if (isInit) {
|
||||
contentElt.innerHTML = ''
|
||||
contentElt.appendChild(newSectionEltList)
|
||||
return this.addTrailingNode()
|
||||
}
|
||||
|
||||
// Remove outdated sections
|
||||
sectionsToRemove.cl_each(function (section) {
|
||||
// section may be already removed
|
||||
section.elt.parentNode === contentElt && contentElt.removeChild(section.elt)
|
||||
// To detect sections that come back with built-in undo
|
||||
section.elt.section = undefined
|
||||
})
|
||||
|
||||
if (insertBeforeSection !== undefined) {
|
||||
contentElt.insertBefore(newSectionEltList, insertBeforeSection.elt)
|
||||
} else {
|
||||
contentElt.appendChild(newSectionEltList)
|
||||
}
|
||||
|
||||
// Remove unauthorized nodes (text nodes outside of sections or duplicated sections via copy/paste)
|
||||
var childNode = contentElt.firstChild
|
||||
while (childNode) {
|
||||
var nextNode = childNode.nextSibling
|
||||
if (!childNode.section) {
|
||||
contentElt.removeChild(childNode)
|
||||
}
|
||||
childNode = nextNode
|
||||
}
|
||||
this.addTrailingNode()
|
||||
self.$trigger('highlighted')
|
||||
editor.selectionMgr.restoreSelection()
|
||||
editor.selectionMgr.updateCursorCoordinates()
|
||||
}.cl_bind(this))
|
||||
|
||||
return sectionList
|
||||
}
|
||||
|
||||
function highlight(section) {
|
||||
var html = editor.options.sectionHighlighter(section).replace(/\n/g, lfHtml)
|
||||
var sectionElt = editor.$document.createElement('div')
|
||||
sectionElt.className = 'cledit-section'
|
||||
sectionElt.innerHTML = html
|
||||
section.setElement(sectionElt)
|
||||
self.$trigger('sectionHighlighted', section)
|
||||
}
|
||||
}
|
||||
|
||||
cledit.Highlighter = Highlighter
|
||||
|
183
src/cledit/cleditKeystroke.js
Normal file
183
src/cledit/cleditKeystroke.js
Normal file
|
@ -0,0 +1,183 @@
|
|||
var cledit = require('./cleditCore')
|
||||
|
||||
function Keystroke(handler, priority) {
|
||||
this.handler = handler
|
||||
this.priority = priority || 100
|
||||
}
|
||||
|
||||
cledit.Keystroke = Keystroke
|
||||
|
||||
var clearNewline
|
||||
var charTypes = Object.create(null)
|
||||
|
||||
// Word separators, as in Sublime Text
|
||||
'./\\()"\'-:,.;<>~!@#$%^&*|+=[]{}`~?'.split('').cl_each(function (wordSeparator) {
|
||||
charTypes[wordSeparator] = 'wordSeparator'
|
||||
})
|
||||
charTypes[' '] = 'space'
|
||||
charTypes['\t'] = 'space'
|
||||
charTypes['\n'] = 'newLine'
|
||||
|
||||
function getNextWordOffset(text, offset, isBackward) {
|
||||
var previousType
|
||||
while ((isBackward && offset > 0) || (!isBackward && offset < text.length)) {
|
||||
var currentType = charTypes[isBackward ? text[offset - 1] : text[offset]] || 'word'
|
||||
if (previousType && currentType !== previousType) {
|
||||
if (previousType === 'word' || currentType === 'space' || previousType === 'newLine' || currentType === 'newLine') {
|
||||
break
|
||||
}
|
||||
}
|
||||
previousType = currentType
|
||||
isBackward ? offset-- : offset++
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
cledit.defaultKeystrokes = [
|
||||
|
||||
new Keystroke(function (evt, state, editor) {
|
||||
if ((!evt.ctrlKey && !evt.metaKey) || evt.altKey) {
|
||||
return
|
||||
}
|
||||
var keyCode = evt.charCode || evt.keyCode
|
||||
var keyCodeChar = String.fromCharCode(keyCode).toLowerCase()
|
||||
var action
|
||||
switch (keyCodeChar) {
|
||||
case 'y':
|
||||
action = 'redo'
|
||||
break
|
||||
case 'z':
|
||||
action = evt.shiftKey ? 'redo' : 'undo'
|
||||
break
|
||||
}
|
||||
if (action) {
|
||||
evt.preventDefault()
|
||||
setTimeout(function () {
|
||||
editor.undoMgr[action]()
|
||||
}, 10)
|
||||
return true
|
||||
}
|
||||
}),
|
||||
|
||||
new Keystroke(function (evt, state) {
|
||||
if (evt.which !== 9 /* tab */ || evt.metaKey || evt.ctrlKey) {
|
||||
return
|
||||
}
|
||||
|
||||
function strSplice(str, i, remove, add) {
|
||||
remove = +remove || 0
|
||||
add = add || ''
|
||||
return str.slice(0, i) + add + str.slice(i + remove)
|
||||
}
|
||||
|
||||
evt.preventDefault()
|
||||
var isInverse = evt.shiftKey
|
||||
var lf = state.before.lastIndexOf('\n') + 1
|
||||
if (isInverse) {
|
||||
if (/\s/.test(state.before.charAt(lf))) {
|
||||
state.before = strSplice(state.before, lf, 1)
|
||||
}
|
||||
state.selection = state.selection.replace(/^[ \t]/gm, '')
|
||||
} else {
|
||||
if (state.selection) {
|
||||
state.before = strSplice(state.before, lf, 0, '\t')
|
||||
state.selection = state.selection.replace(/\n(?=[\s\S])/g, '\n\t')
|
||||
} else {
|
||||
state.before += '\t'
|
||||
}
|
||||
}
|
||||
return true
|
||||
}),
|
||||
|
||||
new Keystroke(function (evt, state, editor) {
|
||||
if (evt.which !== 13 /* enter */) {
|
||||
clearNewline = false
|
||||
return
|
||||
}
|
||||
|
||||
evt.preventDefault()
|
||||
var lf = state.before.lastIndexOf('\n') + 1
|
||||
if (clearNewline) {
|
||||
state.before = state.before.substring(0, lf)
|
||||
state.selection = ''
|
||||
clearNewline = false
|
||||
return true
|
||||
}
|
||||
clearNewline = false
|
||||
var previousLine = state.before.slice(lf)
|
||||
var indent = previousLine.match(/^\s*/)[0]
|
||||
if (indent.length) {
|
||||
clearNewline = true
|
||||
}
|
||||
|
||||
editor.undoMgr.setCurrentMode('single')
|
||||
state.before += '\n' + indent
|
||||
state.selection = ''
|
||||
return true
|
||||
}),
|
||||
|
||||
new Keystroke(function (evt, state, editor) {
|
||||
if (evt.which !== 8 /* backspace */ && evt.which !== 46 /* delete */) {
|
||||
return
|
||||
}
|
||||
|
||||
editor.undoMgr.setCurrentMode('delete')
|
||||
if (!state.selection) {
|
||||
var isJump = (cledit.Utils.isMac && evt.altKey) || (!cledit.Utils.isMac && evt.ctrlKey)
|
||||
if (isJump) {
|
||||
// Custom kill word behavior
|
||||
var text = state.before + state.after
|
||||
var offset = getNextWordOffset(text, state.before.length, evt.which === 8)
|
||||
if (evt.which === 8) {
|
||||
state.before = state.before.slice(0, offset)
|
||||
} else {
|
||||
state.after = state.after.slice(offset - text.length)
|
||||
}
|
||||
evt.preventDefault()
|
||||
return true
|
||||
} else if (evt.which === 8 && state.before.slice(-1) === '\n') {
|
||||
// Special treatment for end of lines
|
||||
state.before = state.before.slice(0, -1)
|
||||
evt.preventDefault()
|
||||
return true
|
||||
} else if (evt.which === 46 && state.after.slice(0, 1) === '\n') {
|
||||
state.after = state.after.slice(1)
|
||||
evt.preventDefault()
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
state.selection = ''
|
||||
evt.preventDefault()
|
||||
return true
|
||||
}
|
||||
}),
|
||||
|
||||
new Keystroke(function (evt, state, editor) {
|
||||
if (evt.which !== 37 /* left arrow */ && evt.which !== 39 /* right arrow */) {
|
||||
return
|
||||
}
|
||||
var isJump = (cledit.Utils.isMac && evt.altKey) || (!cledit.Utils.isMac && evt.ctrlKey)
|
||||
if (!isJump) {
|
||||
return
|
||||
}
|
||||
|
||||
// Custom jump behavior
|
||||
var textContent = editor.getContent()
|
||||
var offset = getNextWordOffset(textContent, editor.selectionMgr.selectionEnd, evt.which === 37)
|
||||
if (evt.shiftKey) {
|
||||
// rebuild the state completely
|
||||
var min = Math.min(editor.selectionMgr.selectionStart, offset)
|
||||
var max = Math.max(editor.selectionMgr.selectionStart, offset)
|
||||
state.before = textContent.slice(0, min)
|
||||
state.after = textContent.slice(max)
|
||||
state.selection = textContent.slice(min, max)
|
||||
state.isBackwardSelection = editor.selectionMgr.selectionStart > offset
|
||||
} else {
|
||||
state.before = textContent.slice(0, offset)
|
||||
state.after = textContent.slice(offset)
|
||||
state.selection = ''
|
||||
}
|
||||
evt.preventDefault()
|
||||
return true
|
||||
})
|
||||
]
|
44
src/cledit/cleditMarker.js
Normal file
44
src/cledit/cleditMarker.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
var cledit = require('./cleditCore')
|
||||
|
||||
var DIFF_DELETE = -1
|
||||
var DIFF_INSERT = 1
|
||||
var DIFF_EQUAL = 0
|
||||
|
||||
var idCounter = 0
|
||||
|
||||
function Marker(offset, trailing) {
|
||||
this.id = idCounter++
|
||||
this.offset = offset
|
||||
this.trailing = trailing
|
||||
}
|
||||
|
||||
Marker.prototype.adjustOffset = function (diffs) {
|
||||
var startOffset = 0
|
||||
diffs.cl_each(function (diff) {
|
||||
var diffType = diff[0]
|
||||
var diffText = diff[1]
|
||||
var diffOffset = diffText.length
|
||||
switch (diffType) {
|
||||
case DIFF_EQUAL:
|
||||
startOffset += diffOffset
|
||||
break
|
||||
case DIFF_INSERT:
|
||||
if (
|
||||
this.trailing
|
||||
? this.offset > startOffset
|
||||
: this.offset >= startOffset
|
||||
) {
|
||||
this.offset += diffOffset
|
||||
}
|
||||
startOffset += diffOffset
|
||||
break
|
||||
case DIFF_DELETE:
|
||||
if (this.offset > startOffset) {
|
||||
this.offset -= Math.min(diffOffset, this.offset - startOffset)
|
||||
}
|
||||
break
|
||||
}
|
||||
}.cl_bind(this))
|
||||
}
|
||||
|
||||
cledit.Marker = Marker
|
399
src/cledit/cleditSelectionMgr.js
Normal file
399
src/cledit/cleditSelectionMgr.js
Normal file
|
@ -0,0 +1,399 @@
|
|||
var cledit = require('./cleditCore')
|
||||
|
||||
function SelectionMgr(editor) {
|
||||
var debounce = cledit.Utils.debounce
|
||||
var contentElt = editor.$contentElt
|
||||
var scrollElt = editor.$scrollElt
|
||||
cledit.Utils.createEventHooks(this)
|
||||
|
||||
var self = this
|
||||
var lastSelectionStart = 0
|
||||
var lastSelectionEnd = 0
|
||||
this.selectionStart = 0
|
||||
this.selectionEnd = 0
|
||||
this.cursorCoordinates = {}
|
||||
this.adjustTop = 0
|
||||
this.adjustBottom = 0
|
||||
|
||||
this.findContainer = function (offset) {
|
||||
var result = cledit.Utils.findContainer(contentElt, offset)
|
||||
if (result.container.nodeValue === '\n') {
|
||||
var hdLfElt = result.container.parentNode
|
||||
if (hdLfElt.className === 'hd-lf' && hdLfElt.previousSibling && hdLfElt.previousSibling.tagName === 'BR') {
|
||||
result.container = hdLfElt.parentNode
|
||||
result.offsetInContainer = Array.prototype.indexOf.call(result.container.childNodes, result.offsetInContainer === 0 ? hdLfElt.previousSibling : hdLfElt)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
this.createRange = function (start, end) {
|
||||
var range = editor.$document.createRange()
|
||||
if (start === end) {
|
||||
end = start = isNaN(start) ? start : this.findContainer(start < 0 ? 0 : start)
|
||||
} else {
|
||||
start = isNaN(start) ? start : this.findContainer(start < 0 ? 0 : start)
|
||||
end = isNaN(end) ? end : this.findContainer(end < 0 ? 0 : end)
|
||||
}
|
||||
range.setStart(start.container, start.offsetInContainer)
|
||||
range.setEnd(end.container, end.offsetInContainer)
|
||||
return range
|
||||
}
|
||||
|
||||
var adjustScroll
|
||||
var debouncedUpdateCursorCoordinates = debounce(function () {
|
||||
var coordinates = this.getCoordinates(this.selectionEnd, this.selectionEndContainer, this.selectionEndOffset)
|
||||
if (this.cursorCoordinates.top !== coordinates.top ||
|
||||
this.cursorCoordinates.height !== coordinates.height ||
|
||||
this.cursorCoordinates.left !== coordinates.left
|
||||
) {
|
||||
this.cursorCoordinates = coordinates
|
||||
this.$trigger('cursorCoordinatesChanged', coordinates)
|
||||
}
|
||||
if (adjustScroll) {
|
||||
var adjustTop, adjustBottom
|
||||
adjustTop = adjustBottom = scrollElt.clientHeight / 2 * editor.options.cursorFocusRatio
|
||||
adjustTop = this.adjustTop || adjustTop
|
||||
adjustBottom = this.adjustBottom || adjustTop
|
||||
if (adjustTop && adjustBottom) {
|
||||
var cursorMinY = scrollElt.scrollTop + adjustTop
|
||||
var cursorMaxY = scrollElt.scrollTop + scrollElt.clientHeight - adjustBottom
|
||||
if (this.cursorCoordinates.top < cursorMinY) {
|
||||
scrollElt.scrollTop += this.cursorCoordinates.top - cursorMinY
|
||||
} else if (this.cursorCoordinates.top + this.cursorCoordinates.height > cursorMaxY) {
|
||||
scrollElt.scrollTop += this.cursorCoordinates.top + this.cursorCoordinates.height - cursorMaxY
|
||||
}
|
||||
}
|
||||
}
|
||||
adjustScroll = false
|
||||
}.cl_bind(this))
|
||||
|
||||
this.updateCursorCoordinates = function (adjustScrollParam) {
|
||||
adjustScroll = adjustScroll || adjustScrollParam
|
||||
debouncedUpdateCursorCoordinates()
|
||||
}
|
||||
|
||||
var oldSelectionRange
|
||||
|
||||
function checkSelection(selectionRange) {
|
||||
if (!oldSelectionRange ||
|
||||
oldSelectionRange.startContainer !== selectionRange.startContainer ||
|
||||
oldSelectionRange.startOffset !== selectionRange.startOffset ||
|
||||
oldSelectionRange.endContainer !== selectionRange.endContainer ||
|
||||
oldSelectionRange.endOffset !== selectionRange.endOffset
|
||||
) {
|
||||
oldSelectionRange = selectionRange
|
||||
self.$trigger('selectionChanged', self.selectionStart, self.selectionEnd, selectionRange)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
this.restoreSelection = function () {
|
||||
var min = Math.min(this.selectionStart, this.selectionEnd)
|
||||
var max = Math.max(this.selectionStart, this.selectionEnd)
|
||||
var selectionRange = this.createRange(min, max)
|
||||
if (editor.$document.contains(selectionRange.commonAncestorContainer)) {
|
||||
var selection = editor.$window.getSelection()
|
||||
selection.removeAllRanges()
|
||||
var isBackward = this.selectionStart > this.selectionEnd
|
||||
if (selection.extend) {
|
||||
var beginRange = selectionRange.cloneRange()
|
||||
beginRange.collapse(!isBackward)
|
||||
selection.addRange(beginRange)
|
||||
if (isBackward) {
|
||||
selection.extend(selectionRange.startContainer, selectionRange.startOffset)
|
||||
} else {
|
||||
selection.extend(selectionRange.endContainer, selectionRange.endOffset)
|
||||
}
|
||||
} else {
|
||||
selection.addRange(selectionRange)
|
||||
}
|
||||
checkSelection(selectionRange)
|
||||
return selectionRange
|
||||
}
|
||||
}
|
||||
|
||||
var saveLastSelection = debounce(function () {
|
||||
lastSelectionStart = self.selectionStart
|
||||
lastSelectionEnd = self.selectionEnd
|
||||
}, 50)
|
||||
|
||||
function setSelection(start, end) {
|
||||
if (start === undefined) {
|
||||
start = self.selectionStart
|
||||
}
|
||||
if (start < 0) {
|
||||
start = 0
|
||||
}
|
||||
if (end === undefined) {
|
||||
end = this.selectionEnd
|
||||
}
|
||||
if (end < 0) {
|
||||
end = 0
|
||||
}
|
||||
self.selectionStart = start
|
||||
self.selectionEnd = end
|
||||
saveLastSelection()
|
||||
}
|
||||
|
||||
this.setSelectionStartEnd = function (start, end, focus) {
|
||||
setSelection(start, end)
|
||||
return focus !== false && this.restoreSelection()
|
||||
}
|
||||
|
||||
this.saveSelectionState = (function () {
|
||||
// Credit: https://github.com/timdown/rangy
|
||||
function arrayContains(arr, val) {
|
||||
var i = arr.length
|
||||
while (i--) {
|
||||
if (arr[i] === val) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
|
||||
var p
|
||||
var n = selfIsAncestor ? node : node.parentNode
|
||||
while (n) {
|
||||
p = n.parentNode
|
||||
if (p === ancestor) {
|
||||
return n
|
||||
}
|
||||
n = p
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getNodeIndex(node) {
|
||||
var i = 0
|
||||
while ((node = node.previousSibling)) {
|
||||
++i
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
function getCommonAncestor(node1, node2) {
|
||||
var ancestors = []
|
||||
var n
|
||||
for (n = node1; n; n = n.parentNode) {
|
||||
ancestors.push(n)
|
||||
}
|
||||
|
||||
for (n = node2; n; n = n.parentNode) {
|
||||
if (arrayContains(ancestors, n)) {
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function comparePoints(nodeA, offsetA, nodeB, offsetB) {
|
||||
// See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
|
||||
var nodeC, root, childA, childB, n
|
||||
if (nodeA === nodeB) {
|
||||
// Case 1: nodes are the same
|
||||
return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1
|
||||
} else if (
|
||||
(nodeC = getClosestAncestorIn(nodeB, nodeA, true))
|
||||
) {
|
||||
// Case 2: node C (container B or an ancestor) is a child node of A
|
||||
return offsetA <= getNodeIndex(nodeC) ? -1 : 1
|
||||
} else if (
|
||||
(nodeC = getClosestAncestorIn(nodeA, nodeB, true))
|
||||
) {
|
||||
// Case 3: node C (container A or an ancestor) is a child node of B
|
||||
return getNodeIndex(nodeC) < offsetB ? -1 : 1
|
||||
} else {
|
||||
root = getCommonAncestor(nodeA, nodeB)
|
||||
if (!root) {
|
||||
throw new Error('comparePoints error: nodes have no common ancestor')
|
||||
}
|
||||
|
||||
// Case 4: containers are siblings or descendants of siblings
|
||||
childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true)
|
||||
childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true)
|
||||
|
||||
if (childA === childB) {
|
||||
// This shouldn't be possible
|
||||
throw module.createError('comparePoints got to case 4 and childA and childB are the same!')
|
||||
} else {
|
||||
n = root.firstChild
|
||||
while (n) {
|
||||
if (n === childA) {
|
||||
return -1
|
||||
} else if (n === childB) {
|
||||
return 1
|
||||
}
|
||||
n = n.nextSibling
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
var selectionStart = self.selectionStart
|
||||
var selectionEnd = self.selectionEnd
|
||||
var selection = editor.$window.getSelection()
|
||||
var result
|
||||
if (selection.rangeCount > 0) {
|
||||
var selectionRange = selection.getRangeAt(0)
|
||||
var node = selectionRange.startContainer
|
||||
if ((contentElt.compareDocumentPosition(node) & window.Node.DOCUMENT_POSITION_CONTAINED_BY) || contentElt === node) {
|
||||
var offset = selectionRange.startOffset
|
||||
if (node.firstChild && offset > 0) {
|
||||
node = node.childNodes[offset - 1]
|
||||
offset = node.textContent.length
|
||||
}
|
||||
var container = node
|
||||
while (node !== contentElt) {
|
||||
while ((node = node.previousSibling)) {
|
||||
offset += (node.textContent || '').length
|
||||
}
|
||||
node = container = container.parentNode
|
||||
}
|
||||
var selectionText = selectionRange + ''
|
||||
// Fix end of line when only br is selected
|
||||
var brElt = selectionRange.endContainer.firstChild
|
||||
if (brElt && brElt.tagName === 'BR' && selectionRange.endOffset === 1) {
|
||||
selectionText += '\n'
|
||||
}
|
||||
if (comparePoints(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset) === 1) {
|
||||
selectionStart = offset + selectionText.length
|
||||
selectionEnd = offset
|
||||
} else {
|
||||
selectionStart = offset
|
||||
selectionEnd = offset + selectionText.length
|
||||
}
|
||||
|
||||
if (selectionStart === selectionEnd && selectionStart === editor.getContent().length) {
|
||||
// If cursor is after the trailingNode
|
||||
selectionStart = --selectionEnd
|
||||
result = self.setSelectionStartEnd(selectionStart, selectionEnd)
|
||||
} else {
|
||||
setSelection(selectionStart, selectionEnd)
|
||||
result = checkSelection(selectionRange)
|
||||
result = result || lastSelectionStart !== self.selectionStart // selectionRange doesn't change when selection is at the start of a section
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function saveCheckChange() {
|
||||
return save() && (lastSelectionStart !== self.selectionStart || lastSelectionEnd !== self.selectionEnd)
|
||||
}
|
||||
|
||||
var nextTickAdjustScroll = false
|
||||
var debouncedSave = debounce(function () {
|
||||
self.updateCursorCoordinates(saveCheckChange() && nextTickAdjustScroll)
|
||||
// In some cases we have to wait a little longer to see the selection change (Cmd+A on Chrome OSX)
|
||||
longerDebouncedSave()
|
||||
})
|
||||
var longerDebouncedSave = debounce(function () {
|
||||
self.updateCursorCoordinates(saveCheckChange() && nextTickAdjustScroll)
|
||||
nextTickAdjustScroll = false
|
||||
}, 10)
|
||||
|
||||
return function (debounced, adjustScroll, forceAdjustScroll) {
|
||||
if (forceAdjustScroll) {
|
||||
lastSelectionStart = undefined
|
||||
lastSelectionEnd = undefined
|
||||
}
|
||||
if (debounced) {
|
||||
nextTickAdjustScroll = nextTickAdjustScroll || adjustScroll
|
||||
return debouncedSave()
|
||||
} else {
|
||||
save()
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
this.getSelectedText = function () {
|
||||
var min = Math.min(this.selectionStart, this.selectionEnd)
|
||||
var max = Math.max(this.selectionStart, this.selectionEnd)
|
||||
return editor.getContent().substring(min, max)
|
||||
}
|
||||
|
||||
this.getCoordinates = function (inputOffset, container, offsetInContainer) {
|
||||
if (!container) {
|
||||
var offset = this.findContainer(inputOffset)
|
||||
container = offset.container
|
||||
offsetInContainer = offset.offsetInContainer
|
||||
}
|
||||
var containerElt = container
|
||||
if (!containerElt.hasChildNodes()) {
|
||||
containerElt = container.parentNode
|
||||
}
|
||||
var isInvisible = false
|
||||
var index = editor.$allElements.indexOf(containerElt)
|
||||
while (containerElt.offsetHeight === 0 && index > 0) {
|
||||
isInvisible = true
|
||||
containerElt = editor.$allElements[--index]
|
||||
}
|
||||
var rect
|
||||
var contentRect
|
||||
var left = 'left'
|
||||
if (isInvisible || container.textContent === '\n') {
|
||||
rect = containerElt.getBoundingClientRect()
|
||||
} else {
|
||||
var selectedChar = editor.getContent()[inputOffset]
|
||||
var startOffset = {
|
||||
container: container,
|
||||
offsetInContainer: offsetInContainer
|
||||
}
|
||||
var endOffset = {
|
||||
container: container,
|
||||
offsetInContainer: offsetInContainer
|
||||
}
|
||||
if (inputOffset > 0 && (selectedChar === undefined || selectedChar === '\n')) {
|
||||
left = 'right'
|
||||
if (startOffset.offsetInContainer === 0) {
|
||||
// Need to calculate offset-1
|
||||
startOffset = inputOffset - 1
|
||||
} else {
|
||||
startOffset.offsetInContainer -= 1
|
||||
}
|
||||
} else {
|
||||
if (endOffset.offsetInContainer === container.textContent.length) {
|
||||
// Need to calculate offset+1
|
||||
endOffset = inputOffset + 1
|
||||
} else {
|
||||
endOffset.offsetInContainer += 1
|
||||
}
|
||||
}
|
||||
var range = this.createRange(startOffset, endOffset)
|
||||
rect = range.getBoundingClientRect()
|
||||
}
|
||||
contentRect = contentElt.getBoundingClientRect()
|
||||
return {
|
||||
top: Math.round(rect.top - contentRect.top + contentElt.scrollTop),
|
||||
height: Math.round(rect.height),
|
||||
left: Math.round(rect[left] - contentRect.left + contentElt.scrollLeft)
|
||||
}
|
||||
}
|
||||
|
||||
this.getClosestWordOffset = function (offset) {
|
||||
var offsetStart = 0
|
||||
var offsetEnd = 0
|
||||
var nextOffset = 0
|
||||
editor.getContent().split(/\s/).cl_some(function (word) {
|
||||
if (word) {
|
||||
offsetStart = nextOffset
|
||||
offsetEnd = nextOffset + word.length
|
||||
if (offsetEnd > offset) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
nextOffset += word.length + 1
|
||||
})
|
||||
return {
|
||||
start: offsetStart,
|
||||
end: offsetEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cledit.SelectionMgr = SelectionMgr
|
178
src/cledit/cleditUndoMgr.js
Normal file
178
src/cledit/cleditUndoMgr.js
Normal file
|
@ -0,0 +1,178 @@
|
|||
var DiffMatchPatch = require('diff-match-patch');
|
||||
var cledit = require('./cleditCore')
|
||||
|
||||
function UndoMgr(editor) {
|
||||
cledit.Utils.createEventHooks(this)
|
||||
|
||||
/* eslint-disable new-cap */
|
||||
var diffMatchPatch = new DiffMatchPatch()
|
||||
/* eslint-enable new-cap */
|
||||
|
||||
var self = this
|
||||
var selectionMgr
|
||||
var undoStack = []
|
||||
var redoStack = []
|
||||
var currentState
|
||||
var previousPatches = []
|
||||
var currentPatches = []
|
||||
var debounce = cledit.Utils.debounce
|
||||
|
||||
self.options = {
|
||||
undoStackMaxSize: 200,
|
||||
bufferStateUntilIdle: 1000,
|
||||
patchHandler: {
|
||||
makePatches: function (oldContent, newContent, diffs) {
|
||||
return diffMatchPatch.patch_make(oldContent, diffs)
|
||||
},
|
||||
applyPatches: function (patches, content) {
|
||||
return diffMatchPatch.patch_apply(patches, content)[0]
|
||||
},
|
||||
reversePatches: function (patches) {
|
||||
patches = diffMatchPatch.patch_deepCopy(patches).reverse()
|
||||
patches.cl_each(function (patch) {
|
||||
patch.diffs.cl_each(function (diff) {
|
||||
diff[0] = -diff[0]
|
||||
})
|
||||
})
|
||||
return patches
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function State() { }
|
||||
|
||||
function StateMgr() {
|
||||
var currentTime, lastTime
|
||||
var lastMode
|
||||
|
||||
this.isBufferState = function () {
|
||||
currentTime = Date.now()
|
||||
return this.currentMode !== 'single' &&
|
||||
this.currentMode === lastMode &&
|
||||
currentTime - lastTime < self.options.bufferStateUntilIdle
|
||||
}
|
||||
|
||||
this.setDefaultMode = function (mode) {
|
||||
this.currentMode = this.currentMode || mode
|
||||
}
|
||||
|
||||
this.resetMode = function () {
|
||||
stateMgr.currentMode = undefined
|
||||
lastMode = undefined
|
||||
}
|
||||
|
||||
this.saveMode = function () {
|
||||
lastMode = this.currentMode
|
||||
this.currentMode = undefined
|
||||
lastTime = currentTime
|
||||
}
|
||||
}
|
||||
|
||||
function addToStack(stack) {
|
||||
return function () {
|
||||
stack.push(this)
|
||||
this.patches = previousPatches
|
||||
previousPatches = []
|
||||
}
|
||||
}
|
||||
|
||||
State.prototype.addToUndoStack = addToStack(undoStack)
|
||||
State.prototype.addToRedoStack = addToStack(redoStack)
|
||||
|
||||
var stateMgr = new StateMgr()
|
||||
this.setCurrentMode = function (mode) {
|
||||
stateMgr.currentMode = mode
|
||||
}
|
||||
this.setDefaultMode = stateMgr.setDefaultMode.cl_bind(stateMgr)
|
||||
|
||||
this.addDiffs = function (oldContent, newContent, diffs) {
|
||||
var patches = self.options.patchHandler.makePatches(oldContent, newContent, diffs)
|
||||
currentPatches.push.apply(currentPatches, patches)
|
||||
}
|
||||
|
||||
function saveCurrentPatches() {
|
||||
// Move currentPatches into previousPatches
|
||||
Array.prototype.push.apply(previousPatches, currentPatches)
|
||||
currentPatches = []
|
||||
}
|
||||
|
||||
this.saveState = debounce(function () {
|
||||
redoStack.length = 0
|
||||
if (!stateMgr.isBufferState()) {
|
||||
currentState.addToUndoStack()
|
||||
|
||||
// Limit the size of the stack
|
||||
while (undoStack.length > self.options.undoStackMaxSize) {
|
||||
undoStack.shift()
|
||||
}
|
||||
}
|
||||
saveCurrentPatches()
|
||||
currentState = new State()
|
||||
stateMgr.saveMode()
|
||||
self.$trigger('undoStateChange')
|
||||
})
|
||||
|
||||
this.canUndo = function () {
|
||||
return !!undoStack.length
|
||||
}
|
||||
|
||||
this.canRedo = function () {
|
||||
return !!redoStack.length
|
||||
}
|
||||
|
||||
function restoreState(patches, isForward) {
|
||||
// Update editor
|
||||
var content = editor.getContent()
|
||||
if (!isForward) {
|
||||
patches = self.options.patchHandler.reversePatches(patches)
|
||||
}
|
||||
|
||||
var newContent = self.options.patchHandler.applyPatches(patches, content)
|
||||
var newContentText = newContent.text || newContent
|
||||
var range = editor.setContent(newContentText, true)
|
||||
var selection = newContent.selection || {
|
||||
start: range.end,
|
||||
end: range.end
|
||||
}
|
||||
|
||||
selectionMgr.setSelectionStartEnd(selection.start, selection.end)
|
||||
selectionMgr.updateCursorCoordinates(true)
|
||||
|
||||
stateMgr.resetMode()
|
||||
self.$trigger('undoStateChange')
|
||||
editor.adjustCursorPosition()
|
||||
}
|
||||
|
||||
this.undo = function () {
|
||||
var state = undoStack.pop()
|
||||
if (!state) {
|
||||
return
|
||||
}
|
||||
saveCurrentPatches()
|
||||
currentState.addToRedoStack()
|
||||
restoreState(currentState.patches)
|
||||
previousPatches = state.patches
|
||||
currentState = state
|
||||
}
|
||||
|
||||
this.redo = function () {
|
||||
var state = redoStack.pop()
|
||||
if (!state) {
|
||||
return
|
||||
}
|
||||
currentState.addToUndoStack()
|
||||
restoreState(state.patches, true)
|
||||
previousPatches = state.patches
|
||||
currentState = state
|
||||
}
|
||||
|
||||
this.init = function (options) {
|
||||
self.options.cl_extend(options || {})
|
||||
selectionMgr = editor.selectionMgr
|
||||
if (!currentState) {
|
||||
currentState = new State()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cledit.UndoMgr = UndoMgr
|
123
src/cledit/cleditUtils.js
Normal file
123
src/cledit/cleditUtils.js
Normal file
|
@ -0,0 +1,123 @@
|
|||
var cledit = require('./cleditCore')
|
||||
|
||||
var Utils = {
|
||||
isGecko: 'MozAppearance' in document.documentElement.style,
|
||||
isWebkit: 'WebkitAppearance' in document.documentElement.style,
|
||||
isMsie: 'msTransform' in document.documentElement.style,
|
||||
isMac: navigator.userAgent.indexOf('Mac OS X') !== -1
|
||||
}
|
||||
|
||||
// Faster than setTimeout(0). Credit: https://github.com/stefanpenner/es6-promise
|
||||
Utils.defer = (function () {
|
||||
var queue = new Array(1000)
|
||||
var queueLength = 0
|
||||
function flush() {
|
||||
for (var i = 0; i < queueLength; i++) {
|
||||
try {
|
||||
queue[i]()
|
||||
} catch (e) {
|
||||
console.error(e.message, e.stack)
|
||||
}
|
||||
queue[i] = undefined
|
||||
}
|
||||
queueLength = 0
|
||||
}
|
||||
|
||||
var iterations = 0
|
||||
var observer = new window.MutationObserver(flush)
|
||||
var node = document.createTextNode('')
|
||||
observer.observe(node, { characterData: true })
|
||||
|
||||
return function (fn) {
|
||||
queue[queueLength++] = fn
|
||||
if (queueLength === 1) {
|
||||
node.data = (iterations = ++iterations % 2)
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
Utils.debounce = function (func, wait) {
|
||||
var timeoutId, isExpected
|
||||
return wait
|
||||
? function () {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(func, wait)
|
||||
}
|
||||
: function () {
|
||||
if (!isExpected) {
|
||||
isExpected = true
|
||||
Utils.defer(function () {
|
||||
isExpected = false
|
||||
func()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Utils.createEventHooks = function (object) {
|
||||
var listenerMap = Object.create(null)
|
||||
object.$trigger = function (eventType) {
|
||||
var listeners = listenerMap[eventType]
|
||||
if (listeners) {
|
||||
var args = Array.prototype.slice.call(arguments, 1)
|
||||
listeners.cl_each(function (listener) {
|
||||
try {
|
||||
listener.apply(object, args)
|
||||
} catch (e) {
|
||||
console.error(e.message, e.stack)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
object.on = function (eventType, listener) {
|
||||
var listeners = listenerMap[eventType]
|
||||
if (!listeners) {
|
||||
listeners = []
|
||||
listenerMap[eventType] = listeners
|
||||
}
|
||||
listeners.push(listener)
|
||||
}
|
||||
object.off = function (eventType, listener) {
|
||||
var listeners = listenerMap[eventType]
|
||||
if (listeners) {
|
||||
var index = listeners.indexOf(listener)
|
||||
if (~index) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Utils.findContainer = function (elt, offset) {
|
||||
var containerOffset = 0
|
||||
var container
|
||||
do {
|
||||
container = elt
|
||||
elt = elt.firstChild
|
||||
if (elt) {
|
||||
do {
|
||||
var len = elt.textContent.length
|
||||
if (containerOffset <= offset && containerOffset + len > offset) {
|
||||
break
|
||||
}
|
||||
containerOffset += len
|
||||
} while ((elt = elt.nextSibling))
|
||||
}
|
||||
} while (elt && elt.firstChild && elt.nodeType !== 3)
|
||||
|
||||
if (elt) {
|
||||
return {
|
||||
container: elt,
|
||||
offsetInContainer: offset - containerOffset
|
||||
}
|
||||
}
|
||||
while (container.lastChild) {
|
||||
container = container.lastChild
|
||||
}
|
||||
return {
|
||||
container: container,
|
||||
offsetInContainer: container.nodeType === 3 ? container.textContent.length : 0
|
||||
}
|
||||
}
|
||||
|
||||
cledit.Utils = Utils
|
33
src/cledit/cleditWatcher.js
Normal file
33
src/cledit/cleditWatcher.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
var cledit = require('./cleditCore')
|
||||
|
||||
function Watcher(editor, listener) {
|
||||
this.isWatching = false
|
||||
var contentObserver
|
||||
this.startWatching = function () {
|
||||
this.stopWatching()
|
||||
this.isWatching = true
|
||||
contentObserver = new window.MutationObserver(listener)
|
||||
contentObserver.observe(editor.$contentElt, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true
|
||||
})
|
||||
}
|
||||
this.stopWatching = function () {
|
||||
if (contentObserver) {
|
||||
contentObserver.disconnect()
|
||||
contentObserver = undefined
|
||||
}
|
||||
this.isWatching = false
|
||||
}
|
||||
this.noWatch = function (cb) {
|
||||
if (this.isWatching === true) {
|
||||
this.stopWatching()
|
||||
cb()
|
||||
return this.startWatching()
|
||||
}
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
cledit.Watcher = Watcher
|
166
src/cledit/clunderscore.js
Normal file
166
src/cledit/clunderscore.js
Normal file
|
@ -0,0 +1,166 @@
|
|||
var arrayProperties = {}
|
||||
var liveCollectionProperties = {}
|
||||
var functionProperties = {}
|
||||
var objectProperties = {}
|
||||
var slice = Array.prototype.slice
|
||||
|
||||
arrayProperties.cl_each = function (cb) {
|
||||
var i = 0
|
||||
var length = this.length
|
||||
for (; i < length; i++) {
|
||||
cb(this[i], i, this)
|
||||
}
|
||||
}
|
||||
|
||||
arrayProperties.cl_map = function (cb) {
|
||||
var i = 0
|
||||
var length = this.length
|
||||
var result = Array(length)
|
||||
for (; i < length; i++) {
|
||||
result[i] = cb(this[i], i, this)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
arrayProperties.cl_reduce = function (cb, memo) {
|
||||
var i = 0
|
||||
var length = this.length
|
||||
for (; i < length; i++) {
|
||||
memo = cb(memo, this[i], i, this)
|
||||
}
|
||||
return memo
|
||||
}
|
||||
|
||||
arrayProperties.cl_some = function (cb) {
|
||||
var i = 0
|
||||
var length = this.length
|
||||
for (; i < length; i++) {
|
||||
if (cb(this[i], i, this)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
arrayProperties.cl_filter = function (cb) {
|
||||
var i = 0
|
||||
var length = this.length
|
||||
var result = []
|
||||
for (; i < length; i++) {
|
||||
cb(this[i], i, this) && result.push(this[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
liveCollectionProperties.cl_each = function (cb) {
|
||||
slice.call(this).cl_each(cb)
|
||||
}
|
||||
|
||||
liveCollectionProperties.cl_map = function (cb) {
|
||||
return slice.call(this).cl_map(cb)
|
||||
}
|
||||
|
||||
liveCollectionProperties.cl_reduce = function (cb, memo) {
|
||||
return slice.call(this).cl_reduce(cb, memo)
|
||||
}
|
||||
|
||||
functionProperties.cl_bind = function (context) {
|
||||
var self = this
|
||||
var args = slice.call(arguments, 1)
|
||||
context = context || null
|
||||
return args.length
|
||||
? function () {
|
||||
return arguments.length
|
||||
? self.apply(context, args.concat(slice.call(arguments)))
|
||||
: self.apply(context, args)
|
||||
}
|
||||
: function () {
|
||||
return arguments.length
|
||||
? self.apply(context, arguments)
|
||||
: self.call(context)
|
||||
}
|
||||
}
|
||||
|
||||
objectProperties.cl_each = function (cb) {
|
||||
var i = 0
|
||||
var keys = Object.keys(this)
|
||||
var length = keys.length
|
||||
for (; i < length; i++) {
|
||||
cb(this[keys[i]], keys[i], this)
|
||||
}
|
||||
}
|
||||
|
||||
objectProperties.cl_map = function (cb) {
|
||||
var i = 0
|
||||
var keys = Object.keys(this)
|
||||
var length = keys.length
|
||||
var result = Array(length)
|
||||
for (; i < length; i++) {
|
||||
result[i] = cb(this[keys[i]], keys[i], this)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
objectProperties.cl_reduce = function (cb, memo) {
|
||||
var i = 0
|
||||
var keys = Object.keys(this)
|
||||
var length = keys.length
|
||||
for (; i < length; i++) {
|
||||
memo = cb(memo, this[keys[i]], keys[i], this)
|
||||
}
|
||||
return memo
|
||||
}
|
||||
|
||||
objectProperties.cl_some = function (cb) {
|
||||
var i = 0
|
||||
var keys = Object.keys(this)
|
||||
var length = keys.length
|
||||
for (; i < length; i++) {
|
||||
if (cb(this[keys[i]], keys[i], this)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
objectProperties.cl_extend = function (obj) {
|
||||
if (obj) {
|
||||
var i = 0
|
||||
var keys = Object.keys(obj)
|
||||
var length = keys.length
|
||||
for (; i < length; i++) {
|
||||
this[keys[i]] = obj[keys[i]]
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
function build(properties) {
|
||||
return objectProperties.cl_reduce.call(properties, function (memo, value, key) {
|
||||
memo[key] = {
|
||||
value: value
|
||||
}
|
||||
return memo
|
||||
}, {})
|
||||
}
|
||||
|
||||
arrayProperties = build(arrayProperties)
|
||||
liveCollectionProperties = build(liveCollectionProperties)
|
||||
functionProperties = build(functionProperties)
|
||||
objectProperties = build(objectProperties)
|
||||
|
||||
/* eslint-disable no-extend-native */
|
||||
Object.defineProperties(Array.prototype, arrayProperties)
|
||||
Object.defineProperties(Int8Array.prototype, arrayProperties)
|
||||
Object.defineProperties(Uint8Array.prototype, arrayProperties)
|
||||
Object.defineProperties(Uint8ClampedArray.prototype, arrayProperties)
|
||||
Object.defineProperties(Int16Array.prototype, arrayProperties)
|
||||
Object.defineProperties(Uint16Array.prototype, arrayProperties)
|
||||
Object.defineProperties(Int32Array.prototype, arrayProperties)
|
||||
Object.defineProperties(Uint32Array.prototype, arrayProperties)
|
||||
Object.defineProperties(Float32Array.prototype, arrayProperties)
|
||||
Object.defineProperties(Float64Array.prototype, arrayProperties)
|
||||
Object.defineProperties(Function.prototype, functionProperties)
|
||||
Object.defineProperties(Object.prototype, objectProperties)
|
||||
if (typeof window !== 'undefined') {
|
||||
Object.defineProperties(HTMLCollection.prototype, liveCollectionProperties)
|
||||
Object.defineProperties(NodeList.prototype, liveCollectionProperties)
|
||||
}
|
424
src/cledit/htmlSanitizer.js
Normal file
424
src/cledit/htmlSanitizer.js
Normal file
|
@ -0,0 +1,424 @@
|
|||
const aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/;
|
||||
const imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/;
|
||||
|
||||
const urlParsingNode = window.document.createElement('a');
|
||||
|
||||
function sanitizeUri(uri, isImage) {
|
||||
const regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist;
|
||||
urlParsingNode.setAttribute('href', uri);
|
||||
const normalizedVal = urlParsingNode.href;
|
||||
if (normalizedVal !== '' && !normalizedVal.match(regex)) {
|
||||
return `unsafe:${normalizedVal}`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
var buf;
|
||||
|
||||
/* jshint -W083 */
|
||||
|
||||
// Regular Expressions for parsing tags and attributes
|
||||
var START_TAG_REGEXP =
|
||||
/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,
|
||||
END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/,
|
||||
ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
|
||||
BEGIN_TAG_REGEXP = /^</,
|
||||
BEGING_END_TAGE_REGEXP = /^<\//,
|
||||
COMMENT_REGEXP = /<!--(.*?)-->/g,
|
||||
DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
|
||||
CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
|
||||
SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
|
||||
// Match everything outside of normal chars and " (quote character)
|
||||
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
|
||||
|
||||
|
||||
// Good source of info about elements and attributes
|
||||
// http://dev.w3.org/html5/spec/Overview.html#semantics
|
||||
// http://simon.html5.org/html-elements
|
||||
|
||||
// Safe Void Elements - HTML5
|
||||
// http://dev.w3.org/html5/spec/Overview.html#void-elements
|
||||
var voidElements = makeMap("area,br,col,hr,img,wbr");
|
||||
|
||||
// Elements that you can, intentionally, leave open (and which close themselves)
|
||||
// http://dev.w3.org/html5/spec/Overview.html#optional-tags
|
||||
var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
|
||||
optionalEndTagInlineElements = makeMap("rp,rt"),
|
||||
optionalEndTagElements = {
|
||||
...optionalEndTagInlineElements,
|
||||
...optionalEndTagBlockElements,
|
||||
};
|
||||
|
||||
// Safe Block Elements - HTML5
|
||||
var blockElements = {
|
||||
...optionalEndTagBlockElements,
|
||||
...makeMap("address,article," +
|
||||
"aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
|
||||
"h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")
|
||||
};
|
||||
|
||||
// benweet: Add iframe
|
||||
blockElements.iframe = true;
|
||||
|
||||
// Inline Elements - HTML5
|
||||
var inlineElements = {
|
||||
...optionalEndTagInlineElements,
|
||||
...makeMap("a,abbr,acronym,b," +
|
||||
"bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
|
||||
"samp,small,span,strike,strong,sub,sup,time,tt,u,var")
|
||||
};
|
||||
|
||||
// SVG Elements
|
||||
// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
|
||||
// Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
|
||||
// They can potentially allow for arbitrary javascript to be executed. See #11290
|
||||
var svgElements = makeMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," +
|
||||
"hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," +
|
||||
"radialGradient,rect,stop,svg,switch,text,title,tspan,use");
|
||||
|
||||
// Special Elements (can contain anything)
|
||||
var specialElements = makeMap("script,style");
|
||||
|
||||
var validElements = {
|
||||
...voidElements,
|
||||
...blockElements,
|
||||
...inlineElements,
|
||||
...optionalEndTagElements,
|
||||
...svgElements,
|
||||
};
|
||||
|
||||
//Attributes that have href and hence need to be sanitized
|
||||
var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href");
|
||||
|
||||
var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
|
||||
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
|
||||
'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
|
||||
'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
|
||||
'valign,value,vspace,width');
|
||||
|
||||
// SVG attributes (without "id" and "name" attributes)
|
||||
// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
|
||||
var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
|
||||
'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
|
||||
'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
|
||||
'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
|
||||
'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' +
|
||||
'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' +
|
||||
'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' +
|
||||
'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' +
|
||||
'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' +
|
||||
'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' +
|
||||
'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' +
|
||||
'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' +
|
||||
'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' +
|
||||
'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' +
|
||||
'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true);
|
||||
|
||||
var validAttrs = {
|
||||
...uriAttrs,
|
||||
...svgAttrs,
|
||||
...htmlAttrs,
|
||||
};
|
||||
|
||||
// benweet: Add id and allowfullscreen (YouTube iframe)
|
||||
validAttrs.id = true;
|
||||
validAttrs.allowfullscreen = true;
|
||||
|
||||
function makeMap(str, lowercaseKeys) {
|
||||
var obj = {},
|
||||
items = str.split(','),
|
||||
i;
|
||||
for (i = 0; i < items.length; i++) {
|
||||
obj[lowercaseKeys ? items[i].toLowerCase() : items[i]] = true;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @example
|
||||
* htmlParser(htmlString, {
|
||||
* start: function(tag, attrs, unary) {},
|
||||
* end: function(tag) {},
|
||||
* chars: function(text) {},
|
||||
* comment: function(text) {}
|
||||
* });
|
||||
*
|
||||
* @param {string} html string
|
||||
* @param {object} handler
|
||||
*/
|
||||
function htmlParser(html, handler) {
|
||||
if (typeof html !== 'string') {
|
||||
if (html === null || typeof html === 'undefined') {
|
||||
html = '';
|
||||
} else {
|
||||
html = '' + html;
|
||||
}
|
||||
}
|
||||
var index, chars, match, stack = [],
|
||||
last = html,
|
||||
text;
|
||||
stack.last = function () {
|
||||
return stack[stack.length - 1];
|
||||
};
|
||||
|
||||
while (html) {
|
||||
text = '';
|
||||
chars = true;
|
||||
|
||||
// Make sure we're not in a script or style element
|
||||
if (!stack.last() || !specialElements[stack.last()]) {
|
||||
|
||||
// Comment
|
||||
if (html.indexOf("<!--") === 0) {
|
||||
// comments containing -- are not allowed unless they terminate the comment
|
||||
index = html.indexOf("--", 4);
|
||||
|
||||
if (index >= 0 && html.lastIndexOf("-->", index) === index) {
|
||||
if (handler.comment) handler.comment(html.substring(4, index));
|
||||
html = html.substring(index + 3);
|
||||
chars = false;
|
||||
}
|
||||
// DOCTYPE
|
||||
} else if (DOCTYPE_REGEXP.test(html)) {
|
||||
match = html.match(DOCTYPE_REGEXP);
|
||||
|
||||
if (match) {
|
||||
html = html.replace(match[0], '');
|
||||
chars = false;
|
||||
}
|
||||
// end tag
|
||||
} else if (BEGING_END_TAGE_REGEXP.test(html)) {
|
||||
match = html.match(END_TAG_REGEXP);
|
||||
|
||||
if (match) {
|
||||
html = html.substring(match[0].length);
|
||||
match[0].replace(END_TAG_REGEXP, parseEndTag);
|
||||
chars = false;
|
||||
}
|
||||
|
||||
// start tag
|
||||
} else if (BEGIN_TAG_REGEXP.test(html)) {
|
||||
match = html.match(START_TAG_REGEXP);
|
||||
|
||||
if (match) {
|
||||
// We only have a valid start-tag if there is a '>'.
|
||||
if (match[4]) {
|
||||
html = html.substring(match[0].length);
|
||||
match[0].replace(START_TAG_REGEXP, parseStartTag);
|
||||
}
|
||||
chars = false;
|
||||
} else {
|
||||
// no ending tag found --- this piece should be encoded as an entity.
|
||||
text += '<';
|
||||
html = html.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (chars) {
|
||||
index = html.indexOf("<");
|
||||
|
||||
text += index < 0 ? html : html.substring(0, index);
|
||||
html = index < 0 ? "" : html.substring(index);
|
||||
|
||||
if (handler.chars) handler.chars(decodeEntities(text));
|
||||
}
|
||||
|
||||
} else {
|
||||
// IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w].
|
||||
html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
|
||||
function (all, text) {
|
||||
text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
|
||||
|
||||
if (handler.chars) handler.chars(decodeEntities(text));
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
parseEndTag("", stack.last());
|
||||
}
|
||||
|
||||
if (html == last) {
|
||||
// benweet
|
||||
// throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
|
||||
// "of html: {0}", html);
|
||||
stack.reverse();
|
||||
return stack.cl_each(function (tag) {
|
||||
buf.push('</');
|
||||
buf.push(tag);
|
||||
buf.push('>');
|
||||
});
|
||||
}
|
||||
last = html;
|
||||
}
|
||||
|
||||
// Clean up any remaining tags
|
||||
parseEndTag();
|
||||
|
||||
function parseStartTag(tag, tagName, rest, unary) {
|
||||
tagName = tagName && tagName.toLowerCase();
|
||||
if (blockElements[tagName]) {
|
||||
while (stack.last() && inlineElements[stack.last()]) {
|
||||
parseEndTag("", stack.last());
|
||||
}
|
||||
}
|
||||
|
||||
if (optionalEndTagElements[tagName] && stack.last() == tagName) {
|
||||
parseEndTag("", tagName);
|
||||
}
|
||||
|
||||
unary = voidElements[tagName] || !!unary;
|
||||
|
||||
if (!unary) {
|
||||
stack.push(tagName);
|
||||
}
|
||||
|
||||
var attrs = {};
|
||||
|
||||
rest.replace(ATTR_REGEXP,
|
||||
function (match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
|
||||
var value = doubleQuotedValue || singleQuotedValue || unquotedValue || '';
|
||||
|
||||
attrs[name] = decodeEntities(value);
|
||||
});
|
||||
if (handler.start) handler.start(tagName, attrs, unary);
|
||||
}
|
||||
|
||||
function parseEndTag(tag, tagName) {
|
||||
var pos = 0,
|
||||
i;
|
||||
tagName = tagName && tagName.toLowerCase();
|
||||
if (tagName) {
|
||||
// Find the closest opened tag of the same type
|
||||
for (pos = stack.length - 1; pos >= 0; pos--) {
|
||||
if (stack[pos] == tagName) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos >= 0) {
|
||||
// Close all the open elements, up the stack
|
||||
for (i = stack.length - 1; i >= pos; i--)
|
||||
if (handler.end) handler.end(stack[i]);
|
||||
|
||||
// Remove the open elements from the stack
|
||||
stack.length = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hiddenPre = document.createElement("pre");
|
||||
/**
|
||||
* decodes all entities into regular string
|
||||
* @param value
|
||||
* @returns {string} A string with decoded entities.
|
||||
*/
|
||||
function decodeEntities(value) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
hiddenPre.innerHTML = value.replace(/</g, "<");
|
||||
// innerText depends on styling as it doesn't display hidden elements.
|
||||
// Therefore, it's better to use textContent not to cause unnecessary reflows.
|
||||
return hiddenPre.textContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes all potentially dangerous characters, so that the
|
||||
* resulting string can be safely inserted into attribute or
|
||||
* element text.
|
||||
* @param value
|
||||
* @returns {string} escaped text
|
||||
*/
|
||||
function encodeEntities(value) {
|
||||
return value.
|
||||
replace(/&/g, '&').
|
||||
replace(SURROGATE_PAIR_REGEXP, function (value) {
|
||||
var hi = value.charCodeAt(0);
|
||||
var low = value.charCodeAt(1);
|
||||
return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
|
||||
}).
|
||||
replace(NON_ALPHANUMERIC_REGEXP, function (value) {
|
||||
return '&#' + value.charCodeAt(0) + ';';
|
||||
}).
|
||||
replace(/</g, '<').
|
||||
replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* create an HTML/XML writer which writes to buffer
|
||||
* @param {Array} buf use buf.jain('') to get out sanitized html string
|
||||
* @returns {object} in the form of {
|
||||
* start: function(tag, attrs, unary) {},
|
||||
* end: function(tag) {},
|
||||
* chars: function(text) {},
|
||||
* comment: function(text) {}
|
||||
* }
|
||||
*/
|
||||
function htmlSanitizeWriter(buf, uriValidator) {
|
||||
var ignore = false;
|
||||
var out = buf.push.bind(buf);
|
||||
return {
|
||||
start: function (tag, attrs, unary) {
|
||||
tag = tag && tag.toLowerCase();
|
||||
if (!ignore && specialElements[tag]) {
|
||||
ignore = tag;
|
||||
}
|
||||
if (!ignore && validElements[tag] === true) {
|
||||
out('<');
|
||||
out(tag);
|
||||
Object.keys(attrs).forEach(function (key) {
|
||||
var value = attrs[key];
|
||||
var lkey = key && key.toLowerCase();
|
||||
var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
|
||||
if (validAttrs[lkey] === true &&
|
||||
(uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
|
||||
out(' ');
|
||||
out(key);
|
||||
out('="');
|
||||
out(encodeEntities(value));
|
||||
out('"');
|
||||
}
|
||||
});
|
||||
out(unary ? '/>' : '>');
|
||||
}
|
||||
},
|
||||
end: function (tag) {
|
||||
tag = tag && tag.toLowerCase();
|
||||
if (!ignore && validElements[tag] === true) {
|
||||
out('</');
|
||||
out(tag);
|
||||
out('>');
|
||||
}
|
||||
if (tag == ignore) {
|
||||
ignore = false;
|
||||
}
|
||||
},
|
||||
chars: function (chars) {
|
||||
if (!ignore) {
|
||||
out(encodeEntities(chars));
|
||||
}
|
||||
},
|
||||
comment: function (comment) {
|
||||
if (!ignore) {
|
||||
out('<!--');
|
||||
out(encodeEntities(comment));
|
||||
out('-->');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeHtml(html) {
|
||||
buf = [];
|
||||
htmlParser(html, htmlSanitizeWriter(buf, function (uri, isImage) {
|
||||
return !/^unsafe/.test(sanitizeUri(uri, isImage));
|
||||
}));
|
||||
return buf.join('');
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
sanitizeHtml,
|
||||
sanitizeUri,
|
||||
}
|
422
src/cledit/mdGrammar.js
Normal file
422
src/cledit/mdGrammar.js
Normal file
|
@ -0,0 +1,422 @@
|
|||
var charInsideUrl = '(&|[-A-Z0-9+@#/%?=~_|[\\]()!:,.;])'
|
||||
var charEndingUrl = '(&|[-A-Z0-9+@#/%=~_|[\\])])'
|
||||
var urlPattern = new RegExp('(https?|ftp)(://' + charInsideUrl + '*' + charEndingUrl + ')(?=$|\\W)', 'gi')
|
||||
var emailPattern = /(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)/gi
|
||||
|
||||
var markup = {
|
||||
'comment': /<!--[\w\W]*?-->/g,
|
||||
'tag': {
|
||||
pattern: /<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|[^\s'">=]+))?\s*)*\/?>/gi,
|
||||
inside: {
|
||||
'tag': {
|
||||
pattern: /^<\/?[\w:-]+/i,
|
||||
inside: {
|
||||
'punctuation': /^<\/?/,
|
||||
'namespace': /^[\w-]+?:/
|
||||
}
|
||||
},
|
||||
'attr-value': {
|
||||
pattern: /=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi,
|
||||
inside: {
|
||||
'punctuation': /=|>|"/g
|
||||
}
|
||||
},
|
||||
'punctuation': /\/?>/g,
|
||||
'attr-name': {
|
||||
pattern: /[\w:-]+/g,
|
||||
inside: {
|
||||
'namespace': /^[\w-]+?:/
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'entity': /&#?[\da-z]{1,8};/gi
|
||||
}
|
||||
|
||||
var latex = {
|
||||
// A tex command e.g. \foo
|
||||
'keyword': /\\(?:[^a-zA-Z]|[a-zA-Z]+)/g,
|
||||
// Curly and square braces
|
||||
'lparen': /[[({]/g,
|
||||
// Curly and square braces
|
||||
'rparen': /[\])}]/g,
|
||||
// A comment. Tex comments start with % and go to
|
||||
// the end of the line
|
||||
'comment': /%.*/g
|
||||
}
|
||||
|
||||
module.exports = function (options) {
|
||||
options = options || {}
|
||||
var grammar = {}
|
||||
var insideFences = options.insideFences || {}
|
||||
insideFences['cl cl-pre'] = /`{3}|~{3}/
|
||||
if (options.fences) {
|
||||
grammar['pre gfm'] = {
|
||||
pattern: /^(`{3}|~{3}).*\n(?:[\s\S]*?)\n\1 *$/gm,
|
||||
inside: insideFences
|
||||
}
|
||||
}
|
||||
grammar.li = {
|
||||
pattern: new RegExp(
|
||||
[
|
||||
'^ {0,3}(?:[*+\\-]|\\d+\\.)[ \\t].+\\n', // Item line
|
||||
'(?:',
|
||||
'(?:',
|
||||
'.*\\S.*\\n', // Non-empty line
|
||||
'|',
|
||||
'[ \\t]*\\n(?! ?\\S)', // Or empty line not followed by unindented line
|
||||
')',
|
||||
')*'
|
||||
].join(''),
|
||||
'gm'
|
||||
),
|
||||
inside: {
|
||||
'cl cl-li': /^[ \t]*([*+\-]|\d+\.)[ \t]/gm
|
||||
}
|
||||
}
|
||||
if (options.fences) {
|
||||
grammar.li.inside['pre gfm'] = {
|
||||
pattern: /^((?: {4}|\t)+)(`{3}|~{3}).*\n(?:[\s\S]*?)\n\1\2\s*$/gm,
|
||||
inside: insideFences
|
||||
}
|
||||
}
|
||||
grammar.blockquote = {
|
||||
pattern: /^ {0,3}>.+(?:\n[ \t]*\S.*)*/gm,
|
||||
inside: {
|
||||
'cl cl-gt': /^\s*>/gm,
|
||||
'li': grammar.li
|
||||
}
|
||||
}
|
||||
grammar['h1 alt'] = {
|
||||
pattern: /^.+\n=+[ \t]*$/gm,
|
||||
inside: {
|
||||
'cl cl-hash': /=+[ \t]*$/
|
||||
}
|
||||
}
|
||||
grammar['h2 alt'] = {
|
||||
pattern: /^.+\n-+[ \t]*$/gm,
|
||||
inside: {
|
||||
'cl cl-hash': /-+[ \t]*$/
|
||||
}
|
||||
}
|
||||
for (var i = 6; i >= 1; i--) {
|
||||
grammar['h' + i] = {
|
||||
pattern: new RegExp('^#{' + i + '}[ \t].+$', 'gm'),
|
||||
inside: {
|
||||
'cl cl-hash': new RegExp('^#{' + i + '}')
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.tables) {
|
||||
grammar.table = {
|
||||
pattern: new RegExp(
|
||||
[
|
||||
'^',
|
||||
'[ ]{0,3}',
|
||||
'[|]', // Initial pipe
|
||||
'.+\\n', // Header Row
|
||||
'[ ]{0,3}',
|
||||
'[|][ ]*[-:]+[-| :]*\\n', // Separator
|
||||
'(?:[ \t]*[|].*\\n?)*', // Table rows
|
||||
'$'
|
||||
].join(''),
|
||||
'gm'
|
||||
),
|
||||
inside: {}
|
||||
}
|
||||
grammar['table alt'] = {
|
||||
pattern: new RegExp(
|
||||
[
|
||||
'^',
|
||||
'[ ]{0,3}',
|
||||
'\\S.*[|].*\\n', // Header Row
|
||||
'[ ]{0,3}',
|
||||
'[-:]+[ ]*[|][-| :]*\\n', // Separator
|
||||
'(?:.*[|].*\\n?)*', // Table rows
|
||||
'$' // Stop at final newline
|
||||
].join(''),
|
||||
'gm'
|
||||
),
|
||||
inside: {}
|
||||
}
|
||||
}
|
||||
if (options.deflists) {
|
||||
grammar.deflist = {
|
||||
pattern: new RegExp(
|
||||
[
|
||||
'^ {0,3}\\S.*\\n', // Description line
|
||||
'(?:[ \\t]*\\n)?', // Optional empty line
|
||||
'(?:',
|
||||
'[ \\t]*:[ \\t].*\\n', // Colon line
|
||||
'(?:',
|
||||
'(?:',
|
||||
'.*\\S.*\\n', // Non-empty line
|
||||
'|',
|
||||
'[ \\t]*\\n(?! ?\\S)', // Or empty line not followed by unindented line
|
||||
')',
|
||||
')*',
|
||||
'(?:[ \\t]*\\n)*', // Empty lines
|
||||
')+'
|
||||
].join(''),
|
||||
'gm'
|
||||
),
|
||||
inside: {
|
||||
'deflist-desc': {
|
||||
pattern: /( {0,3}\S.*\n(?:[ \t]*\n)?)[\s\S]*/,
|
||||
lookbehind: true,
|
||||
inside: {
|
||||
'cl': /^[ \t]*:[ \t]/gm
|
||||
}
|
||||
},
|
||||
'term': /.+/g
|
||||
}
|
||||
}
|
||||
if (options.fences) {
|
||||
grammar.deflist.inside['deflist-desc'].inside['pre gfm'] = {
|
||||
pattern: /^((?: {4}|\t)+)(`{3}|~{3}).*\n(?:[\s\S]*?)\n\1\2\s*$/gm,
|
||||
inside: insideFences
|
||||
}
|
||||
}
|
||||
}
|
||||
grammar.hr = {
|
||||
pattern: /^ {0,3}([*\-_] *){3,}$/gm
|
||||
}
|
||||
if (options.footnotes) {
|
||||
grammar.fndef = {
|
||||
pattern: /^ {0,3}\[\^.*?\]:.*$/gm,
|
||||
inside: {
|
||||
'ref-id': {
|
||||
pattern: /^ {0,3}\[\^.*?\]/,
|
||||
inside: {
|
||||
cl: /(\[\^|\])/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.abbrs) {
|
||||
grammar.abbrdef = {
|
||||
pattern: /^ {0,3}\*\[.*?\]:.*$/gm,
|
||||
inside: {
|
||||
'abbr-id': {
|
||||
pattern: /^ {0,3}\*\[.*?\]/,
|
||||
inside: {
|
||||
cl: /(\*\[|\])/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
grammar.linkdef = {
|
||||
pattern: /^ {0,3}\[.*?\]:.*$/gm,
|
||||
inside: {
|
||||
'link-id': {
|
||||
pattern: /^ {0,3}\[.*?\]/,
|
||||
inside: {
|
||||
cl: /[\[\]]/
|
||||
}
|
||||
},
|
||||
url: urlPattern
|
||||
}
|
||||
}
|
||||
grammar.p = {
|
||||
pattern: /^ {0,3}\S.*$(\n.*\S.*)*/gm,
|
||||
inside: {}
|
||||
}
|
||||
if (options.tocs) {
|
||||
grammar.p.inside['cl cl-toc'] = /^[ \t]*\[toc\]$/mi
|
||||
}
|
||||
grammar.pre = {
|
||||
pattern: /(?: {4}|\t).*\S.*\n((?: {4}|\t).*\n)*/g
|
||||
}
|
||||
|
||||
var rest = {}
|
||||
rest.code = {
|
||||
pattern: /(`+)[\s\S]*?\1/g,
|
||||
inside: {
|
||||
'cl cl-code': /`/
|
||||
}
|
||||
}
|
||||
if (options.maths) {
|
||||
rest['math block'] = {
|
||||
pattern: /\\\\\[[\s\S]*?\\\\\]/g,
|
||||
inside: {
|
||||
'cl cl-bracket-start': /^\\\\\[/,
|
||||
'cl cl-bracket-end': /\\\\\]$/,
|
||||
rest: latex
|
||||
}
|
||||
}
|
||||
rest['math inline'] = {
|
||||
pattern: /\\\\\([\s\S]*?\\\\\)/g,
|
||||
inside: {
|
||||
'cl cl-bracket-start': /^\\\\\(/,
|
||||
'cl cl-bracket-end': /\\\\\)$/,
|
||||
rest: latex
|
||||
}
|
||||
}
|
||||
rest['math expr block'] = {
|
||||
pattern: /(\$\$)[\s\S]*?\1/g,
|
||||
inside: {
|
||||
'cl cl-bracket-start': /^\$\$/,
|
||||
'cl cl-bracket-end': /\$\$$/,
|
||||
rest: latex
|
||||
}
|
||||
}
|
||||
rest['math expr inline'] = {
|
||||
pattern: /\$(?!\s)[\s\S]*?\S\$(?!\d)/g,
|
||||
inside: {
|
||||
'cl cl-bracket-start': /^\$/,
|
||||
'cl cl-bracket-end': /\$$/,
|
||||
rest: latex
|
||||
}
|
||||
}
|
||||
rest['latex block'] = {
|
||||
pattern: /\\begin\{([a-z]*\*?)\}[\s\S]*?\\?\\end\{\1\}/g,
|
||||
inside: {
|
||||
'keyword': /\\(begin|end)/,
|
||||
rest: latex
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.footnotes) {
|
||||
rest.inlinefn = {
|
||||
pattern: /\^\[.+?\]/g,
|
||||
inside: {
|
||||
'cl': /(\^\[|\])/
|
||||
}
|
||||
}
|
||||
rest.fn = {
|
||||
pattern: /\[\^.+?\]/g,
|
||||
inside: {
|
||||
'cl': /(\[\^|\])/
|
||||
}
|
||||
}
|
||||
}
|
||||
rest.img = {
|
||||
pattern: /!\[.*?\]\(.+?\)/g,
|
||||
inside: {
|
||||
'cl cl-title': /['‘][^'’]*['’]|["“][^"”]*["”](?=\)$)/,
|
||||
'cl cl-src': {
|
||||
pattern: /(\]\()[^\('" \t]+(?=[\)'" \t])/,
|
||||
lookbehind: true
|
||||
}
|
||||
}
|
||||
}
|
||||
rest.link = {
|
||||
pattern: /\[.*?\]\(.+?\)/gm,
|
||||
inside: {
|
||||
'cl cl-underlined-text': {
|
||||
pattern: /(\[)[^\]]*/,
|
||||
lookbehind: true
|
||||
},
|
||||
'cl cl-title': /['‘][^'’]*['’]|["“][^"”]*["”](?=\)$)/
|
||||
}
|
||||
}
|
||||
rest.imgref = {
|
||||
pattern: /!\[.*?\][ \t]*\[.*?\]/g
|
||||
}
|
||||
rest.linkref = {
|
||||
pattern: /\[.*?\][ \t]*\[.*?\]/g,
|
||||
inside: {
|
||||
'cl cl-underlined-text': {
|
||||
pattern: /^(\[)[^\]]*(?=\][ \t]*\[)/,
|
||||
lookbehind: true
|
||||
}
|
||||
}
|
||||
}
|
||||
rest.comment = markup.comment
|
||||
rest.tag = markup.tag
|
||||
rest.url = urlPattern
|
||||
rest.email = emailPattern
|
||||
rest.strong = {
|
||||
pattern: /(^|[^\w*])([_\*])\2(?![_\*])[\s\S]*?\2{2}(?=([^\w*]|$))/gm,
|
||||
lookbehind: true,
|
||||
inside: {
|
||||
'cl cl-strong cl-start': /^([_\*])\1/,
|
||||
'cl cl-strong cl-close': /([_\*])\1$/
|
||||
}
|
||||
}
|
||||
rest.em = {
|
||||
pattern: /(^|[^\w*])([_\*])(?![_\*])[\s\S]*?\2(?=([^\w*]|$))/gm,
|
||||
lookbehind: true,
|
||||
inside: {
|
||||
'cl cl-em cl-start': /^[_\*]/,
|
||||
'cl cl-em cl-close': /[_\*]$/
|
||||
}
|
||||
}
|
||||
if (options.dels) {
|
||||
rest.del = {
|
||||
pattern: /(^|[^\w*])(~~)[\s\S]*?\2(?=([^\w*]|$))/gm,
|
||||
lookbehind: true,
|
||||
inside: {
|
||||
'cl': /~~/,
|
||||
'cl-del-text': /[^~]+/
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.subs) {
|
||||
rest.sub = {
|
||||
pattern: /(~)(?=\S)(.*?\S)\1/gm,
|
||||
inside: {
|
||||
'cl': /~/
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.sups) {
|
||||
rest.sup = {
|
||||
pattern: /(\^)(?=\S)(.*?\S)\1/gm,
|
||||
inside: {
|
||||
'cl': /\^/
|
||||
}
|
||||
}
|
||||
}
|
||||
rest.entity = markup.entity
|
||||
|
||||
for (var c = 6; c >= 1; c--) {
|
||||
grammar['h' + c].inside.rest = rest
|
||||
}
|
||||
grammar['h1 alt'].inside.rest = rest
|
||||
grammar['h2 alt'].inside.rest = rest
|
||||
if (options.tables) {
|
||||
grammar.table.inside.rest = rest
|
||||
grammar['table alt'].inside.rest = rest
|
||||
}
|
||||
grammar.p.inside.rest = rest
|
||||
grammar.blockquote.inside.rest = rest
|
||||
grammar.li.inside.rest = rest
|
||||
if (options.footnotes) {
|
||||
grammar.fndef.inside.rest = rest
|
||||
}
|
||||
if (options.deflists) {
|
||||
grammar.deflist.inside['deflist-desc'].inside.rest = rest
|
||||
}
|
||||
|
||||
var restLight = {
|
||||
code: rest.code,
|
||||
inlinefn: rest.inlinefn,
|
||||
fn: rest.fn,
|
||||
link: rest.link,
|
||||
linkref: rest.linkref
|
||||
}
|
||||
rest.strong.inside.rest = restLight
|
||||
rest.em.inside.rest = restLight
|
||||
if (options.dels) {
|
||||
rest.del.inside.rest = restLight
|
||||
}
|
||||
|
||||
var inside = {
|
||||
code: rest.code,
|
||||
comment: rest.comment,
|
||||
tag: rest.tag,
|
||||
strong: rest.strong,
|
||||
em: rest.em,
|
||||
del: rest.del,
|
||||
sub: rest.sub,
|
||||
sup: rest.sup,
|
||||
entity: markup.entity
|
||||
}
|
||||
rest.link.inside['cl cl-underlined-text'].inside = inside
|
||||
rest.linkref.inside['cl cl-underlined-text'].inside = inside
|
||||
|
||||
return grammar
|
||||
}
|
1362
src/cledit/pagedown.js
Normal file
1362
src/cledit/pagedown.js
Normal file
File diff suppressed because it is too large
Load Diff
48
src/components/App.vue
Normal file
48
src/components/App.vue
Normal file
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<layout></layout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Layout from './Layout';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
@import 'common/markdownHighlighting';
|
||||
@import 'common/prism';
|
||||
@import 'common/flex';
|
||||
@import 'common/base';
|
||||
|
||||
body {
|
||||
background-color: #f3f3f3;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
tab-size: 4;
|
||||
text-rendering: auto;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
* {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
</style>
|
88
src/components/ButtonBar.vue
Normal file
88
src/components/ButtonBar.vue
Normal file
|
@ -0,0 +1,88 @@
|
|||
<template>
|
||||
<div class="button-bar">
|
||||
<div class="button-bar__inner button-bar__inner--left" @click="toggleEditor(false)">
|
||||
<div class="button-bar__button">
|
||||
<icon-eye></icon-eye>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-bar__inner button-bar__inner--top">
|
||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': showNavigationBar }" @click="toggleNavigationBar()">
|
||||
<icon-navigation-bar></icon-navigation-bar>
|
||||
</div>
|
||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': showSidePreview }" @click="toggleSidePreview()">
|
||||
<icon-side-preview></icon-side-preview>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-bar__inner button-bar__inner--bottom">
|
||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': showStatusBar }" @click="toggleStatusBar()">
|
||||
<icon-status-bar></icon-status-bar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: mapState('layout', [
|
||||
'showNavigationBar',
|
||||
'showEditor',
|
||||
'showSidePreview',
|
||||
'showSideBar',
|
||||
'showStatusBar',
|
||||
]),
|
||||
methods: mapActions('layout', [
|
||||
'toggleNavigationBar',
|
||||
'toggleEditor',
|
||||
'toggleSidePreview',
|
||||
'toggleSideBar',
|
||||
'toggleStatusBar',
|
||||
]),
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.button-bar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.button-bar__inner {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.button-bar__inner--bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.button-bar__button {
|
||||
cursor: pointer;
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
margin: 3px 0;
|
||||
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.button-bar__button--on {
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.button-bar__inner--left {
|
||||
left: -52px;
|
||||
z-index: 1;
|
||||
|
||||
.button-bar__button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
64
src/components/Editor.vue
Normal file
64
src/components/Editor.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div class="editor">
|
||||
<pre class="editor__inner markdown-highlighting" :style="{ 'padding-left': editorPadding + 'px', 'padding-right': editorPadding + 'px' }"></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: mapState('layout', [
|
||||
'editorPadding',
|
||||
]),
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
|
||||
.editor {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.editor__inner {
|
||||
font-family: $font-family-editor;
|
||||
font-variant-ligatures: no-common-ligatures;
|
||||
margin: 0;
|
||||
padding: 10px 20px 360px 110px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
|
||||
* {
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.cledit-section {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.discussion-highlight,
|
||||
.find-replace-highlight {
|
||||
background-color: transparentize(#ffe400, 0.5);
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.monospaced {
|
||||
font-family: $font-family-monospace !important;
|
||||
font-size: $font-size-monospace !important;
|
||||
|
||||
* {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
165
src/components/Layout.vue
Normal file
165
src/components/Layout.vue
Normal file
|
@ -0,0 +1,165 @@
|
|||
<template>
|
||||
<div class="layout">
|
||||
<div class="layout__panel layout__panel--inner-1" :style="{ top: inner1Y + 'px', height: inner1Height + 'px' }">
|
||||
<div class="layout__panel layout__panel--inner-2" :style="{ height: inner2Height + 'px' }">
|
||||
<div class="layout__panel layout__panel--inner-3" :style="{ left: inner3X + 'px', width: inner3Width + 'px' }">
|
||||
<div class="layout__panel layout__panel--button-bar">
|
||||
<button-bar></button-bar>
|
||||
</div>
|
||||
<div class="layout__panel layout__panel--preview" v-show="showSidePreview || !showEditor" :style="{ width: previewWidth + 'px', 'font-size': fontSize + 'px' }">
|
||||
<preview></preview>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout__panel layout__panel--editor" v-show="showEditor" :style="{ width: editorWidth + 'px', 'font-size': fontSize + 'px' }">
|
||||
<editor></editor>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout__panel layout__panel--status-bar" :style="{ top: statusBarY + 'px' }">
|
||||
<status-bar></status-bar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout__panel layout__panel--navigation-bar" :style="{ top: navigationBarY + 'px' }">
|
||||
<navigation-bar></navigation-bar>
|
||||
</div>
|
||||
<div class="layout__panel layout__panel--side-bar" :style="{ left: sideBarX + 'px' }">
|
||||
<side-bar></side-bar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import NavigationBar from './NavigationBar';
|
||||
import SideBar from './SideBar';
|
||||
import ButtonBar from './ButtonBar';
|
||||
import StatusBar from './StatusBar';
|
||||
import Editor from './Editor';
|
||||
import Preview from './Preview';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import constants from '../services/constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NavigationBar,
|
||||
SideBar,
|
||||
ButtonBar,
|
||||
StatusBar,
|
||||
Editor,
|
||||
Preview,
|
||||
},
|
||||
computed: mapState('layout', {
|
||||
showEditor: 'showEditor',
|
||||
showSidePreview: 'showSidePreview',
|
||||
fontSize: 'fontSize',
|
||||
inner1Y: 'inner1Y',
|
||||
inner1Height: 'inner1Height',
|
||||
inner2Height: 'inner2Height',
|
||||
inner3X: 'inner3X',
|
||||
inner3Width: 'inner3Width',
|
||||
navigationBarY: 'navigationBarY',
|
||||
sideBarX: 'sideBarX',
|
||||
statusBarY: 'statusBarY',
|
||||
previewWidth: 'previewWidth',
|
||||
editorWidth: 'editorWidth',
|
||||
}),
|
||||
methods: {
|
||||
...mapActions('layout', [
|
||||
'updateStyle',
|
||||
]),
|
||||
saveSelection: () => editorSvc.saveSelection(true),
|
||||
},
|
||||
created() {
|
||||
this.updateStyle();
|
||||
window.addEventListener('resize', this.updateStyle);
|
||||
window.addEventListener('keyup', this.saveSelection);
|
||||
window.addEventListener('mouseup', this.saveSelection);
|
||||
window.addEventListener('contextmenu', this.saveSelection);
|
||||
},
|
||||
mounted() {
|
||||
const editorElt = this.$el.querySelector('.editor__inner');
|
||||
const previewElt = this.$el.querySelector('.preview__inner');
|
||||
const tocElt = this.$el.querySelector('.toc__inner');
|
||||
editorSvc.init(editorElt, previewElt, tocElt);
|
||||
|
||||
// TOC click behaviour
|
||||
let isMousedown;
|
||||
function onClick(e) {
|
||||
if (!isMousedown) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
const y = e.clientY - tocElt.getBoundingClientRect().top;
|
||||
|
||||
this.$store.state.sectionDescList.some((sectionDesc) => {
|
||||
if (y >= sectionDesc.tocDimension.endOffset) {
|
||||
return false;
|
||||
}
|
||||
const posInSection = (y - sectionDesc.tocDimension.startOffset)
|
||||
/ (sectionDesc.tocDimension.height || 1);
|
||||
const editorScrollTop = sectionDesc.editorDimension.startOffset
|
||||
+ (sectionDesc.editorDimension.height * posInSection);
|
||||
editorElt.parentNode.scrollTop = editorScrollTop - constants.scrollOffset;
|
||||
const previewScrollTop = sectionDesc.previewDimension.startOffset
|
||||
+ (sectionDesc.previewDimension.height * posInSection);
|
||||
previewElt.parentNode.scrollTop = previewScrollTop - constants.scrollOffset;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
tocElt.addEventListener('mouseup', () => {
|
||||
isMousedown = false;
|
||||
});
|
||||
tocElt.addEventListener('mouseleave', () => {
|
||||
isMousedown = false;
|
||||
});
|
||||
tocElt.addEventListener('mousedown', (e) => {
|
||||
isMousedown = e.which === 1;
|
||||
onClick(e);
|
||||
});
|
||||
tocElt.addEventListener('mousemove', (e) => {
|
||||
onClick(e);
|
||||
});
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.updateStyle);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.layout__panel {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.layout__panel--inner-1 {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.layout__panel--button-bar {
|
||||
/* buttonBarWidth */
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.layout__panel--preview {
|
||||
/* buttonBarWidth */
|
||||
left: 30px;
|
||||
}
|
||||
|
||||
.layout__panel--status-bar {
|
||||
/* statusBarHeight */
|
||||
height: 20px;
|
||||
background-color: #007acc;
|
||||
}
|
||||
|
||||
.layout__panel--side-bar {
|
||||
/* sideBarWidth */
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.layout__panel--navigation-bar {
|
||||
/* navigationBarHeight */
|
||||
height: 44px;
|
||||
}
|
||||
</style>
|
76
src/components/NavigationBar.vue
Normal file
76
src/components/NavigationBar.vue
Normal file
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<div class="navigation-bar">
|
||||
<div class="navigation-bar__button" v-on:click="pagedownClick('bold')">
|
||||
<icon-format-bold></icon-format-bold>
|
||||
</div>
|
||||
<div class="navigation-bar__button" v-on:click="pagedownClick('italic')">
|
||||
<icon-format-italic></icon-format-italic>
|
||||
</div>
|
||||
<div class="navigation-bar__button" v-on:click="pagedownClick('strikethrough')">
|
||||
<icon-format-strikethrough></icon-format-strikethrough>
|
||||
</div>
|
||||
<div class="navigation-bar__button" v-on:click="pagedownClick('heading')">
|
||||
<icon-format-size></icon-format-size>
|
||||
</div>
|
||||
<div class="navigation-bar__button" v-on:click="pagedownClick('hr')">
|
||||
<icon-format-horizontal-rule></icon-format-horizontal-rule>
|
||||
</div>
|
||||
<div class="navigation-bar__button" v-on:click="pagedownClick('ulist')">
|
||||
<icon-format-list-bulleted></icon-format-list-bulleted>
|
||||
</div>
|
||||
<div class="navigation-bar__button" v-on:click="pagedownClick('olist')">
|
||||
<icon-format-list-numbers></icon-format-list-numbers>
|
||||
</div>
|
||||
<div class="navigation-bar__button" v-on:click="pagedownClick('table')">
|
||||
<icon-table></icon-table>
|
||||
</div>
|
||||
<div class="navigation-bar__button" v-on:click="pagedownClick('quote')">
|
||||
<icon-format-quote-close></icon-format-quote-close>
|
||||
</div>
|
||||
<div class="navigation-bar__button" v-on:click="pagedownClick('code')">
|
||||
<icon-code-braces></icon-code-braces>
|
||||
</div>
|
||||
<div class="navigation-bar__button" v-on:click="pagedownClick('link')">
|
||||
<icon-link-variant></icon-link-variant>
|
||||
</div>
|
||||
<div class="navigation-bar__button" v-on:click="pagedownClick('image')">
|
||||
<icon-file-image></icon-file-image>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import editorSvc from '../services/editorSvc';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
pagedownClick(name) {
|
||||
editorSvc.pagedownEditor.uiManager.doClick(name);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.navigation-bar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #2c2c2c;
|
||||
padding: 5px 15px 0;
|
||||
}
|
||||
|
||||
.navigation-bar__button {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.67);
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 4px;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
48
src/components/Preview.vue
Normal file
48
src/components/Preview.vue
Normal file
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<div class="preview">
|
||||
<div class="preview__inner" :style="{ 'padding-left': previewPadding + 'px', 'padding-right': previewPadding + 'px' }">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
const appUri = `${window.location.protocol}//${window.location.host}`;
|
||||
|
||||
export default {
|
||||
computed: mapState('layout', [
|
||||
'previewPadding',
|
||||
]),
|
||||
mounted() {
|
||||
this.$el.addEventListener('click', (evt) => {
|
||||
let elt = evt.target;
|
||||
while (elt !== this.$el) {
|
||||
if (elt.href && elt.href.match(/^https?:\/\//)
|
||||
&& (!elt.hash || elt.href.slice(0, appUri.length) !== appUri)) {
|
||||
evt.preventDefault();
|
||||
const wnd = window.open(elt.href, '_blank');
|
||||
wnd.focus();
|
||||
return;
|
||||
}
|
||||
elt = elt.parentNode;
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.preview {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.preview__inner {
|
||||
margin: 0;
|
||||
padding: 0 1035px 360px;
|
||||
}
|
||||
</style>
|
17
src/components/SideBar.vue
Normal file
17
src/components/SideBar.vue
Normal file
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<div class="side-bar">
|
||||
<toc>
|
||||
</toc>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import Toc from './Toc';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Toc,
|
||||
},
|
||||
};
|
||||
</script>
|
122
src/components/StatusBar.vue
Normal file
122
src/components/StatusBar.vue
Normal file
|
@ -0,0 +1,122 @@
|
|||
<template>
|
||||
<div class="stat-panel panel no-overflow">
|
||||
<div class="stat-panel__block stat-panel__block--left">
|
||||
<span class="stat-panel__block-name">
|
||||
Text
|
||||
<small v-show="textSelection">(selection)</small>
|
||||
</span>
|
||||
<span v-for="stat in textStats" :key="stat.id">
|
||||
<span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat-panel__block stat-panel__block--right">
|
||||
<span class="stat-panel__block-name">
|
||||
HTML
|
||||
<small v-show="htmlSelection">(selection)</small>
|
||||
</span>
|
||||
<span v-for="stat in htmlStats" :key="stat.id">
|
||||
<span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import editorEngineSvc from '../services/editorEngineSvc';
|
||||
import utils from '../services/utils';
|
||||
|
||||
class Stat {
|
||||
constructor(name, regex) {
|
||||
this.id = utils.uid();
|
||||
this.name = name;
|
||||
this.regex = new RegExp(regex, 'gm');
|
||||
this.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
textSelection: false,
|
||||
htmlSelection: false,
|
||||
textStats: [
|
||||
new Stat('bytes', '[\\s\\S]'),
|
||||
new Stat('words', '\\S+'),
|
||||
new Stat('lines', '\n'),
|
||||
],
|
||||
htmlStats: [
|
||||
new Stat('characters', '\\S'),
|
||||
new Stat('words', '\\S+'),
|
||||
new Stat('paragraphs', '\\S.*'),
|
||||
],
|
||||
}),
|
||||
created() {
|
||||
editorSvc.$on('sectionList', () => this.computeText());
|
||||
editorSvc.$on('selectionRange', () => this.computeText());
|
||||
editorSvc.$on('previewText', () => this.computeHtml());
|
||||
editorSvc.$on('previewSelectionRange', () => this.computeHtml());
|
||||
},
|
||||
|
||||
methods: {
|
||||
computeText() {
|
||||
this.textSelection = false;
|
||||
let text = editorEngineSvc.clEditor.getContent();
|
||||
const selectedText = editorEngineSvc.clEditor.selectionMgr.getSelectedText();
|
||||
if (selectedText) {
|
||||
this.textSelection = true;
|
||||
text = selectedText;
|
||||
}
|
||||
this.textStats.forEach((stat) => {
|
||||
stat.value = (text.match(stat.regex) || []).length;
|
||||
});
|
||||
},
|
||||
computeHtml() {
|
||||
let text;
|
||||
if (editorSvc.previewSelectionRange) {
|
||||
text = editorSvc.previewSelectionRange.toString();
|
||||
}
|
||||
this.htmlSelection = true;
|
||||
if (!text) {
|
||||
this.htmlSelection = false;
|
||||
text = editorSvc.previewText;
|
||||
}
|
||||
if (text !== undefined) {
|
||||
this.htmlStats.cl_each((stat) => {
|
||||
stat.value = (text.match(stat.regex) || []).length;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.stat-panel {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.stat-panel__block {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.stat-panel__block--left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.stat-panel__block--right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.stat-panel__block-name {
|
||||
font-weight: 500;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.stat-panel__value {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
6
src/components/Toc.vue
Normal file
6
src/components/Toc.vue
Normal file
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<div class="toc">
|
||||
<div class="toc__inner">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
145
src/components/common/base.scss
Normal file
145
src/components/common/base.scss
Normal file
|
@ -0,0 +1,145 @@
|
|||
@import '../../node_modules/normalize-scss/sass/normalize';
|
||||
|
||||
@include normalize();
|
||||
|
||||
html,
|
||||
body {
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
font-family: $font-family-editor;
|
||||
font-variant-ligatures: common-ligatures;
|
||||
line-height: 1.65;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
blockquote,
|
||||
pre,
|
||||
ul,
|
||||
ol,
|
||||
dl {
|
||||
margin: 1.2em 0;
|
||||
}
|
||||
|
||||
ol ul,
|
||||
ul ol,
|
||||
ul ul,
|
||||
ol ol {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $link-color;
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: $font-family-monospace;
|
||||
font-size: $font-size-monospace;
|
||||
|
||||
* {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: $code-bg;
|
||||
border-radius: $border-radius-base;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid rgba(128, 128, 128, 0.15);
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
background-color: $code-bg;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
.toc ul {
|
||||
list-style-type: none;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
background-color: transparent;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-right: 1px solid #e0e0e0;
|
||||
padding: 8px 12px;
|
||||
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
kbd {
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(63, 63, 63, 0.25);
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 0 rgba(63, 63, 63, 0.25);
|
||||
color: #333;
|
||||
display: inline-block;
|
||||
font-size: 0.7em;
|
||||
margin: 0 0.1em;
|
||||
padding: 0.1em 0.6em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
abbr {
|
||||
&[title] {
|
||||
border-bottom: 1px dotted #777;
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
|
||||
.footnote {
|
||||
font-size: 0.8em;
|
||||
position: relative;
|
||||
top: -0.25em;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.export-container {
|
||||
margin-bottom: 180px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
> :not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):not([align]) {
|
||||
text-align: justify;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.export-container {
|
||||
width: 750px;
|
||||
}
|
||||
}
|
19
src/components/common/flex.scss
Normal file
19
src/components/common/flex.scss
Normal file
|
@ -0,0 +1,19 @@
|
|||
.flex {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -moz-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex--row {
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex--column {
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
232
src/components/common/markdownHighlighting.scss
Normal file
232
src/components/common/markdownHighlighting.scss
Normal file
|
@ -0,0 +1,232 @@
|
|||
@import './variables';
|
||||
|
||||
.markdown-highlighting {
|
||||
color: $editor-color;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
-webkit-font-smoothing: auto;
|
||||
-moz-osx-font-smoothing: auto;
|
||||
font-weight: $editor-font-weight-base;
|
||||
|
||||
.code {
|
||||
font-family: $font-family-monospace;
|
||||
font-size: $font-size-monospace;
|
||||
|
||||
* {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.pre {
|
||||
color: $editor-color;
|
||||
font-family: $font-family-monospace;
|
||||
font-size: $font-size-monospace;
|
||||
word-break: break-all;
|
||||
|
||||
[class*='language-'] {
|
||||
color: $editor-color-dark;
|
||||
}
|
||||
|
||||
* {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tag {
|
||||
color: $editor-color;
|
||||
font-family: $font-family-monospace;
|
||||
font-size: $font-size-monospace;
|
||||
font-weight: $editor-font-weight-bold;
|
||||
|
||||
.punctuation,
|
||||
.attr-value,
|
||||
.attr-name {
|
||||
font-weight: $editor-font-weight-base;
|
||||
}
|
||||
|
||||
* {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.latex,
|
||||
.math {
|
||||
color: $editor-color;
|
||||
}
|
||||
|
||||
.entity {
|
||||
color: $editor-color;
|
||||
font-family: $font-family-monospace;
|
||||
font-size: $font-size-monospace;
|
||||
font-style: italic;
|
||||
|
||||
* {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
font-family: $font-family-monospace;
|
||||
font-size: $font-size-monospace;
|
||||
|
||||
* {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.comment {
|
||||
color: $editor-color-light;
|
||||
}
|
||||
|
||||
.keyword {
|
||||
color: $editor-color-dark;
|
||||
font-weight: $editor-font-weight-bold;
|
||||
}
|
||||
|
||||
.code,
|
||||
.img,
|
||||
.img-wrapper,
|
||||
.imgref,
|
||||
.cl-toc {
|
||||
background-color: $code-bg;
|
||||
border-radius: $code-border-radius;
|
||||
padding: 0.15em 0;
|
||||
}
|
||||
|
||||
.img-wrapper {
|
||||
display: inline-block;
|
||||
|
||||
.img {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
padding: 0 0.15em;
|
||||
}
|
||||
}
|
||||
|
||||
.cl-toc {
|
||||
font-size: 2.8em;
|
||||
padding: 0.15em;
|
||||
}
|
||||
|
||||
.blockquote {
|
||||
color: rgba(0, 0, 0, 0.48);
|
||||
}
|
||||
|
||||
.h1,
|
||||
.h2,
|
||||
.h3,
|
||||
.h4,
|
||||
.h5,
|
||||
.h6 {
|
||||
font-weight: $editor-font-weight-bold;
|
||||
}
|
||||
|
||||
.h1,
|
||||
.h11 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.h2,
|
||||
.h22 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.h3 {
|
||||
font-size: 1.17em;
|
||||
}
|
||||
|
||||
.h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.h5 {
|
||||
font-size: 0.83em;
|
||||
}
|
||||
|
||||
.h6 {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.cl-hash {
|
||||
color: $editor-color-light;
|
||||
}
|
||||
|
||||
.cl,
|
||||
.hr {
|
||||
color: $editor-color-light;
|
||||
font-style: normal;
|
||||
font-weight: $editor-font-weight-base;
|
||||
}
|
||||
|
||||
.em,
|
||||
.em .cl {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.strong,
|
||||
.strong .cl,
|
||||
.term {
|
||||
font-weight: $editor-font-weight-bold;
|
||||
}
|
||||
|
||||
.cl-del-text {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.url,
|
||||
.email,
|
||||
.cl-underlined-text {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.linkdef .url {
|
||||
color: $editor-color-light;
|
||||
}
|
||||
|
||||
.fn,
|
||||
.inlinefn,
|
||||
.sup {
|
||||
font-size: smaller;
|
||||
position: relative;
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
.sub {
|
||||
bottom: -0.25em;
|
||||
font-size: smaller;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.img,
|
||||
.imgref,
|
||||
.link,
|
||||
.linkref {
|
||||
color: $editor-color-light;
|
||||
|
||||
.cl-underlined-text {
|
||||
color: $editor-color-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.cl-title {
|
||||
color: $editor-color;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-highlighting--inline {
|
||||
.h1,
|
||||
.h11,
|
||||
.h2,
|
||||
.h22,
|
||||
.h3,
|
||||
.h4,
|
||||
.h5,
|
||||
.h6,
|
||||
.cl-toc {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
73
src/components/common/prism.scss
Normal file
73
src/components/common/prism.scss
Normal file
|
@ -0,0 +1,73 @@
|
|||
.token.pre.gfm,
|
||||
.prism {
|
||||
* {
|
||||
font-weight: inherit !important;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #708090;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #905;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #690;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #a67f59;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #07a;
|
||||
}
|
||||
|
||||
.token.function {
|
||||
color: #dd4a68;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
13
src/components/common/variables.scss
Normal file
13
src/components/common/variables.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
$font-family-editor: "Helvetica Neue", Helvetica, sans-serif;
|
||||
$font-family-monospace: "Lucida Sans Typewriter", "Lucida Console", monaco, Courrier, monospace;
|
||||
$font-size-monospace: 0.9em;
|
||||
$code-bg: rgba(0, 0, 0, 0.05);
|
||||
$code-border-radius: 3px;
|
||||
$link-color: #0366d6;
|
||||
$border-radius-base: 3px;
|
||||
|
||||
$editor-color: rgba(0, 0, 0, 0.8);
|
||||
$editor-color-light: rgba(0, 0, 0, 0.28);
|
||||
$editor-color-dark: #000;
|
||||
$editor-font-weight-base: 400;
|
||||
$editor-font-weight-bold: 600;
|
1
src/extensions/index.js
Normal file
1
src/extensions/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
import './markdownExt';
|
197
src/extensions/markdownExt.js
Normal file
197
src/extensions/markdownExt.js
Normal file
|
@ -0,0 +1,197 @@
|
|||
import Prism from 'prismjs';
|
||||
import markdownitAbbr from 'markdown-it-abbr';
|
||||
import markdownitDeflist from 'markdown-it-deflist';
|
||||
import markdownitFootnote from 'markdown-it-footnote';
|
||||
import markdownitSub from 'markdown-it-sub';
|
||||
import markdownitSup from 'markdown-it-sup';
|
||||
import extensionSvc from '../services/extensionSvc';
|
||||
|
||||
const coreBaseRules = [
|
||||
'normalize',
|
||||
'block',
|
||||
'inline',
|
||||
'linkify',
|
||||
'replacements',
|
||||
'smartquotes',
|
||||
];
|
||||
const blockBaseRules = [
|
||||
'code',
|
||||
'fence',
|
||||
'blockquote',
|
||||
'hr',
|
||||
'list',
|
||||
'reference',
|
||||
'heading',
|
||||
'lheading',
|
||||
'html_block',
|
||||
'table',
|
||||
'paragraph',
|
||||
];
|
||||
const inlineBaseRules = [
|
||||
'text',
|
||||
'newline',
|
||||
'escape',
|
||||
'backticks',
|
||||
'strikethrough',
|
||||
'emphasis',
|
||||
'link',
|
||||
'image',
|
||||
'autolink',
|
||||
'html_inline',
|
||||
'entity',
|
||||
];
|
||||
const inlineBaseRules2 = [
|
||||
'balance_pairs',
|
||||
'strikethrough',
|
||||
'emphasis',
|
||||
'text_collapse',
|
||||
];
|
||||
|
||||
extensionSvc.onGetOptions((options, properties) => {
|
||||
options.abbr = properties['ext:markdown:abbr'] !== 'false';
|
||||
options.breaks = properties['ext:markdown:breaks'] === 'true';
|
||||
options.deflist = properties['ext:markdown:deflist'] !== 'false';
|
||||
options.del = properties['ext:markdown:del'] !== 'false';
|
||||
options.fence = properties['ext:markdown:fence'] !== 'false';
|
||||
options.footnote = properties['ext:markdown:footnote'] !== 'false';
|
||||
options.linkify = properties['ext:markdown:linkify'] !== 'false';
|
||||
options.sub = properties['ext:markdown:sub'] !== 'false';
|
||||
options.sup = properties['ext:markdown:sup'] !== 'false';
|
||||
options.table = properties['ext:markdown:table'] !== 'false';
|
||||
options.typographer = properties['ext:markdown:typographer'] !== 'false';
|
||||
});
|
||||
|
||||
extensionSvc.onInitConverter(0, (markdown, options) => {
|
||||
markdown.set({
|
||||
html: true,
|
||||
breaks: !!options.breaks,
|
||||
linkify: !!options.linkify,
|
||||
typographer: !!options.typographer,
|
||||
langPrefix: 'prism language-',
|
||||
});
|
||||
|
||||
markdown.core.ruler.enable(coreBaseRules);
|
||||
|
||||
const blockRules = blockBaseRules.slice();
|
||||
if (!options.fence) {
|
||||
blockRules.splice(blockRules.indexOf('fence'), 1);
|
||||
}
|
||||
if (!options.table) {
|
||||
blockRules.splice(blockRules.indexOf('table'), 1);
|
||||
}
|
||||
markdown.block.ruler.enable(blockRules);
|
||||
|
||||
const inlineRules = inlineBaseRules.slice();
|
||||
const inlineRules2 = inlineBaseRules2.slice();
|
||||
if (!options.del) {
|
||||
inlineRules.splice(blockRules.indexOf('strikethrough'), 1);
|
||||
inlineRules2.splice(blockRules.indexOf('strikethrough'), 1);
|
||||
}
|
||||
markdown.inline.ruler.enable(inlineRules);
|
||||
markdown.inline.ruler2.enable(inlineRules2);
|
||||
|
||||
if (options.abbr) {
|
||||
markdown.use(markdownitAbbr);
|
||||
}
|
||||
if (options.deflist) {
|
||||
markdown.use(markdownitDeflist);
|
||||
}
|
||||
if (options.footnote) {
|
||||
markdown.use(markdownitFootnote);
|
||||
}
|
||||
if (options.sub) {
|
||||
markdown.use(markdownitSub);
|
||||
}
|
||||
if (options.sup) {
|
||||
markdown.use(markdownitSup);
|
||||
}
|
||||
|
||||
markdown.core.ruler.before('replacements', 'anchors', (state) => {
|
||||
const anchorHash = {};
|
||||
let headingOpenToken;
|
||||
let headingContent;
|
||||
state.tokens.cl_each((token) => {
|
||||
if (token.type === 'heading_open') {
|
||||
headingContent = '';
|
||||
headingOpenToken = token;
|
||||
} else if (token.type === 'heading_close') {
|
||||
headingOpenToken.headingContent = headingContent;
|
||||
|
||||
// Slugify according to http://pandoc.org/README.html#extension-auto_identifiers
|
||||
let slug = headingContent
|
||||
.replace(/\s/g, '-') // Replace all spaces and newlines with hyphens
|
||||
.replace(/[\0-,/:-@[-^`{-~]/g, '') // Remove all punctuation, except underscores, hyphens, and periods
|
||||
.toLowerCase(); // Convert all alphabetic characters to lowercase
|
||||
|
||||
// Remove everything up to the first letter
|
||||
let i;
|
||||
for (i = 0; i < slug.length; i += 1) {
|
||||
const charCode = slug.charCodeAt(i);
|
||||
if ((charCode >= 0x61 && charCode <= 0x7A) || charCode > 0x7E) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing is left after this, use the identifier `section`
|
||||
slug = slug.slice(i) || 'section';
|
||||
|
||||
let anchor = slug;
|
||||
let index = 1;
|
||||
while (Object.prototype.hasOwnProperty.call(anchorHash, anchor)) {
|
||||
anchor = `${slug}-${index}`;
|
||||
index += 1;
|
||||
}
|
||||
anchorHash[anchor] = true;
|
||||
headingOpenToken.headingAnchor = anchor;
|
||||
headingOpenToken.attrs = [
|
||||
['id', anchor],
|
||||
];
|
||||
headingOpenToken = undefined;
|
||||
} else if (headingOpenToken) {
|
||||
headingContent += token.children.cl_reduce((result, child) => {
|
||||
if (child.type !== 'footnote_ref') {
|
||||
return result + child.content;
|
||||
}
|
||||
return result;
|
||||
}, '');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Wrap tables into a div for scrolling
|
||||
markdown.renderer.rules.table_open = (tokens, idx, opts) =>
|
||||
`<div class="table-wrapper">${markdown.renderer.renderToken(tokens, idx, opts)}`;
|
||||
markdown.renderer.rules.table_close = (tokens, idx, opts) =>
|
||||
`${markdown.renderer.renderToken(tokens, idx, opts)}</div>`;
|
||||
|
||||
// Transform style into align attribute to pass the HTML sanitizer
|
||||
const textAlignLength = 'text-align:'.length;
|
||||
markdown.renderer.rules.td_open = (tokens, idx, opts) => {
|
||||
const token = tokens[idx];
|
||||
if (token.attrs && token.attrs.length && token.attrs[0][0] === 'style') {
|
||||
token.attrs = [
|
||||
['align', token.attrs[0][1].slice(textAlignLength)],
|
||||
];
|
||||
}
|
||||
return markdown.renderer.renderToken(tokens, idx, opts);
|
||||
};
|
||||
markdown.renderer.rules.th_open = markdown.renderer.rules.td_open;
|
||||
|
||||
markdown.renderer.rules.footnote_ref = (tokens, idx) => {
|
||||
const n = Number(tokens[idx].meta.id + 1).toString();
|
||||
let id = `fnref${n}`;
|
||||
if (tokens[idx].meta.subId > 0) {
|
||||
id += `:${tokens[idx].meta.subId}`;
|
||||
}
|
||||
return `<sup class="footnote-ref"><a href="#fn${n}" id="${id}">${n}</a></sup>`;
|
||||
};
|
||||
});
|
||||
|
||||
extensionSvc.onSectionPreview((elt) => {
|
||||
elt.querySelectorAll('.prism').cl_each((prismElt) => {
|
||||
if (!prismElt.highlighted) {
|
||||
Prism.highlightElement(prismElt);
|
||||
}
|
||||
prismElt.highlighted = true;
|
||||
});
|
||||
});
|
5
src/icons/CodeBraces.vue
Normal file
5
src/icons/CodeBraces.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-linejoin="round" d="M 8,3C 6.89543,3 6,3.89539 6,5L 6,9C 6,10.1046 5.10457,11 4,11L 3,11L 3,13L 4,13C 5.10457,13 6,13.8954 6,15L 6,19C 6,20.1046 6.92841,20.7321 8,21L 10,21L 10,19L 8,19L 8,14C 8,12.8954 7.10457,12 6,12C 7.10457,12 8,11.1046 8,10L 8,5L 10,5L 10,3M 16,3C 17.1046,3 18,3.89539 18,5L 18,9C 18,10.1046 18.8954,11 20,11L 21,11L 21,13L 20,13C 18.8954,13 18,13.8954 18,15L 18,19C 18,20.1046 17.0716,20.7321 16,21L 14,21L 14,19L 16,19L 16,14C 16,12.8954 16.8954,12 18,12C 16.8954,12 16,11.1046 16,10L 16,5L 14,5L 14,3L 16,3 Z " />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/Eye.vue
Normal file
5
src/icons/Eye.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-linejoin="round" d="M 11.9994,8.99813C 10.3424,8.99813 8.99941,10.3411 8.99941,11.9981C 8.99941,13.6551 10.3424,14.9981 11.9994,14.9981C 13.6564,14.9981 14.9994,13.6551 14.9994,11.9981C 14.9994,10.3411 13.6564,8.99813 11.9994,8.99813 Z M 11.9994,16.9981C 9.23841,16.9981 6.99941,14.7591 6.99941,11.9981C 6.99941,9.23714 9.23841,6.99813 11.9994,6.99813C 14.7604,6.99813 16.9994,9.23714 16.9994,11.9981C 16.9994,14.7591 14.7604,16.9981 11.9994,16.9981 Z M 11.9994,4.49813C 6.99741,4.49813 2.72741,7.60915 0.99941,11.9981C 2.72741,16.3871 6.99741,19.4981 11.9994,19.4981C 17.0024,19.4981 21.2714,16.3871 22.9994,11.9981C 21.2714,7.60915 17.0024,4.49813 11.9994,4.49813 Z " />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/FileImage.vue
Normal file
5
src/icons/FileImage.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 12.9994,8.99807L 18.4994,8.99807L 12.9994,3.49807L 12.9994,8.99807 Z M 5.99938,1.99809L 13.9994,1.99809L 19.9994,7.99808L 19.9994,19.9981C 19.9994,21.1021 19.1034,21.9981 17.9994,21.9981L 5.98937,21.9981C 4.88537,21.9981 3.99939,21.1021 3.99939,19.9981L 4.0094,3.99808C 4.0094,2.89407 4.89437,1.99809 5.99938,1.99809 Z M 6,20L 15,20L 18,20L 18,12L 14,16L 12,14L 6,20 Z M 8,9C 6.89543,9 6,9.89543 6,11C 6,12.1046 6.89543,13 8,13C 9.10457,13 10,12.1046 10,11C 10,9.89543 9.10457,9 8,9 Z " />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/FormatBold.vue
Normal file
5
src/icons/FormatBold.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M13.125,16.5l-3.5,0l0,-3l3.5,0c0.828,0 1.5,0.672 1.5,1.5c0,0.828 -0.672,1.5 -1.5,1.5Zm-3.5,-9l3,0c0.828,0 1.5,0.672 1.5,1.5c0,0.828 -0.672,1.5 -1.5,1.5l-3,0m5.6,1.288c0.966,-0.674 1.65,-1.767 1.65,-2.788c0,-2.255 -1.745,-4 -4,-4l-6.25,0l0,14l7.042,0c2.094,0 3.708,-1.698 3.708,-3.792c0,-1.516 -0.863,-2.814 -2.15,-3.42Z" />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/FormatHorizontalRule.vue
Normal file
5
src/icons/FormatHorizontalRule.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-linejoin="round" d="M3,13l5,0l0,-2l-5,0l0,2Zm6.5,0l5,0l0,-2l-5,0l0,2Zm6.5,0l5,0l0,-2l-5,0l0,2Z" />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/FormatItalic.vue
Normal file
5
src/icons/FormatItalic.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-linejoin="round" d="M10,5l0,3l2.214,0l-3.428,8l-2.786,0l0,3l8,0l0,-3l-2.214,0l3.428,-8l2.786,0l0,-3l-8,0Z" />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/FormatListBulleted.vue
Normal file
5
src/icons/FormatListBulleted.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 7,5L 21,5L 21,7L 7,7L 7,5 Z M 7,13L 7,11L 21,11L 21,13L 7,13 Z M 4,4.50001C 4.83,4.50001 5.5,5.16993 5.5,6.00001C 5.5,6.83008 4.83,7.50001 4,7.50001C 3.17,7.50001 2.5,6.83008 2.5,6.00001C 2.5,5.16993 3.17,4.50001 4,4.50001 Z M 4,10.5C 4.83,10.5 5.5,11.17 5.5,12C 5.5,12.83 4.83,13.5 4,13.5C 3.17,13.5 2.5,12.83 2.5,12C 2.5,11.17 3.17,10.5 4,10.5 Z M 7,19L 7,17L 21,17L 21,19L 7,19 Z M 4,16.5C 4.83,16.5 5.5,17.17 5.5,18C 5.5,18.83 4.83,19.5 4,19.5C 3.17,19.5 2.5,18.83 2.5,18C 2.5,17.17 3.17,16.5 4,16.5 Z " />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/FormatListNumbers.vue
Normal file
5
src/icons/FormatListNumbers.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 6.99938,12.998L 20.9994,12.998L 20.9994,10.998L 6.99938,10.998M 6.99938,18.998L 20.9994,18.998L 20.9994,16.998L 6.99938,16.998M 6.99938,6.99805L 20.9994,6.99805L 20.9994,4.99805L 6.99938,4.99805M 1.99938,10.998L 3.79939,10.998L 1.99938,13.0981L 1.99938,13.998L 4.99938,13.998L 4.99938,12.998L 3.19938,12.998L 4.99938,10.898L 4.99938,9.99805L 1.99938,9.99805M 2.99938,7.99805L 3.99938,7.99805L 3.99938,3.99805L 1.99938,3.99805L 1.99938,4.99805L 2.99938,4.99805M 1.99938,16.998L 3.99938,16.998L 3.99938,17.498L 2.99938,17.498L 2.99938,18.498L 3.99938,18.498L 3.99938,18.998L 1.99938,18.998L 1.99938,19.998L 4.99938,19.998L 4.99938,15.998L 1.99938,15.998L 1.99938,16.998 Z " />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/FormatQuoteClose.vue
Normal file
5
src/icons/FormatQuoteClose.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-linejoin="round" d="M 14,17L 17,17L 19,13L 19,7L 13,7L 13,13L 16,13M 6,17L 9,17L 11,13L 11,7L 5,7L 5,13L 8,13L 6,17 Z " />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/FormatSize.vue
Normal file
5
src/icons/FormatSize.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-linejoin="round" d="M 3,12L 6,12L 6,19L 9,19L 9,12L 12,12L 12,9L 3,9M 9,4L 9,7L 14,7L 14,19L 17,19L 17,7L 22,7L 22,4L 9,4 Z " />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/FormatStrikethrough.vue
Normal file
5
src/icons/FormatStrikethrough.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="miter" d="M20.275,12.05l0,1.505l-3.305,0c0.756,1.61 0.72,6.009 -4.682,6.03c-6.265,0.023 -6.03,-4.89 -6.03,-4.89l2.986,0.023c0.023,2.539 2.362,2.539 2.844,2.504c0.482,-0.035 2.28,-0.023 2.421,-1.787c0.066,-0.818 -0.761,-1.444 -1.665,-1.88l-9.119,0l0,-1.505l16.55,0Zm-2.698,-3.091l-2.997,-0.024c0,0 0.129,-2.08 -2.468,-2.092c-2.598,-0.012 -2.386,1.658 -2.363,1.869c0.024,0.212 0.247,1.246 2.257,1.74l-4.737,0c0,0 -2.624,-5.062 3.785,-5.936c6.547,-0.893 6.535,4.455 6.523,4.443Z" />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/LinkVariant.vue
Normal file
5
src/icons/LinkVariant.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-linejoin="round" d="M 10.5858,13.4142C 10.9763,13.8047 10.9763,14.4379 10.5858,14.8284C 10.1952,15.2189 9.56207,15.2189 9.17154,14.8284C 7.21892,12.8758 7.21892,9.70995 9.17154,7.75733L 9.17157,7.75736L 12.707,4.2219C 14.6596,2.26928 17.8255,2.26929 19.7781,4.2219C 21.7307,6.17452 21.7307,9.34034 19.7781,11.293L 18.2925,12.7785C 18.3008,11.9583 18.1659,11.1368 17.8876,10.355L 18.3639,9.87865C 19.5355,8.70708 19.5355,6.80759 18.3639,5.63602C 17.1923,4.46445 15.2929,4.46445 14.1213,5.63602L 10.5858,9.17155C 9.41419,10.3431 9.41419,12.2426 10.5858,13.4142 Z M 13.4142,9.17155C 13.8047,8.78103 14.4379,8.78103 14.8284,9.17155C 16.781,11.1242 16.781,14.29 14.8284,16.2426L 14.8284,16.2426L 11.2929,19.7782C 9.34026,21.7308 6.17444,21.7308 4.22182,19.7782C 2.26921,17.8255 2.2692,14.6597 4.22182,12.7071L 5.70744,11.2215C 5.69913,12.0417 5.8341,12.8631 6.11234,13.645L 5.63601,14.1213C 4.46444,15.2929 4.46444,17.1924 5.63601,18.3639C 6.80758,19.5355 8.70708,19.5355 9.87865,18.3639L 13.4142,14.8284C 14.5858,13.6568 14.5858,11.7573 13.4142,10.5858C 13.0237,10.1952 13.0237,9.56207 13.4142,9.17155 Z " />
|
||||
</svg>
|
||||
</template>
|
6
src/icons/NavigationBar.vue
Normal file
6
src/icons/NavigationBar.vue
Normal file
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="miter" d="M19,8.977l-14,0l0,10l14,0m0,2l-14,0c-1.104,0 -2,-0.896 -2,-2l0,-10c0,-1.105 0.896,-2 2,-2l14,0c1.105,0 2,0.895 2,2l0,10c0,1.104 -0.895,2 -2,2Z" />
|
||||
<rect x="3" y="3.023" width="18" height="2" />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/SidePreview.vue
Normal file
5
src/icons/SidePreview.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="miter" d="M11,20.977l-6,0c-1.104,0 -2,-0.896 -2,-2l0,-14c0,-1.105 0.896,-2 2,-2l14,0c1.105,0 2,0.895 2,2l0,14c0,1.104 -0.895,2 -2,2l-6,0l0,0.023l-2,0l0,-0.023Zm0,-2l0,-14l-6,0l0,14l6,0Zm8,-14l-6,0l0,14l6,0l0,-14Z" />
|
||||
</svg>
|
||||
</template>
|
6
src/icons/StatusBar.vue
Normal file
6
src/icons/StatusBar.vue
Normal file
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="miter" d="M19,15.023l-14,0l0,-10l14,0m0,-2l-14,0c-1.104,0 -2,0.896 -2,2l0,10c0,1.105 0.896,2 2,2l14,0c1.105,0 2,-0.895 2,-2l0,-10c0,-1.104 -0.895,-2 -2,-2Z" />
|
||||
<rect x="3" y="18.977" width="18" height="2" />
|
||||
</svg>
|
||||
</template>
|
5
src/icons/Table.vue
Normal file
5
src/icons/Table.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
|
||||
<path fill="#000000" fill-opacity="1" stroke-width="1.33333" stroke-linejoin="round" d="M 5,4L 19,4C 20.1046,4 21,4.89543 21,6L 21,18C 21,19.1046 20.1046,20 19,20L 5,20C 3.89543,20 3,19.1046 3,18L 3,6C 3,4.89543 3.89543,4 5,4 Z M 5,8L 5,12L 11,12L 11,8L 5,8 Z M 13,8L 13,12L 19,12L 19,8L 13,8 Z M 5,14L 5,18L 11,18L 11,14L 5,14 Z M 13,14L 13,18L 19,18L 19,14L 13,14 Z " />
|
||||
</svg>
|
||||
</template>
|
34
src/icons/index.js
Normal file
34
src/icons/index.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import Vue from 'vue';
|
||||
import FormatBold from './FormatBold';
|
||||
import FormatItalic from './FormatItalic';
|
||||
import FormatQuoteClose from './FormatQuoteClose';
|
||||
import CodeBraces from './CodeBraces';
|
||||
import LinkVariant from './LinkVariant';
|
||||
import FileImage from './FileImage';
|
||||
import Table from './Table';
|
||||
import FormatListNumbers from './FormatListNumbers';
|
||||
import FormatListBulleted from './FormatListBulleted';
|
||||
import FormatSize from './FormatSize';
|
||||
import FormatHorizontalRule from './FormatHorizontalRule';
|
||||
import FormatStrikethrough from './FormatStrikethrough';
|
||||
import StatusBar from './StatusBar';
|
||||
import NavigationBar from './NavigationBar';
|
||||
import SidePreview from './SidePreview';
|
||||
import Eye from './Eye';
|
||||
|
||||
Vue.component('iconFormatBold', FormatBold);
|
||||
Vue.component('iconFormatItalic', FormatItalic);
|
||||
Vue.component('iconFormatQuoteClose', FormatQuoteClose);
|
||||
Vue.component('iconCodeBraces', CodeBraces);
|
||||
Vue.component('iconLinkVariant', LinkVariant);
|
||||
Vue.component('iconFileImage', FileImage);
|
||||
Vue.component('iconTable', Table);
|
||||
Vue.component('iconFormatListNumbers', FormatListNumbers);
|
||||
Vue.component('iconFormatListBulleted', FormatListBulleted);
|
||||
Vue.component('iconFormatSize', FormatSize);
|
||||
Vue.component('iconFormatHorizontalRule', FormatHorizontalRule);
|
||||
Vue.component('iconFormatStrikethrough', FormatStrikethrough);
|
||||
Vue.component('iconStatusBar', StatusBar);
|
||||
Vue.component('iconNavigationBar', NavigationBar);
|
||||
Vue.component('iconSidePreview', SidePreview);
|
||||
Vue.component('iconEye', Eye);
|
15
src/index.js
Normal file
15
src/index.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import Vue from 'vue';
|
||||
import App from './components/App';
|
||||
import store from './store';
|
||||
import './extensions/';
|
||||
import './services/optional';
|
||||
import './icons/';
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
/* eslint-disable no-new */
|
||||
new Vue({
|
||||
el: '#app',
|
||||
store,
|
||||
render: h => h(App),
|
||||
});
|
117
src/markdown/sample.md
Normal file
117
src/markdown/sample.md
Normal file
|
@ -0,0 +1,117 @@
|
|||
# Basic writing
|
||||
|
||||
## Styling text
|
||||
|
||||
Make text **bold** or *italic* by using either `*` or `_` around the text.
|
||||
|
||||
_This text will be italic_
|
||||
__This text will be bold__
|
||||
|
||||
Create strikethrough text by using `~~`.
|
||||
|
||||
~~Mistaken text.~~
|
||||
|
||||
## Blockquotes
|
||||
|
||||
Indicate blockquotes with a `>`.
|
||||
|
||||
> Blockquote
|
||||
|
||||
## Headings
|
||||
|
||||
Create a heading by adding one or more `#` symbols before your heading text.
|
||||
|
||||
##### Heading level 5
|
||||
###### Heading level 6
|
||||
|
||||
## Horizontal rules
|
||||
|
||||
Insert a horizontal rule by putting three or more `-`, `*`, or `_` on a line by themselves.
|
||||
|
||||
----------
|
||||
|
||||
## Table of contents
|
||||
|
||||
Insert a table of contents using the marker `[TOC]`.
|
||||
|
||||
[TOC]
|
||||
|
||||
|
||||
# Lists
|
||||
|
||||
## Unordered lists
|
||||
|
||||
Make an unordered list by preceding list items with either a `*` or a `-`.
|
||||
|
||||
- Item
|
||||
- Item
|
||||
* Item
|
||||
|
||||
## Ordered lists
|
||||
|
||||
Make an ordered list by preceding list items with a number.
|
||||
|
||||
1. Item 1
|
||||
2. Item 2
|
||||
3. Item 3
|
||||
|
||||
|
||||
# Code formatting
|
||||
|
||||
## Inline formats
|
||||
|
||||
Use single backticks to format text in a special `monospace format`.
|
||||
|
||||
## Multiple lines
|
||||
|
||||
Indent four spaces or a tab to format text as its own distinct block.
|
||||
|
||||
var foo = 'bar'; // baz
|
||||
|
||||
## Code highlighting
|
||||
|
||||
Use triple backticks including the language identifier to have syntax highlighting.
|
||||
|
||||
```js
|
||||
var foo = 'bar'; // baz
|
||||
```
|
||||
|
||||
|
||||
# Links and images
|
||||
|
||||
## Links
|
||||
|
||||
Create a link by wrapping link text in brackets, and then wrapping the link in parentheses.
|
||||
|
||||
[Visit Classeur](http://classeur.io)
|
||||
|
||||
## Images
|
||||
|
||||
Images are like links, but have an exclamation point in front of them.
|
||||
|
||||
![Classeur Logo](http://classeur.io/images/logo.png)
|
||||
|
||||
## Footnotes
|
||||
|
||||
To create footnotes, add a label starting with a `^` between a set of square brackets like this[^footnote], and then, declare the linked content.
|
||||
|
||||
[^footnote]: Here is the content of the footnote.
|
||||
|
||||
|
||||
# Tables
|
||||
|
||||
Create tables by assembling a list of words and dividing them with hyphens (for the first row), and then separating each column with a pipe.
|
||||
|
||||
First Header | Second Header
|
||||
------------- | -------------
|
||||
Content Cell | Content Cell
|
||||
Content Cell | Content Cell
|
||||
|
||||
By including colons within the header row, you can define text to be left-aligned, right-aligned, or center-aligned.
|
||||
|
||||
| Left-Aligned | Center Aligned | Right Aligned |
|
||||
| :------------ |:---------------:| -----:|
|
||||
| col 3 is | some wordy text | $1600 |
|
||||
| col 2 is | centered | $12 |
|
||||
| zebra stripes | are neat | $1 |
|
||||
|
228
src/services/animationSvc.js
Normal file
228
src/services/animationSvc.js
Normal file
|
@ -0,0 +1,228 @@
|
|||
import bezierEasing from 'bezier-easing';
|
||||
|
||||
const easings = {
|
||||
materialIn: bezierEasing(0.75, 0, 0.8, 0.25),
|
||||
materialOut: bezierEasing(0.25, 0.8, 0.25, 1.0),
|
||||
};
|
||||
|
||||
const vendors = ['moz', 'webkit'];
|
||||
for (let x = 0; x < vendors.length && !window.requestAnimationFrame; x += 1) {
|
||||
window.requestAnimationFrame = window[`${vendors[x]}RequestAnimationFrame`];
|
||||
window.cancelAnimationFrame = window[`${vendors[x]}CancelAnimationFrame`] ||
|
||||
window[`${vendors[x]}CancelRequestAnimationFrame`];
|
||||
}
|
||||
|
||||
const transformStyles = [
|
||||
'WebkitTransform',
|
||||
'MozTransform',
|
||||
'msTransform',
|
||||
'OTransform',
|
||||
'transform',
|
||||
];
|
||||
|
||||
const transitionEndEvents = {
|
||||
WebkitTransition: 'webkitTransitionEnd',
|
||||
MozTransition: 'transitionend',
|
||||
msTransition: 'MSTransitionEnd',
|
||||
OTransition: 'oTransitionEnd',
|
||||
transition: 'transitionend',
|
||||
};
|
||||
|
||||
function getStyle(styles) {
|
||||
const elt = document.createElement('div');
|
||||
return styles.reduce((result, style) => {
|
||||
if (elt.style[style] === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return style;
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
const transformStyle = getStyle(transformStyles);
|
||||
const transitionStyle = getStyle(Object.keys(transitionEndEvents));
|
||||
const transitionEndEvent = transitionEndEvents[transitionStyle];
|
||||
|
||||
function identity(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
function ElementAttribute(name) {
|
||||
this.name = name;
|
||||
this.setStart = (animation) => {
|
||||
const value = animation.elt[name];
|
||||
animation.$start[name] = value;
|
||||
return value !== undefined && animation.$end[name] !== undefined;
|
||||
};
|
||||
this.applyCurrent = (animation) => {
|
||||
animation.elt[name] = animation.$current[name];
|
||||
};
|
||||
}
|
||||
|
||||
function StyleAttribute(name, unit, defaultValue, wrap = identity) {
|
||||
this.name = name;
|
||||
this.setStart = (animation) => {
|
||||
let value = parseFloat(animation.elt.style[name]);
|
||||
if (isNaN(value)) {
|
||||
value = animation.$current[name] || defaultValue;
|
||||
}
|
||||
animation.$start[name] = value;
|
||||
return animation.$end[name] !== undefined;
|
||||
};
|
||||
this.applyCurrent = (animation) => {
|
||||
animation.elt.style[name] = wrap(animation.$current[name]) + unit;
|
||||
};
|
||||
}
|
||||
|
||||
function TransformAttribute(name, unit, defaultValue, wrap = identity) {
|
||||
this.name = name;
|
||||
this.setStart = (animation) => {
|
||||
let value = animation.$current[name];
|
||||
if (value === undefined) {
|
||||
value = defaultValue;
|
||||
}
|
||||
animation.$start[name] = value;
|
||||
if (animation.$end[name] === undefined) {
|
||||
animation.$end[name] = value;
|
||||
}
|
||||
return value !== undefined;
|
||||
};
|
||||
this.applyCurrent = (animation) => {
|
||||
const value = animation.$current[name];
|
||||
return value !== defaultValue && `${name}(${wrap(value)}${unit})`;
|
||||
};
|
||||
}
|
||||
|
||||
const attributes = [
|
||||
new ElementAttribute('scrollTop'),
|
||||
new ElementAttribute('scrollLeft'),
|
||||
new StyleAttribute('opacity', '', 1),
|
||||
new StyleAttribute('zIndex', '', 0),
|
||||
new TransformAttribute('translateX', 'px', 0, Math.round),
|
||||
new TransformAttribute('translateY', 'px', 0, Math.round),
|
||||
new TransformAttribute('scale', '', 1),
|
||||
new TransformAttribute('rotate', 'deg', 0),
|
||||
].concat([
|
||||
'width',
|
||||
'height',
|
||||
'top',
|
||||
'right',
|
||||
'bottom',
|
||||
'left',
|
||||
].map(name => new StyleAttribute(name, 'px', 0, Math.round)));
|
||||
|
||||
class Animation {
|
||||
constructor(elt) {
|
||||
this.elt = elt;
|
||||
this.$current = {};
|
||||
this.$pending = {};
|
||||
}
|
||||
|
||||
start(param1, param2, param3) {
|
||||
let endCb = param1;
|
||||
let stepCb = param2;
|
||||
let useTransition = false;
|
||||
if (typeof param1 === 'boolean') {
|
||||
useTransition = param1;
|
||||
endCb = param2;
|
||||
stepCb = param3;
|
||||
}
|
||||
|
||||
this.stop();
|
||||
this.$start = {};
|
||||
this.$end = this.$pending;
|
||||
this.$pending = {};
|
||||
this.$attributes = attributes.filter(attribute => attribute.setStart(this));
|
||||
this.$end.duration = this.$end.duration || 0;
|
||||
this.$end.delay = this.$end.delay || 0;
|
||||
this.$end.easing = easings[this.$end.easing] || easings.materialOut;
|
||||
this.$end.endCb = typeof endCb === 'function' && endCb;
|
||||
this.$end.stepCb = typeof stepCb === 'function' && stepCb;
|
||||
this.$startTime = Date.now() + this.$end.delay;
|
||||
this.loop(this.$end.duration && useTransition);
|
||||
return this.elt;
|
||||
}
|
||||
|
||||
stop() {
|
||||
window.cancelAnimationFrame(this.$requestId);
|
||||
}
|
||||
|
||||
loop(useTransition) {
|
||||
const onTransitionEnd = (evt) => {
|
||||
if (evt.target === this.elt) {
|
||||
this.elt.removeEventListener(transitionEndEvent, onTransitionEnd);
|
||||
const endCb = this.$end.endCb;
|
||||
this.$end.endCb = undefined;
|
||||
if (endCb) {
|
||||
endCb();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let progress = (Date.now() - this.$startTime) / this.$end.duration;
|
||||
let transition = '';
|
||||
if (useTransition) {
|
||||
progress = 1;
|
||||
const transitions = [
|
||||
'all',
|
||||
`${this.$end.duration}ms`,
|
||||
this.$end.easing.toCSS(),
|
||||
];
|
||||
if (this.$end.delay) {
|
||||
transitions.push(`${this.$end.duration}ms`);
|
||||
}
|
||||
transition = transitions.join(' ');
|
||||
if (this.$end.endCb) {
|
||||
this.elt.addEventListener(transitionEndEvent, onTransitionEnd);
|
||||
}
|
||||
} else if (progress < 1) {
|
||||
this.$requestId = window.requestAnimationFrame(() => this.loop(false));
|
||||
if (progress < 0) {
|
||||
return;
|
||||
}
|
||||
} else if (this.$end.endCb) {
|
||||
this.$requestId = window.requestAnimationFrame(this.$end.endCb);
|
||||
}
|
||||
|
||||
const coeff = this.$end.easing.get(progress);
|
||||
const transforms = this.$attributes.reduce((result, attribute) => {
|
||||
if (progress < 1) {
|
||||
const diff = this.$end[attribute.name] - this.$start[attribute.name];
|
||||
this.$current[attribute.name] = this.$start[attribute.name] + (diff * coeff);
|
||||
} else {
|
||||
this.$current[attribute.name] = this.$end[attribute.name];
|
||||
}
|
||||
const transform = attribute.applyCurrent(this);
|
||||
if (transform) {
|
||||
result.push(transform);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
if (transforms.length) {
|
||||
transforms.push('translateZ(0)'); // activate GPU
|
||||
}
|
||||
const transform = transforms.join(' ');
|
||||
this.elt.style[transformStyle] = transform;
|
||||
this.elt.style[transitionStyle] = transition;
|
||||
if (this.$end.stepCb) {
|
||||
this.$end.stepCb();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attributes.map(attribute => attribute.name).concat('duration', 'easing', 'delay')
|
||||
.forEach((name) => {
|
||||
Animation.prototype[name] = function setter(val) {
|
||||
this.$pending[name] = val;
|
||||
return this;
|
||||
};
|
||||
});
|
||||
|
||||
function animate(elt) {
|
||||
if (!elt.$animation) {
|
||||
elt.$animation = new Animation(elt);
|
||||
}
|
||||
return elt.$animation;
|
||||
}
|
||||
|
||||
export default { animate };
|
3
src/services/constants.js
Normal file
3
src/services/constants.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
scrollOffset: 20,
|
||||
};
|
158
src/services/editorEngineSvc.js
Normal file
158
src/services/editorEngineSvc.js
Normal file
|
@ -0,0 +1,158 @@
|
|||
import DiffMatchPatch from 'diff-match-patch';
|
||||
import cledit from '../cledit/cledit';
|
||||
import clDiffUtils from '../cledit/cldiffutils';
|
||||
import store from '../store';
|
||||
|
||||
let clEditor;
|
||||
const newDiscussionMarker0 = new cledit.Marker(0);
|
||||
const newDiscussionMarker1 = new cledit.Marker(0, true);
|
||||
let markerKeys;
|
||||
let markerIdxMap;
|
||||
let previousPatchableText;
|
||||
let currentPatchableText;
|
||||
let discussionMarkers;
|
||||
let content;
|
||||
let isChangePatch;
|
||||
|
||||
function getDiscussionMarkers(discussion, discussionId, onMarker) {
|
||||
function getMarker(offsetName) {
|
||||
const markerOffset = discussion[offsetName];
|
||||
const markerKey = discussionId + offsetName;
|
||||
let marker = discussionMarkers[markerKey];
|
||||
if (markerOffset !== undefined) {
|
||||
if (!marker) {
|
||||
marker = new cledit.Marker(markerOffset, offsetName === 'offset1');
|
||||
marker.discussionId = discussionId;
|
||||
marker.offsetName = offsetName;
|
||||
clEditor.addMarker(marker);
|
||||
discussionMarkers[markerKey] = marker;
|
||||
}
|
||||
onMarker(marker);
|
||||
}
|
||||
}
|
||||
getMarker('offset0');
|
||||
getMarker('offset1');
|
||||
}
|
||||
|
||||
function syncDiscussionMarkers() {
|
||||
Object.keys(discussionMarkers)
|
||||
.forEach((markerKey) => {
|
||||
const marker = discussionMarkers[markerKey];
|
||||
// Remove marker if discussion was removed
|
||||
const discussion = content.discussions[marker.discussionId];
|
||||
if (!discussion || discussion[marker.offsetName] === undefined) {
|
||||
clEditor.removeMarker(marker);
|
||||
delete discussionMarkers[markerKey];
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(content.discussions)
|
||||
.forEach((discussionId) => {
|
||||
const discussion = content.discussions[discussionId];
|
||||
getDiscussionMarkers(discussion, discussionId, (marker) => {
|
||||
discussion[marker.offsetName] = marker.offset;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const diffMatchPatch = new DiffMatchPatch();
|
||||
|
||||
function makePatches() {
|
||||
const diffs = diffMatchPatch.diff_main(previousPatchableText, currentPatchableText);
|
||||
return diffMatchPatch.patch_make(previousPatchableText, diffs);
|
||||
}
|
||||
|
||||
function applyPatches(patches) {
|
||||
const newPatchableText = diffMatchPatch.patch_apply(patches, currentPatchableText)[0];
|
||||
let result = newPatchableText;
|
||||
if (markerKeys.length) {
|
||||
// Strip text markers
|
||||
result = result.replace(new RegExp(`[\ue000-${String.fromCharCode((0xe000 + markerKeys.length) - 1)}]`, 'g'), '');
|
||||
}
|
||||
// Expect a `contentChanged` event
|
||||
if (result !== clEditor.getContent()) {
|
||||
previousPatchableText = currentPatchableText;
|
||||
currentPatchableText = newPatchableText;
|
||||
isChangePatch = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function reversePatches(patches) {
|
||||
const result = diffMatchPatch.patch_deepCopy(patches).reverse();
|
||||
result.forEach((patch) => {
|
||||
patch.diffs.forEach((diff) => {
|
||||
diff[0] = -diff[0];
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export default {
|
||||
clEditor: null,
|
||||
lastChange: 0,
|
||||
lastExternalChange: 0,
|
||||
createClEditor(editorElt) {
|
||||
this.clEditor = cledit(editorElt, editorElt.parentNode);
|
||||
clEditor = this.clEditor;
|
||||
markerKeys = [];
|
||||
markerIdxMap = Object.create(null);
|
||||
discussionMarkers = {};
|
||||
clEditor.on('contentChanged', (text) => {
|
||||
store.commit('files/setCurrentFileContentText', text);
|
||||
syncDiscussionMarkers();
|
||||
if (!isChangePatch) {
|
||||
previousPatchableText = currentPatchableText;
|
||||
currentPatchableText = clDiffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
||||
} else {
|
||||
// Take a chance to restore discussion offsets on undo/redo
|
||||
content.text = currentPatchableText;
|
||||
clDiffUtils.restoreDiscussionOffsets(content, markerKeys);
|
||||
content.discussions.cl_each((discussion, discussionId) => {
|
||||
getDiscussionMarkers(discussion, discussionId, (marker) => {
|
||||
marker.offset = discussion[marker.offsetName];
|
||||
});
|
||||
});
|
||||
}
|
||||
isChangePatch = false;
|
||||
this.lastChange = Date.now();
|
||||
});
|
||||
clEditor.addMarker(newDiscussionMarker0);
|
||||
clEditor.addMarker(newDiscussionMarker1);
|
||||
},
|
||||
initClEditor(opts, reinit) {
|
||||
if (store.state.files.currentFile.content) {
|
||||
const options = Object.assign({}, opts);
|
||||
|
||||
if (content !== store.state.files.currentFile.content) {
|
||||
content = store.state.files.currentFile.content;
|
||||
currentPatchableText = clDiffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
||||
previousPatchableText = currentPatchableText;
|
||||
syncDiscussionMarkers();
|
||||
}
|
||||
|
||||
if (reinit) {
|
||||
options.content = content.text;
|
||||
options.selectionStart = content.state.selectionStart;
|
||||
options.selectionEnd = content.state.selectionEnd;
|
||||
}
|
||||
|
||||
options.patchHandler = {
|
||||
makePatches,
|
||||
applyPatches: patches => applyPatches(patches),
|
||||
reversePatches,
|
||||
};
|
||||
clEditor.init(options);
|
||||
}
|
||||
},
|
||||
applyContent(isExternal) {
|
||||
if (!clEditor) {
|
||||
return null;
|
||||
}
|
||||
if (isExternal) {
|
||||
this.lastExternalChange = Date.now();
|
||||
}
|
||||
syncDiscussionMarkers();
|
||||
return clEditor.setContent(content.text, isExternal);
|
||||
},
|
||||
};
|
734
src/services/editorSvc.js
Normal file
734
src/services/editorSvc.js
Normal file
|
@ -0,0 +1,734 @@
|
|||
import Vue from 'vue';
|
||||
import DiffMatchPatch from 'diff-match-patch';
|
||||
import Prism from 'prismjs';
|
||||
import markdownItPandocRenderer from 'markdown-it-pandoc-renderer';
|
||||
import cledit from '../cledit/cledit';
|
||||
import pagedown from '../cledit/pagedown';
|
||||
import htmlSanitizer from '../cledit/htmlSanitizer';
|
||||
import markdownConversionSvc from './markdownConversionSvc';
|
||||
import markdownGrammarSvc from './markdownGrammarSvc';
|
||||
import sectionUtils from './sectionUtils';
|
||||
import extensionSvc from './extensionSvc';
|
||||
import constants from './constants';
|
||||
import animationSvc from './animationSvc';
|
||||
import editorEngineSvc from './editorEngineSvc';
|
||||
import store from '../store';
|
||||
|
||||
const debounce = cledit.Utils.debounce;
|
||||
|
||||
const allowDebounce = (action, wait) => {
|
||||
let timeoutId;
|
||||
return (doDebounce = false) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (doDebounce) {
|
||||
timeoutId = setTimeout(() => action(), wait);
|
||||
} else {
|
||||
action();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const diffMatchPatch = new DiffMatchPatch();
|
||||
let reinitClEditor = true;
|
||||
let isPreviewRefreshed = false;
|
||||
let tokens;
|
||||
const anchorHash = {};
|
||||
|
||||
const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event bus
|
||||
// Elements
|
||||
editorElt: null,
|
||||
previewElt: null,
|
||||
tocElt: null,
|
||||
// Other objects
|
||||
pagedownEditor: null,
|
||||
options: {},
|
||||
prismGrammars: {},
|
||||
converter: null,
|
||||
parsingCtx: null,
|
||||
conversionCtx: null,
|
||||
sectionList: null,
|
||||
sectionDescList: [],
|
||||
sectionDescMeasuredList: null,
|
||||
sectionDescWithDiffsList: null,
|
||||
selectionRange: null,
|
||||
previewSelectionRange: null,
|
||||
previewSelectionStartOffset: null,
|
||||
previewHtml: null,
|
||||
previewText: null,
|
||||
|
||||
/**
|
||||
* Get element and dimension that handles scrolling.
|
||||
*/
|
||||
getObjectToScroll() {
|
||||
let elt = this.editorElt.parentNode;
|
||||
let dimensionKey = 'editorDimension';
|
||||
if (!store.state.layout.showEditor) {
|
||||
elt = this.previewElt.parentNode;
|
||||
dimensionKey = 'previewDimension';
|
||||
}
|
||||
return {
|
||||
elt,
|
||||
dimensionKey,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an object describing the position of the scroll bar in the file.
|
||||
*/
|
||||
getScrollPosition() {
|
||||
const objToScroll = this.getObjectToScroll();
|
||||
const scrollTop = objToScroll.elt.scrollTop;
|
||||
let result;
|
||||
if (this.sectionDescMeasuredList) {
|
||||
this.sectionDescMeasuredList.some((sectionDesc, sectionIdx) => {
|
||||
if (scrollTop >= sectionDesc[objToScroll.dimensionKey].endOffset) {
|
||||
return false;
|
||||
}
|
||||
const posInSection = (scrollTop - sectionDesc[objToScroll.dimensionKey].startOffset) /
|
||||
(sectionDesc[objToScroll.dimensionKey].height || 1);
|
||||
result = {
|
||||
sectionIdx,
|
||||
posInSection,
|
||||
};
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the offset in the preview corresponding to the offset of the markdown in the editor
|
||||
*/
|
||||
getPreviewOffset(editorOffset) {
|
||||
let previewOffset = 0;
|
||||
let offset = editorOffset;
|
||||
this.sectionDescList.some((sectionDesc) => {
|
||||
if (!sectionDesc.textToPreviewDiffs) {
|
||||
previewOffset = undefined;
|
||||
return true;
|
||||
}
|
||||
if (sectionDesc.section.text.length >= offset) {
|
||||
previewOffset += diffMatchPatch.diff_xIndex(sectionDesc.textToPreviewDiffs, offset);
|
||||
return true;
|
||||
}
|
||||
offset -= sectionDesc.section.text.length;
|
||||
previewOffset += sectionDesc.previewText.length;
|
||||
return false;
|
||||
});
|
||||
return previewOffset;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the offset of the markdown in the editor corresponding to the offset in the preview
|
||||
*/
|
||||
getEditorOffset(previewOffset) {
|
||||
let offset = previewOffset;
|
||||
let editorOffset = 0;
|
||||
this.sectionDescList.some((sectionDesc) => {
|
||||
if (!sectionDesc.textToPreviewDiffs) {
|
||||
editorOffset = undefined;
|
||||
return true;
|
||||
}
|
||||
if (sectionDesc.previewText.length >= offset) {
|
||||
const previewToTextDiffs = sectionDesc.textToPreviewDiffs
|
||||
.map(diff => [-diff[0], diff[1]]);
|
||||
editorOffset += diffMatchPatch.diff_xIndex(previewToTextDiffs, offset);
|
||||
return true;
|
||||
}
|
||||
offset -= sectionDesc.previewText.length;
|
||||
editorOffset += sectionDesc.section.text.length;
|
||||
return false;
|
||||
});
|
||||
return editorOffset;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the pandoc AST generated from the file tokens and the converter options
|
||||
*/
|
||||
getPandocAst() {
|
||||
return tokens && markdownItPandocRenderer(tokens, this.converter.options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the Prism grammar with the options
|
||||
*/
|
||||
initPrism() {
|
||||
const options = {
|
||||
insideFences: markdownConversionSvc.defaultOptions.insideFences,
|
||||
...this.options,
|
||||
};
|
||||
this.prismGrammars = markdownGrammarSvc.makeGrammars(options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the markdown-it converter with the options
|
||||
*/
|
||||
initConverter() {
|
||||
this.converter = markdownConversionSvc.createConverter(this.options, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the cledit editor with markdown-it section parser and Prism highlighter
|
||||
*/
|
||||
initClEditor() {
|
||||
if (!store.state.files.currentFile.isLoaded) {
|
||||
reinitClEditor = true;
|
||||
isPreviewRefreshed = false;
|
||||
editorEngineSvc.clEditor.toggleEditable(false);
|
||||
return;
|
||||
}
|
||||
const options = {
|
||||
sectionHighlighter: section => Prism.highlight(
|
||||
section.text, this.prismGrammars[section.data]),
|
||||
sectionParser: (text) => {
|
||||
this.parsingCtx = markdownConversionSvc.parseSections(this.converter, text);
|
||||
return this.parsingCtx.sections;
|
||||
},
|
||||
};
|
||||
editorEngineSvc.initClEditor(options, reinitClEditor);
|
||||
editorEngineSvc.clEditor.toggleEditable(true);
|
||||
reinitClEditor = false;
|
||||
this.restoreScrollPosition();
|
||||
},
|
||||
|
||||
/**
|
||||
* Finish the conversion initiated by the section parser
|
||||
*/
|
||||
convert() {
|
||||
this.conversionCtx = markdownConversionSvc.convert(this.parsingCtx, this.conversionCtx);
|
||||
this.$emit('conversionCtx', this.conversionCtx);
|
||||
tokens = this.parsingCtx.markdownState.tokens;
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh the preview with the result of `convert()`
|
||||
*/
|
||||
refreshPreview() {
|
||||
const newSectionDescList = [];
|
||||
let sectionPreviewElt;
|
||||
let sectionTocElt;
|
||||
let sectionIdx = 0;
|
||||
let sectionDescIdx = 0;
|
||||
let insertBeforePreviewElt = this.previewElt.firstChild;
|
||||
let insertBeforeTocElt = this.tocElt.firstChild;
|
||||
let previewHtml = '';
|
||||
let heading;
|
||||
this.conversionCtx.htmlSectionDiff.forEach((item) => {
|
||||
for (let i = 0; i < item[1].length; i += 1) {
|
||||
const section = this.conversionCtx.sectionList[sectionIdx];
|
||||
if (item[0] === 0) {
|
||||
const sectionDesc = this.sectionDescList[sectionDescIdx];
|
||||
sectionDescIdx += 1;
|
||||
sectionDesc.editorElt = section.elt;
|
||||
newSectionDescList.push(sectionDesc);
|
||||
previewHtml += sectionDesc.html;
|
||||
sectionIdx += 1;
|
||||
insertBeforePreviewElt.classList.remove('modified');
|
||||
insertBeforePreviewElt = insertBeforePreviewElt.nextSibling;
|
||||
insertBeforeTocElt.classList.remove('modified');
|
||||
insertBeforeTocElt = insertBeforeTocElt.nextSibling;
|
||||
} else if (item[0] === -1) {
|
||||
sectionDescIdx += 1;
|
||||
sectionPreviewElt = insertBeforePreviewElt;
|
||||
insertBeforePreviewElt = insertBeforePreviewElt.nextSibling;
|
||||
this.previewElt.removeChild(sectionPreviewElt);
|
||||
sectionTocElt = insertBeforeTocElt;
|
||||
insertBeforeTocElt = insertBeforeTocElt.nextSibling;
|
||||
this.tocElt.removeChild(sectionTocElt);
|
||||
} else if (item[0] === 1) {
|
||||
const html = this.conversionCtx.htmlSectionList[sectionIdx];
|
||||
sectionIdx += 1;
|
||||
|
||||
// Create preview section element
|
||||
sectionPreviewElt = document.createElement('div');
|
||||
sectionPreviewElt.className = 'cl-preview-section modified';
|
||||
sectionPreviewElt.innerHTML = htmlSanitizer.sanitizeHtml(html);
|
||||
if (insertBeforePreviewElt) {
|
||||
this.previewElt.insertBefore(sectionPreviewElt, insertBeforePreviewElt);
|
||||
} else {
|
||||
this.previewElt.appendChild(sectionPreviewElt);
|
||||
}
|
||||
extensionSvc.sectionPreview(sectionPreviewElt, this.options);
|
||||
|
||||
// Create TOC section element
|
||||
sectionTocElt = document.createElement('div');
|
||||
sectionTocElt.className = 'cl-toc-section modified';
|
||||
let headingElt = sectionPreviewElt.querySelector('h1, h2, h3, h4, h5, h6');
|
||||
heading = undefined;
|
||||
if (headingElt) {
|
||||
heading = {
|
||||
title: headingElt.textContent,
|
||||
anchor: headingElt.id,
|
||||
level: parseInt(headingElt.tagName.slice(1), 10),
|
||||
};
|
||||
headingElt = headingElt.cloneNode(true);
|
||||
headingElt.removeAttribute('id');
|
||||
sectionTocElt.appendChild(headingElt);
|
||||
}
|
||||
if (insertBeforeTocElt) {
|
||||
this.tocElt.insertBefore(sectionTocElt, insertBeforeTocElt);
|
||||
} else {
|
||||
this.tocElt.appendChild(sectionTocElt);
|
||||
}
|
||||
|
||||
const clonedElt = sectionPreviewElt.cloneNode(true);
|
||||
// Unwrap tables
|
||||
clonedElt.querySelectorAll('.table-wrapper').cl_each((elt) => {
|
||||
while (elt.firstChild) {
|
||||
elt.parentNode.appendChild(elt.firstChild);
|
||||
}
|
||||
elt.parentNode.removeChild(elt);
|
||||
});
|
||||
|
||||
previewHtml += clonedElt.innerHTML;
|
||||
newSectionDescList.push({
|
||||
section,
|
||||
editorElt: section.elt,
|
||||
previewElt: sectionPreviewElt,
|
||||
tocElt: sectionTocElt,
|
||||
html: clonedElt.innerHTML,
|
||||
heading,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
this.sectionDescList = newSectionDescList;
|
||||
this.previewHtml = previewHtml.replace(/^\s+|\s+$/g, '');
|
||||
this.tocElt.classList[
|
||||
this.tocElt.querySelector('.cl-toc-section *') ? 'remove' : 'add'
|
||||
]('toc-tab--empty');
|
||||
|
||||
// Run preview async operations (image loading, mathjax...)
|
||||
const loadingImages = this.previewElt.querySelectorAll('.cl-preview-section.modified img');
|
||||
const loadedPromises = loadingImages.cl_map(imgElt => new Promise((resolve) => {
|
||||
if (!imgElt.src) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const img = new window.Image();
|
||||
img.onload = resolve;
|
||||
img.onerror = resolve;
|
||||
img.src = imgElt.src;
|
||||
}));
|
||||
|
||||
Promise.all(loadedPromises.concat(extensionSvc.asyncPreview(this.options)))
|
||||
.then(() => {
|
||||
this.previewText = this.previewElt.textContent;
|
||||
this.$emit('previewText', this.previewText);
|
||||
// Debounce if sections have already been mesured
|
||||
this.measureSectionDimensions(!!this.sectionDescMeasuredList);
|
||||
this.makeTextToPreviewDiffs(true);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Measure the height of each section in editor, preview and toc.
|
||||
*/
|
||||
measureSectionDimensions: allowDebounce(() => {
|
||||
if (editorSvc.sectionDescList && this.sectionDescList !== editorSvc.sectionDescMeasuredList) {
|
||||
sectionUtils.measureSectionDimensions(editorSvc);
|
||||
editorSvc.sectionDescMeasuredList = editorSvc.sectionDescList;
|
||||
editorSvc.$emit('sectionDescMeasuredList', editorSvc.sectionDescMeasuredList);
|
||||
}
|
||||
}, 500),
|
||||
|
||||
/**
|
||||
* Make the diff between editor's markdown and preview's html.
|
||||
*/
|
||||
makeTextToPreviewDiffs: allowDebounce(() => {
|
||||
if (editorSvc.sectionDescList
|
||||
&& editorSvc.sectionDescList !== editorSvc.sectionDescMeasuredList) {
|
||||
editorSvc.sectionDescList
|
||||
.forEach((sectionDesc) => {
|
||||
if (!sectionDesc.textToPreviewDiffs) {
|
||||
sectionDesc.previewText = sectionDesc.previewElt.textContent;
|
||||
sectionDesc.textToPreviewDiffs = diffMatchPatch.diff_main(
|
||||
sectionDesc.section.text, sectionDesc.previewText);
|
||||
}
|
||||
});
|
||||
editorSvc.sectionDescWithDiffsList = editorSvc.sectionDescList;
|
||||
}
|
||||
}, 50),
|
||||
|
||||
/**
|
||||
* Save editor selection/scroll state into the current file content.
|
||||
*/
|
||||
saveContentState: allowDebounce(() => {
|
||||
const scrollPosition = editorSvc.getScrollPosition() ||
|
||||
store.state.files.currentFile.content.state.scrollPosition;
|
||||
store.commit('files/setCurrentFileContentState', {
|
||||
selectionStart: editorEngineSvc.clEditor.selectionMgr.selectionStart,
|
||||
selectionEnd: editorEngineSvc.clEditor.selectionMgr.selectionEnd,
|
||||
scrollPosition,
|
||||
}, { root: true });
|
||||
}, 100),
|
||||
|
||||
/**
|
||||
* Restore the scroll position from the current file content state.
|
||||
*/
|
||||
restoreScrollPosition() {
|
||||
const scrollPosition = store.state.files.currentFile.content.state.scrollPosition;
|
||||
if (scrollPosition && this.sectionDescMeasuredList) {
|
||||
const objectToScroll = this.getObjectToScroll();
|
||||
const sectionDesc = this.sectionDescMeasuredList[scrollPosition.sectionIdx];
|
||||
if (sectionDesc) {
|
||||
const scrollTop = sectionDesc[objectToScroll.dimensionKey].startOffset +
|
||||
(sectionDesc[objectToScroll.dimensionKey].height * scrollPosition.posInSection);
|
||||
objectToScroll.elt.scrollTop = scrollTop;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Report selection from the preview to the editor.
|
||||
*/
|
||||
saveSelection: allowDebounce(() => {
|
||||
const selection = window.getSelection();
|
||||
let range = selection.rangeCount && selection.getRangeAt(0);
|
||||
if (range) {
|
||||
if (
|
||||
/* eslint-disable no-bitwise */
|
||||
!(editorSvc.previewElt.compareDocumentPosition(range.startContainer)
|
||||
& window.Node.DOCUMENT_POSITION_CONTAINED_BY) ||
|
||||
!(editorSvc.previewElt.compareDocumentPosition(range.endContainer)
|
||||
& window.Node.DOCUMENT_POSITION_CONTAINED_BY)
|
||||
/* eslint-enable no-bitwise */
|
||||
) {
|
||||
range = null;
|
||||
}
|
||||
}
|
||||
if (editorSvc.previewSelectionRange !== range) {
|
||||
let previewSelectionStartOffset;
|
||||
let previewSelectionEndOffset;
|
||||
if (range) {
|
||||
const startRange = document.createRange();
|
||||
startRange.setStart(editorSvc.previewElt, 0);
|
||||
startRange.setEnd(range.startContainer, range.startOffset);
|
||||
previewSelectionStartOffset = `${startRange}`.length;
|
||||
previewSelectionEndOffset = previewSelectionStartOffset + `${range}`.length;
|
||||
const editorStartOffset = editorSvc.getEditorOffset(previewSelectionStartOffset);
|
||||
const editorEndOffset = editorSvc.getEditorOffset(previewSelectionEndOffset);
|
||||
if (editorStartOffset !== undefined && editorEndOffset !== undefined) {
|
||||
editorEngineSvc.clEditor.selectionMgr.setSelectionStartEnd(
|
||||
editorStartOffset, editorEndOffset, false);
|
||||
}
|
||||
}
|
||||
editorSvc.previewSelectionRange = range;
|
||||
}
|
||||
}, 50),
|
||||
|
||||
/**
|
||||
* Scroll the preview (or the editor if preview is hidden) to the specified anchor
|
||||
*/
|
||||
scrollToAnchor(anchor) {
|
||||
let scrollTop = 0;
|
||||
let scrollerElt = this.previewElt.parentNode;
|
||||
const sectionDesc = anchorHash[anchor];
|
||||
if (sectionDesc) {
|
||||
if (store.state.layout.showSidePreview || !store.state.layout.showEditor) {
|
||||
scrollTop = sectionDesc.previewDimension.startOffset;
|
||||
} else {
|
||||
scrollTop = sectionDesc.editorDimension.startOffset - constants.scrollOffset;
|
||||
scrollerElt = this.editorElt.parentNode;
|
||||
}
|
||||
} else {
|
||||
const elt = document.getElementById(anchor);
|
||||
if (elt) {
|
||||
scrollTop = elt.offsetTop;
|
||||
}
|
||||
}
|
||||
const maxScrollTop = scrollerElt.scrollHeight - scrollerElt.offsetHeight;
|
||||
if (scrollTop < 0) {
|
||||
scrollTop = 0;
|
||||
} else if (scrollTop > maxScrollTop) {
|
||||
scrollTop = maxScrollTop;
|
||||
}
|
||||
animationSvc.animate(scrollerElt)
|
||||
.scrollTop(scrollTop)
|
||||
.duration(360)
|
||||
.start();
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply the template to the file content
|
||||
*/
|
||||
// applyTemplate({ state, commit, dispatch, rootState }, template) {
|
||||
// function groupToc(array, level = 1) {
|
||||
// const result = [];
|
||||
// let currentItem;
|
||||
|
||||
// function pushCurrentItem() {
|
||||
// if (currentItem) {
|
||||
// if (currentItem.children.length > 0) {
|
||||
// currentItem.children = groupToc(currentItem.children, level + 1);
|
||||
// }
|
||||
// result.push(currentItem);
|
||||
// }
|
||||
// }
|
||||
// array.forEach((item) => {
|
||||
// if (item.level !== level) {
|
||||
// currentItem = currentItem || {
|
||||
// children: [],
|
||||
// };
|
||||
// currentItem.children.push(item);
|
||||
// } else {
|
||||
// pushCurrentItem();
|
||||
// currentItem = item;
|
||||
// }
|
||||
// });
|
||||
// pushCurrentItem();
|
||||
// return result;
|
||||
// }
|
||||
|
||||
// let toc = [];
|
||||
// state.sectionDescList.cl_each((sectionDesc) => {
|
||||
// if (sectionDesc.heading) {
|
||||
// toc.push({
|
||||
// title: sectionDesc.heading.title,
|
||||
// level: sectionDesc.heading.level,
|
||||
// anchor: sectionDesc.heading.anchor,
|
||||
// children: [],
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// toc = groupToc(toc);
|
||||
|
||||
// const view = {
|
||||
// file: {
|
||||
// name: rootState.files.currentFile.name,
|
||||
// content: {
|
||||
// properties: rootState.files.currentFile.content.properties,
|
||||
// text: rootState.files.currentFile.content.text,
|
||||
// html: state.previewHtml,
|
||||
// toc,
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
// const worker = new window.Worker(clVersion.getAssetPath('templateWorker-min.js'));
|
||||
// worker.postMessage([template, view, clSettingSvc.values.handlebarsHelpers]);
|
||||
// return new Promise((resolve, reject) => {
|
||||
// worker.addEventListener('message', (e) => {
|
||||
// resolve(e.data.toString());
|
||||
// });
|
||||
// setTimeout(() => {
|
||||
// worker.terminate();
|
||||
// reject('Template generation timeout.');
|
||||
// }, 10000);
|
||||
// });
|
||||
// },
|
||||
|
||||
/**
|
||||
* Pass the elements to the store and initialize the editor.
|
||||
*/
|
||||
init(editorElt, previewElt, tocElt) {
|
||||
this.editorElt = editorElt;
|
||||
this.previewElt = previewElt;
|
||||
this.tocElt = tocElt;
|
||||
|
||||
editorEngineSvc.createClEditor(editorElt);
|
||||
editorEngineSvc.clEditor.toggleEditable(false);
|
||||
editorEngineSvc.clEditor.on('contentChanged', (content, diffs, sectionList) => {
|
||||
const parsingCtx = {
|
||||
...this.parsingCtx,
|
||||
sectionList,
|
||||
};
|
||||
this.parsingCtx = parsingCtx;
|
||||
});
|
||||
this.pagedownEditor = pagedown({
|
||||
input: Object.create(editorEngineSvc.clEditor),
|
||||
});
|
||||
this.pagedownEditor.run();
|
||||
this.editorElt.addEventListener('focus', () => {
|
||||
// if (clEditorLayoutSvc.currentControl === 'menu') {
|
||||
// clEditorLayoutSvc.currentControl = undefined
|
||||
// }
|
||||
});
|
||||
// state.pagedownEditor.hooks.set('insertLinkDialog', (callback) => {
|
||||
// clEditorSvc.linkDialogCallback = callback
|
||||
// clEditorLayoutSvc.currentControl = 'linkDialog'
|
||||
// scope.$evalAsync()
|
||||
// return true
|
||||
// })
|
||||
// state.pagedownEditor.hooks.set('insertImageDialog', (callback) => {
|
||||
// clEditorSvc.imageDialogCallback = callback
|
||||
// clEditorLayoutSvc.currentControl = 'imageDialog'
|
||||
// scope.$evalAsync()
|
||||
// return true
|
||||
// })
|
||||
|
||||
this.editorElt.addEventListener('scroll', () => this.saveContentState(true));
|
||||
|
||||
const refreshPreview = () => {
|
||||
this.convert();
|
||||
if (!isPreviewRefreshed) {
|
||||
this.refreshPreview();
|
||||
this.measureSectionDimensions();
|
||||
this.restoreScrollPosition();
|
||||
} else {
|
||||
setTimeout(() => this.refreshPreview(), 10);
|
||||
}
|
||||
isPreviewRefreshed = true;
|
||||
};
|
||||
|
||||
const debouncedRefreshPreview = debounce(refreshPreview, 20);
|
||||
|
||||
let newSectionList;
|
||||
let newSelectionRange;
|
||||
const debouncedEditorChanged = debounce(() => {
|
||||
if (this.sectionList !== newSectionList) {
|
||||
this.sectionList = newSectionList;
|
||||
this.$emit('sectionList', this.sectionList);
|
||||
if (isPreviewRefreshed) {
|
||||
debouncedRefreshPreview();
|
||||
} else {
|
||||
refreshPreview();
|
||||
}
|
||||
}
|
||||
if (this.selectionRange !== newSelectionRange) {
|
||||
this.selectionRange = newSelectionRange;
|
||||
this.$emit('selectionRange', this.selectionRange);
|
||||
}
|
||||
this.saveContentState();
|
||||
}, 10);
|
||||
|
||||
editorEngineSvc.clEditor.selectionMgr.on('selectionChanged', (start, end, selectionRange) => {
|
||||
newSelectionRange = selectionRange;
|
||||
debouncedEditorChanged();
|
||||
});
|
||||
|
||||
/* -----------------------------
|
||||
* Inline images
|
||||
*/
|
||||
|
||||
const imgCache = Object.create(null);
|
||||
|
||||
const addToImgCache = (imgElt) => {
|
||||
let entries = imgCache[imgElt.src];
|
||||
if (!entries) {
|
||||
entries = [];
|
||||
imgCache[imgElt.src] = entries;
|
||||
}
|
||||
entries.push(imgElt);
|
||||
};
|
||||
|
||||
const getFromImgCache = (src) => {
|
||||
const entries = imgCache[src];
|
||||
if (!entries) {
|
||||
return null;
|
||||
}
|
||||
let imgElt;
|
||||
return entries
|
||||
.some((entry) => {
|
||||
if (this.editorElt.contains(entry)) {
|
||||
return false;
|
||||
}
|
||||
imgElt = entry;
|
||||
return true;
|
||||
}) && imgElt;
|
||||
};
|
||||
|
||||
const triggerImgCacheGc = debounce(() => {
|
||||
Object.keys(imgCache).forEach((src) => {
|
||||
const entries = imgCache[src]
|
||||
.filter(imgElt => this.editorElt.contains(imgElt));
|
||||
if (entries.length) {
|
||||
imgCache[src] = entries;
|
||||
} else {
|
||||
delete imgCache[src];
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
|
||||
let imgEltsToCache = [];
|
||||
if (store.state.editor.inlineImages) {
|
||||
editorEngineSvc.clEditor.highlighter.on('sectionHighlighted', (section) => {
|
||||
section.elt.getElementsByClassName('token img').cl_each((imgTokenElt) => {
|
||||
const srcElt = imgTokenElt.querySelector('.token.cl-src');
|
||||
if (srcElt) {
|
||||
// Create an img element before the .img.token and wrap both elements
|
||||
// into a .token.img-wrapper
|
||||
const imgElt = document.createElement('img');
|
||||
imgElt.style.display = 'none';
|
||||
const uri = srcElt.textContent;
|
||||
if (!/^unsafe/.test(htmlSanitizer.sanitizeUri(uri, true))) {
|
||||
imgElt.onload = () => {
|
||||
imgElt.style.display = '';
|
||||
};
|
||||
imgElt.src = uri;
|
||||
imgEltsToCache.push(imgElt);
|
||||
}
|
||||
const imgTokenWrapper = document.createElement('span');
|
||||
imgTokenWrapper.className = 'token img-wrapper';
|
||||
imgTokenElt.parentNode.insertBefore(imgTokenWrapper, imgTokenElt);
|
||||
imgTokenWrapper.appendChild(imgElt);
|
||||
imgTokenWrapper.appendChild(imgTokenElt);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
editorEngineSvc.clEditor.highlighter.on('highlighted', () => {
|
||||
imgEltsToCache.forEach((imgElt) => {
|
||||
const cachedImgElt = getFromImgCache(imgElt.src);
|
||||
if (cachedImgElt) {
|
||||
// Found a previously loaded image that has just been released
|
||||
imgElt.parentNode.replaceChild(cachedImgElt, imgElt);
|
||||
} else {
|
||||
addToImgCache(imgElt);
|
||||
}
|
||||
});
|
||||
imgEltsToCache = [];
|
||||
// Eject released images from cache
|
||||
triggerImgCacheGc();
|
||||
});
|
||||
|
||||
editorEngineSvc.clEditor.on('contentChanged', (content, diffs, sectionList) => {
|
||||
newSectionList = sectionList;
|
||||
debouncedEditorChanged();
|
||||
});
|
||||
|
||||
// scope.$watch('editorLayoutSvc.isEditorOpen', function (isOpen) {
|
||||
// clEditorSvc.cledit.toggleEditable(isOpen)
|
||||
// })
|
||||
|
||||
// scope.$watch('editorLayoutSvc.currentControl', function (currentControl) {
|
||||
// !currentControl && setTimeout(function () {
|
||||
// !scope.isDialogOpen && clEditorSvc.cledit && clEditorSvc.cledit.focus()
|
||||
// }, 1)
|
||||
// })
|
||||
|
||||
// clEditorSvc.setPreviewElt(element[0].querySelector('.preview__inner'))
|
||||
// var previewElt = element[0].querySelector('.preview')
|
||||
// clEditorSvc.isPreviewTop = previewElt.scrollTop < 10
|
||||
// previewElt.addEventListener('scroll', function () {
|
||||
// var isPreviewTop = previewElt.scrollTop < 10
|
||||
// if (isPreviewTop !== clEditorSvc.isPreviewTop) {
|
||||
// clEditorSvc.isPreviewTop = isPreviewTop
|
||||
// scope.$apply()
|
||||
// }
|
||||
// })
|
||||
|
||||
store.watch(
|
||||
state => state.files.currentFile.content.properties,
|
||||
(properties) => {
|
||||
const options = properties && extensionSvc.getOptions(properties, true);
|
||||
if (JSON.stringify(options) !== JSON.stringify(editorSvc.options)) {
|
||||
editorSvc.options = options;
|
||||
editorSvc.initPrism();
|
||||
editorSvc.initConverter();
|
||||
editorSvc.initClEditor();
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
store.watch(state => `${state.layout.editorWidth},${state.layout.previewWidth}`,
|
||||
() => editorSvc.measureSectionDimensions(true));
|
||||
store.watch(state => state.layout.showSidePreview,
|
||||
showSidePreview => showSidePreview && editorSvc.measureSectionDimensions());
|
||||
|
||||
this.$emit('inited');
|
||||
},
|
||||
});
|
||||
|
||||
export default editorSvc;
|
46
src/services/extensionSvc.js
Normal file
46
src/services/extensionSvc.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
const getOptionsListeners = [];
|
||||
const initConverterListeners = [];
|
||||
const sectionPreviewListeners = [];
|
||||
const asyncPreviewListeners = [];
|
||||
|
||||
export default {
|
||||
onGetOptions(listener) {
|
||||
getOptionsListeners.push(listener);
|
||||
},
|
||||
|
||||
onInitConverter(priority, listener) {
|
||||
initConverterListeners[priority] = listener;
|
||||
},
|
||||
|
||||
onSectionPreview(listener) {
|
||||
sectionPreviewListeners.push(listener);
|
||||
},
|
||||
|
||||
onAsyncPreview(listener) {
|
||||
asyncPreviewListeners.push(listener);
|
||||
},
|
||||
|
||||
getOptions(properties, isCurrentFile) {
|
||||
return getOptionsListeners.reduce((options, listener) => {
|
||||
listener(options, properties, isCurrentFile);
|
||||
return options;
|
||||
}, {});
|
||||
},
|
||||
|
||||
initConverter(markdown, options, isCurrentFile) {
|
||||
// Use forEach as it's a sparsed array
|
||||
initConverterListeners.forEach((listener) => {
|
||||
listener(markdown, options, isCurrentFile);
|
||||
});
|
||||
},
|
||||
|
||||
sectionPreview(elt, options) {
|
||||
sectionPreviewListeners.forEach((listener) => {
|
||||
listener(elt, options);
|
||||
});
|
||||
},
|
||||
|
||||
asyncPreview(options) {
|
||||
return Promise.all(asyncPreviewListeners.map(listener => listener(options)));
|
||||
},
|
||||
};
|
262
src/services/markdownConversionSvc.js
Normal file
262
src/services/markdownConversionSvc.js
Normal file
|
@ -0,0 +1,262 @@
|
|||
import DiffMatchPatch from 'diff-match-patch';
|
||||
import Prism from 'prismjs';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import markdownGrammarSvc from './markdownGrammarSvc';
|
||||
import extensionSvc from './extensionSvc';
|
||||
|
||||
const htmlSectionMarker = '\uF111\uF222\uF333\uF444';
|
||||
const diffMatchPatch = new DiffMatchPatch();
|
||||
|
||||
// Create aliases for syntax highlighting
|
||||
const languageAliases = ({
|
||||
js: 'javascript',
|
||||
json: 'javascript',
|
||||
html: 'markup',
|
||||
svg: 'markup',
|
||||
xml: 'markup',
|
||||
py: 'python',
|
||||
rb: 'ruby',
|
||||
ps1: 'powershell',
|
||||
psm1: 'powershell',
|
||||
});
|
||||
Object.keys(languageAliases).forEach((alias) => {
|
||||
const language = languageAliases[alias];
|
||||
Prism.languages[alias] = Prism.languages[language];
|
||||
});
|
||||
|
||||
// Add language parsing capability to markdown fences
|
||||
const insideFences = {};
|
||||
Object.keys(Prism.languages).forEach((name) => {
|
||||
const language = Prism.languages[name];
|
||||
if (Prism.util.type(language) === 'Object') {
|
||||
insideFences[`language-${name}`] = {
|
||||
pattern: new RegExp(`(\`\`\`|~~~)${name}\\W[\\s\\S]*`),
|
||||
inside: {
|
||||
'cl cl-pre': /(```|~~~).*/,
|
||||
rest: language,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Disable spell checking in specific tokens
|
||||
const noSpellcheckTokens = Object.create(null);
|
||||
[
|
||||
'code',
|
||||
'pre',
|
||||
'pre gfm',
|
||||
'math block',
|
||||
'math inline',
|
||||
'math expr block',
|
||||
'math expr inline',
|
||||
'latex block',
|
||||
]
|
||||
.forEach((key) => {
|
||||
noSpellcheckTokens[key] = true;
|
||||
});
|
||||
Prism.hooks.add('wrap', (env) => {
|
||||
if (noSpellcheckTokens[env.type]) {
|
||||
env.attributes.spellcheck = 'false';
|
||||
}
|
||||
});
|
||||
|
||||
function createFlagMap(arr) {
|
||||
return arr.reduce((map, type) => ({ ...map, [type]: true }), {});
|
||||
}
|
||||
const startSectionBlockTypeMap = createFlagMap([
|
||||
'paragraph_open',
|
||||
'blockquote_open',
|
||||
'heading_open',
|
||||
'code',
|
||||
'fence',
|
||||
'table_open',
|
||||
'html_block',
|
||||
'bullet_list_open',
|
||||
'ordered_list_open',
|
||||
'hr',
|
||||
'dl_open',
|
||||
]);
|
||||
const listBlockTypeMap = createFlagMap([
|
||||
'bullet_list_open',
|
||||
'ordered_list_open',
|
||||
]);
|
||||
const blockquoteBlockTypeMap = createFlagMap([
|
||||
'blockquote_open',
|
||||
]);
|
||||
const tableBlockTypeMap = createFlagMap([
|
||||
'table_open',
|
||||
]);
|
||||
const deflistBlockTypeMap = createFlagMap([
|
||||
'dl_open',
|
||||
]);
|
||||
|
||||
function hashArray(arr, valueHash, valueArray) {
|
||||
const hash = [];
|
||||
arr.cl_each((str) => {
|
||||
let strHash = valueHash[str];
|
||||
if (strHash === undefined) {
|
||||
strHash = valueArray.length;
|
||||
valueArray.push(str);
|
||||
valueHash[str] = strHash;
|
||||
}
|
||||
hash.push(strHash);
|
||||
});
|
||||
return String.fromCharCode.apply(null, hash);
|
||||
}
|
||||
|
||||
// Default options for the markdown converter and the grammar
|
||||
const defaultOptions = {
|
||||
abbr: true,
|
||||
breaks: true,
|
||||
deflist: true,
|
||||
del: true,
|
||||
fence: true,
|
||||
footnote: true,
|
||||
linkify: true,
|
||||
math: true,
|
||||
sub: true,
|
||||
sup: true,
|
||||
table: true,
|
||||
typographer: true,
|
||||
insideFences,
|
||||
};
|
||||
|
||||
const markdownConversionSvc = {
|
||||
defaultOptions,
|
||||
|
||||
/**
|
||||
* Creates a converter and init it with extensions.
|
||||
* @returns {Object} A converter.
|
||||
*/
|
||||
createConverter(options, isCurrentFile) {
|
||||
// Let the listeners add the rules
|
||||
const converter = new MarkdownIt('zero');
|
||||
converter.core.ruler.enable([], true);
|
||||
converter.block.ruler.enable([], true);
|
||||
converter.inline.ruler.enable([], true);
|
||||
extensionSvc.initConverter(converter, options, isCurrentFile);
|
||||
Object.keys(startSectionBlockTypeMap).forEach((type) => {
|
||||
const rule = converter.renderer.rules[type] === true || converter.renderer.renderToken;
|
||||
converter.renderer.rules[type] = (tokens, idx, opts) => {
|
||||
if (tokens[idx].sectionDelimiter) {
|
||||
// Add section delimiter
|
||||
return htmlSectionMarker + rule.call(converter.renderer, tokens, idx, opts);
|
||||
}
|
||||
return rule.call(converter.renderer, tokens, idx, opts);
|
||||
};
|
||||
});
|
||||
return converter;
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse markdown sections by passing the 2 first block rules of the markdown-it converter.
|
||||
* @param {Object} converter The markdown-it converter.
|
||||
* @param {String} text The text to be parsed.
|
||||
* @returns {Object} A parsing context to be passed to `convert`.
|
||||
*/
|
||||
parseSections(converter, text) {
|
||||
const markdownState = new converter.core.State(text, converter, {});
|
||||
const markdownCoreRules = converter.core.ruler.getRules('');
|
||||
markdownCoreRules[0](markdownState); // Pass the normalize rule
|
||||
markdownCoreRules[1](markdownState); // Pass the block rule
|
||||
const lines = text.split('\n');
|
||||
if (!lines[lines.length - 1]) {
|
||||
// In cledit, last char is always '\n'.
|
||||
// Remove it as one will be added by addSection
|
||||
lines.pop();
|
||||
}
|
||||
const parsingCtx = {
|
||||
sections: [],
|
||||
converter,
|
||||
markdownState,
|
||||
markdownCoreRules,
|
||||
};
|
||||
let data = 'main';
|
||||
let i = 0;
|
||||
|
||||
function addSection(maxLine) {
|
||||
const section = {
|
||||
text: '',
|
||||
data,
|
||||
};
|
||||
for (; i < maxLine; i += 1) {
|
||||
section.text += `${lines[i]}\n`;
|
||||
}
|
||||
if (section) {
|
||||
parsingCtx.sections.push(section);
|
||||
}
|
||||
}
|
||||
markdownState.tokens.forEach((token, index) => {
|
||||
// index === 0 means there are empty lines at the begining of the file
|
||||
if (token.level === 0 && startSectionBlockTypeMap[token.type] === true) {
|
||||
if (index > 0) {
|
||||
token.sectionDelimiter = true;
|
||||
addSection(token.map[0]);
|
||||
}
|
||||
if (listBlockTypeMap[token.type] === true) {
|
||||
data = 'list';
|
||||
} else if (blockquoteBlockTypeMap[token.type] === true) {
|
||||
data = 'blockquote';
|
||||
} else if (tableBlockTypeMap[token.type] === true) {
|
||||
data = 'table';
|
||||
} else if (deflistBlockTypeMap[token.type] === true) {
|
||||
data = 'deflist';
|
||||
} else {
|
||||
data = 'main';
|
||||
}
|
||||
}
|
||||
});
|
||||
addSection(lines.length);
|
||||
return parsingCtx;
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert markdown sections previously parsed with `parseSections`.
|
||||
* @param {Object} parsingCtx The parsing context returned by `parseSections`.
|
||||
* @param {Object} previousConversionCtx The conversion context returned by a previous call
|
||||
* to `convert`, in order to calculate the `htmlSectionDiff` of the returned conversion context.
|
||||
* @returns {Object} A conversion context.
|
||||
*/
|
||||
convert(parsingCtx, previousConversionCtx) {
|
||||
// This function can be called twice without editor modification
|
||||
// so prevent from converting it again.
|
||||
if (!parsingCtx.markdownState.isConverted) {
|
||||
// Skip 2 first rules previously passed in parseSections
|
||||
parsingCtx.markdownCoreRules.slice(2).forEach(rule => rule(parsingCtx.markdownState));
|
||||
parsingCtx.markdownState.isConverted = true;
|
||||
}
|
||||
const tokens = parsingCtx.markdownState.tokens;
|
||||
const html = parsingCtx.converter.renderer.render(
|
||||
tokens,
|
||||
parsingCtx.converter.options,
|
||||
parsingCtx.markdownState.env,
|
||||
);
|
||||
const htmlSectionList = html.split(htmlSectionMarker);
|
||||
if (htmlSectionList[0] === '') {
|
||||
htmlSectionList.shift();
|
||||
}
|
||||
const valueHash = Object.create(null);
|
||||
const valueArray = [];
|
||||
const newSectionHash = hashArray(htmlSectionList, valueHash, valueArray);
|
||||
let htmlSectionDiff;
|
||||
if (previousConversionCtx) {
|
||||
const oldSectionHash = hashArray(
|
||||
previousConversionCtx.htmlSectionList, valueHash, valueArray);
|
||||
htmlSectionDiff = diffMatchPatch.diff_main(oldSectionHash, newSectionHash, false);
|
||||
} else {
|
||||
htmlSectionDiff = [
|
||||
[1, newSectionHash],
|
||||
];
|
||||
}
|
||||
return {
|
||||
sectionList: parsingCtx.sectionList,
|
||||
htmlSectionList,
|
||||
htmlSectionDiff,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
markdownConversionSvc.defaultConverter = markdownConversionSvc.createConverter(defaultOptions);
|
||||
markdownConversionSvc.defaultPrismGrammars = markdownGrammarSvc.makeGrammars(defaultOptions);
|
||||
|
||||
export default markdownConversionSvc;
|
414
src/services/markdownGrammarSvc.js
Normal file
414
src/services/markdownGrammarSvc.js
Normal file
|
@ -0,0 +1,414 @@
|
|||
const charInsideUrl = '(&|[-A-Z0-9+@#/%?=~_|[\\]()!:,.;])';
|
||||
const charEndingUrl = '(&|[-A-Z0-9+@#/%=~_|[\\])])';
|
||||
const urlPattern = new RegExp(`(https?|ftp)(://${charInsideUrl}*${charEndingUrl})(?=$|\\W)`, 'gi');
|
||||
const emailPattern = /(?:mailto:)?([-.\w]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)/gi;
|
||||
|
||||
const markup = {
|
||||
comment: /<!--[\w\W]*?-->/g,
|
||||
tag: {
|
||||
pattern: /<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|[^\s'">=]+))?\s*)*\/?>/gi,
|
||||
inside: {
|
||||
tag: {
|
||||
pattern: /^<\/?[\w:-]+/i,
|
||||
inside: {
|
||||
punctuation: /^<\/?/,
|
||||
namespace: /^[\w-]+?:/,
|
||||
},
|
||||
},
|
||||
'attr-value': {
|
||||
pattern: /=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi,
|
||||
inside: {
|
||||
punctuation: /=|>|"/g,
|
||||
},
|
||||
},
|
||||
punctuation: /\/?>/g,
|
||||
'attr-name': {
|
||||
pattern: /[\w:-]+/g,
|
||||
inside: {
|
||||
namespace: /^[\w-]+?:/,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
entity: /&#?[\da-z]{1,8};/gi,
|
||||
};
|
||||
|
||||
const latex = {
|
||||
// A tex command e.g. \foo
|
||||
keyword: /\\(?:[^a-zA-Z]|[a-zA-Z]+)/g,
|
||||
// Curly and square braces
|
||||
lparen: /[[({]/g,
|
||||
// Curly and square braces
|
||||
rparen: /[\])}]/g,
|
||||
// A comment. Tex comments start with % and go to
|
||||
// the end of the line
|
||||
comment: /%.*/g,
|
||||
};
|
||||
|
||||
export default {
|
||||
makeGrammars(options) {
|
||||
const grammars = {
|
||||
main: {},
|
||||
list: {},
|
||||
blockquote: {},
|
||||
table: {},
|
||||
deflist: {},
|
||||
};
|
||||
|
||||
grammars.deflist.deflist = {
|
||||
pattern: new RegExp(
|
||||
[
|
||||
'^ {0,3}\\S.*\\n', // Description line
|
||||
'(?:[ \\t]*\\n)?', // Optional empty line
|
||||
'(?:',
|
||||
'[ \\t]*:[ \\t].*\\n', // Colon line
|
||||
'(?:',
|
||||
'(?:',
|
||||
'.*\\S.*\\n', // Non-empty line
|
||||
'|',
|
||||
'[ \\t]*\\n(?! ?\\S)', // Or empty line not followed by unindented line
|
||||
')',
|
||||
')*',
|
||||
'(?:[ \\t]*\\n)*', // Empty lines
|
||||
')+',
|
||||
].join(''),
|
||||
'm',
|
||||
),
|
||||
inside: {
|
||||
term: /^.+/,
|
||||
cl: /^[ \t]*:[ \t]/gm,
|
||||
},
|
||||
};
|
||||
|
||||
const insideFences = options.insideFences || {};
|
||||
insideFences['cl cl-pre'] = /```|~~~/;
|
||||
if (options.fence) {
|
||||
grammars.main['pre gfm'] = {
|
||||
pattern: /^(```|~~~)[\s\S]*?\n\1 *$/gm,
|
||||
inside: insideFences,
|
||||
};
|
||||
grammars.list['pre gfm'] = {
|
||||
pattern: /^(?: {4}|\t)(```|~~~)[\s\S]*?\n(?: {4}|\t)\1\s*$/gm,
|
||||
inside: insideFences,
|
||||
};
|
||||
grammars.deflist.deflist.inside['pre gfm'] = grammars.list['pre gfm'];
|
||||
}
|
||||
|
||||
grammars.main['h1 alt'] = {
|
||||
pattern: /^.+\n=+[ \t]*$/gm,
|
||||
inside: {
|
||||
'cl cl-hash': /=+[ \t]*$/,
|
||||
},
|
||||
};
|
||||
grammars.main['h2 alt'] = {
|
||||
pattern: /^.+\n-+[ \t]*$/gm,
|
||||
inside: {
|
||||
'cl cl-hash': /-+[ \t]*$/,
|
||||
},
|
||||
};
|
||||
for (let i = 6; i >= 1; i -= 1) {
|
||||
grammars.main[`h${i}`] = {
|
||||
pattern: new RegExp(`^#{${i}}[ \t].+$`, 'gm'),
|
||||
inside: {
|
||||
'cl cl-hash': new RegExp(`^#{${i}}`),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const list = /^[ \t]*([*+-]|\d+\.)[ \t]/gm;
|
||||
const blockquote = {
|
||||
pattern: /^\s*>.*(?:\n[ \t]*\S.*)*/gm,
|
||||
inside: {
|
||||
'cl cl-gt': /^\s*>/gm,
|
||||
'cl cl-li': list,
|
||||
},
|
||||
};
|
||||
grammars.list.blockquote = blockquote;
|
||||
grammars.blockquote.blockquote = blockquote;
|
||||
grammars.deflist.deflist.inside.blockquote = blockquote;
|
||||
grammars.list['cl cl-li'] = list;
|
||||
grammars.blockquote['cl cl-li'] = list;
|
||||
grammars.deflist.deflist.inside['cl cl-li'] = list;
|
||||
|
||||
grammars.table.table = {
|
||||
pattern: new RegExp(
|
||||
[
|
||||
'^\\s*\\S.*[|].*\\n', // Header Row
|
||||
'[-| :]+\\n', // Separator
|
||||
'(?:.*[|].*\\n?)*', // Table rows
|
||||
'$',
|
||||
].join(''),
|
||||
'gm',
|
||||
),
|
||||
inside: {
|
||||
'cl cl-title-separator': /^[-| :]+$/gm,
|
||||
'cl cl-pipe': /[|]/gm,
|
||||
},
|
||||
};
|
||||
|
||||
grammars.main.hr = {
|
||||
pattern: /^ {0,3}([*\-_] *){3,}$/gm,
|
||||
};
|
||||
|
||||
const defs = {};
|
||||
if (options.footnote) {
|
||||
defs.fndef = {
|
||||
pattern: /^ {0,3}\[\^.*?\]:.*$/gm,
|
||||
inside: {
|
||||
'ref-id': {
|
||||
pattern: /^ {0,3}\[\^.*?\]/,
|
||||
inside: {
|
||||
cl: /(\[\^|\])/,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (options.abbr) {
|
||||
defs.abbrdef = {
|
||||
pattern: /^ {0,3}\*\[.*?\]:.*$/gm,
|
||||
inside: {
|
||||
'abbr-id': {
|
||||
pattern: /^ {0,3}\*\[.*?\]/,
|
||||
inside: {
|
||||
cl: /(\*\[|\])/,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
defs.linkdef = {
|
||||
pattern: /^ {0,3}\[.*?\]:.*$/gm,
|
||||
inside: {
|
||||
'link-id': {
|
||||
pattern: /^ {0,3}\[.*?\]/,
|
||||
inside: {
|
||||
cl: /[[\]]/,
|
||||
},
|
||||
},
|
||||
url: urlPattern,
|
||||
},
|
||||
};
|
||||
|
||||
Object.keys(defs).forEach((name) => {
|
||||
const def = defs[name];
|
||||
grammars.main[name] = def;
|
||||
grammars.list[name] = def;
|
||||
grammars.blockquote[name] = def;
|
||||
grammars.table[name] = def;
|
||||
grammars.deflist[name] = def;
|
||||
});
|
||||
|
||||
grammars.main.pre = {
|
||||
pattern: /^\s*\n(?: {4}|\t).*\S.*\n((?: {4}|\t).*\n)*/gm,
|
||||
};
|
||||
|
||||
const rest = {};
|
||||
rest.code = {
|
||||
pattern: /(`+)[\s\S]*?\1/g,
|
||||
inside: {
|
||||
'cl cl-code': /`/,
|
||||
},
|
||||
};
|
||||
if (options.math) {
|
||||
rest['math block'] = {
|
||||
pattern: /\\\\\[[\s\S]*?\\\\\]/g,
|
||||
inside: {
|
||||
'cl cl-bracket-start': /^\\\\\[/,
|
||||
'cl cl-bracket-end': /\\\\\]$/,
|
||||
rest: latex,
|
||||
},
|
||||
};
|
||||
rest['math inline'] = {
|
||||
pattern: /\\\\\([\s\S]*?\\\\\)/g,
|
||||
inside: {
|
||||
'cl cl-bracket-start': /^\\\\\(/,
|
||||
'cl cl-bracket-end': /\\\\\)$/,
|
||||
rest: latex,
|
||||
},
|
||||
};
|
||||
rest['math expr block'] = {
|
||||
pattern: /(\$\$)[\s\S]*?\1/g,
|
||||
inside: {
|
||||
'cl cl-bracket-start': /^\$\$/,
|
||||
'cl cl-bracket-end': /\$\$$/,
|
||||
rest: latex,
|
||||
},
|
||||
};
|
||||
rest['math expr inline'] = {
|
||||
pattern: /\$(?!\s)[\s\S]*?\S\$(?!\d)/g,
|
||||
inside: {
|
||||
'cl cl-bracket-start': /^\$/,
|
||||
'cl cl-bracket-end': /\$$/,
|
||||
rest: latex,
|
||||
},
|
||||
};
|
||||
rest['latex block'] = {
|
||||
pattern: /\\begin\{([a-z]*\*?)\}[\s\S]*?\\?\\end\{\1\}/g,
|
||||
inside: {
|
||||
keyword: /\\(begin|end)/,
|
||||
rest: latex,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (options.footnote) {
|
||||
rest.inlinefn = {
|
||||
pattern: /\^\[.+?\]/g,
|
||||
inside: {
|
||||
cl: /(\^\[|\])/,
|
||||
},
|
||||
};
|
||||
rest.fn = {
|
||||
pattern: /\[\^.+?\]/g,
|
||||
inside: {
|
||||
cl: /(\[\^|\])/,
|
||||
},
|
||||
};
|
||||
}
|
||||
rest.img = {
|
||||
pattern: /!\[.*?\]\(.+?\)/g,
|
||||
inside: {
|
||||
'cl cl-title': /['‘][^'’]*['’]|["“][^"”]*["”](?=\)$)/,
|
||||
'cl cl-src': {
|
||||
pattern: /(\]\()[^('" \t]+(?=[)'" \t])/,
|
||||
lookbehind: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
rest.link = {
|
||||
pattern: /\[.*?\]\(.+?\)/gm,
|
||||
inside: {
|
||||
'cl cl-underlined-text': {
|
||||
pattern: /(\[)[^\]]*/,
|
||||
lookbehind: true,
|
||||
},
|
||||
'cl cl-title': /['‘][^'’]*['’]|["“][^"”]*["”](?=\)$)/,
|
||||
},
|
||||
};
|
||||
rest.imgref = {
|
||||
pattern: /!\[.*?\][ \t]*\[.*?\]/g,
|
||||
};
|
||||
rest.linkref = {
|
||||
pattern: /\[.*?\][ \t]*\[.*?\]/g,
|
||||
inside: {
|
||||
'cl cl-underlined-text': {
|
||||
pattern: /^(\[)[^\]]*(?=\][ \t]*\[)/,
|
||||
lookbehind: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
rest.comment = markup.comment;
|
||||
rest.tag = markup.tag;
|
||||
rest.url = urlPattern;
|
||||
rest.email = emailPattern;
|
||||
rest.strong = {
|
||||
pattern: /(^|[^\w*])(__|\*\*)(?![_*])[\s\S]*?\2(?=([^\w*]|$))/gm,
|
||||
lookbehind: true,
|
||||
inside: {
|
||||
'cl cl-strong cl-start': /^(__|\*\*)/,
|
||||
'cl cl-strong cl-close': /(__|\*\*)$/,
|
||||
},
|
||||
};
|
||||
rest.em = {
|
||||
pattern: /(^|[^\w*])(_|\*)(?![_*])[\s\S]*?\2(?=([^\w*]|$))/gm,
|
||||
lookbehind: true,
|
||||
inside: {
|
||||
'cl cl-em cl-start': /^(_|\*)/,
|
||||
'cl cl-em cl-close': /(_|\*)$/,
|
||||
},
|
||||
};
|
||||
rest['strong em'] = {
|
||||
pattern: /(^|[^\w*])(__|\*\*)(_|\*)(?![_*])[\s\S]*?\3\2(?=([^\w*]|$))/gm,
|
||||
lookbehind: true,
|
||||
inside: {
|
||||
'cl cl-strong cl-start': /^(__|\*\*)(_|\*)/,
|
||||
'cl cl-strong cl-close': /(_|\*)(__|\*\*)$/,
|
||||
},
|
||||
};
|
||||
rest['strong em inv'] = {
|
||||
pattern: /(^|[^\w*])(_|\*)(__|\*\*)(?![_*])[\s\S]*?\3\2(?=([^\w*]|$))/gm,
|
||||
lookbehind: true,
|
||||
inside: {
|
||||
'cl cl-strong cl-start': /^(_|\*)(__|\*\*)/,
|
||||
'cl cl-strong cl-close': /(__|\*\*)(_|\*)$/,
|
||||
},
|
||||
};
|
||||
if (options.del) {
|
||||
rest.del = {
|
||||
pattern: /(^|[^\w*])(~~)[\s\S]*?\2(?=([^\w*]|$))/gm,
|
||||
lookbehind: true,
|
||||
inside: {
|
||||
cl: /~~/,
|
||||
'cl-del-text': /[^~]+/,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (options.sub) {
|
||||
rest.sub = {
|
||||
pattern: /(~)(?=\S)(.*?\S)\1/gm,
|
||||
inside: {
|
||||
cl: /~/,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (options.sup) {
|
||||
rest.sup = {
|
||||
pattern: /(\^)(?=\S)(.*?\S)\1/gm,
|
||||
inside: {
|
||||
cl: /\^/,
|
||||
},
|
||||
};
|
||||
}
|
||||
rest.entity = markup.entity;
|
||||
|
||||
for (let c = 6; c >= 1; c -= 1) {
|
||||
grammars.main[`h${c}`].inside.rest = rest;
|
||||
}
|
||||
grammars.main['h1 alt'].inside.rest = rest;
|
||||
grammars.main['h2 alt'].inside.rest = rest;
|
||||
grammars.table.table.inside.rest = rest;
|
||||
grammars.main.rest = rest;
|
||||
grammars.list.rest = rest;
|
||||
grammars.blockquote.blockquote.inside.rest = rest;
|
||||
grammars.deflist.deflist.inside.rest = rest;
|
||||
if (options.footnote) {
|
||||
grammars.main.fndef.inside.rest = rest;
|
||||
}
|
||||
|
||||
const restLight = {
|
||||
code: rest.code,
|
||||
inlinefn: rest.inlinefn,
|
||||
fn: rest.fn,
|
||||
link: rest.link,
|
||||
linkref: rest.linkref,
|
||||
};
|
||||
rest.strong.inside.rest = restLight;
|
||||
rest.em.inside.rest = restLight;
|
||||
if (options.del) {
|
||||
rest.del.inside.rest = restLight;
|
||||
}
|
||||
|
||||
const inside = {
|
||||
code: rest.code,
|
||||
comment: rest.comment,
|
||||
tag: rest.tag,
|
||||
strong: rest.strong,
|
||||
em: rest.em,
|
||||
del: rest.del,
|
||||
sub: rest.sub,
|
||||
sup: rest.sup,
|
||||
entity: markup.entity,
|
||||
};
|
||||
rest.link.inside['cl cl-underlined-text'].inside = inside;
|
||||
rest.linkref.inside['cl cl-underlined-text'].inside = inside;
|
||||
|
||||
// Wrap any other characters to allow paragraph folding
|
||||
Object.keys(grammars).forEach((key) => {
|
||||
const grammar = grammars[key];
|
||||
grammar.rest = grammar.rest || {};
|
||||
grammar.rest.p = /.+/;
|
||||
});
|
||||
|
||||
return grammars;
|
||||
},
|
||||
};
|
1
src/services/optional/index.js
Normal file
1
src/services/optional/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
import './scrollSync';
|
185
src/services/optional/scrollSync.js
Normal file
185
src/services/optional/scrollSync.js
Normal file
|
@ -0,0 +1,185 @@
|
|||
import store from '../../store';
|
||||
import constants from '../constants';
|
||||
import animationSvc from '../animationSvc';
|
||||
import editorSvc from '../editorSvc';
|
||||
|
||||
let editorScrollerElt;
|
||||
let previewScrollerElt;
|
||||
let previewElt;
|
||||
let editorFinishTimeoutId;
|
||||
let previewFinishTimeoutId;
|
||||
let skipAnimation;
|
||||
let isScrollEditor;
|
||||
let isScrollPreview;
|
||||
let isEditorMoving;
|
||||
let isPreviewMoving;
|
||||
let sectionDescList;
|
||||
|
||||
let throttleTimeoutId;
|
||||
let throttleLastTime = 0;
|
||||
|
||||
function throttle(func, wait) {
|
||||
clearTimeout(throttleTimeoutId);
|
||||
const currentTime = Date.now();
|
||||
const localWait = (wait + throttleLastTime) - currentTime;
|
||||
if (localWait < 1) {
|
||||
throttleLastTime = currentTime;
|
||||
func();
|
||||
} else {
|
||||
throttleTimeoutId = setTimeout(() => {
|
||||
throttleLastTime = Date.now();
|
||||
func();
|
||||
}, localWait);
|
||||
}
|
||||
}
|
||||
|
||||
const doScrollSync = () => {
|
||||
const localSkipAnimation = skipAnimation
|
||||
|| !store.state.layout.showSidePreview
|
||||
|| !store.state.layout.showEditor;
|
||||
skipAnimation = false;
|
||||
if (!store.state.editor.scrollSync || !sectionDescList || sectionDescList.length === 0) {
|
||||
return;
|
||||
}
|
||||
let editorScrollTop = editorScrollerElt.scrollTop;
|
||||
if (editorScrollTop < 0) {
|
||||
editorScrollTop = 0;
|
||||
}
|
||||
let previewScrollTop = previewScrollerElt.scrollTop;
|
||||
let scrollTo;
|
||||
if (isScrollEditor) {
|
||||
// Scroll the preview
|
||||
isScrollEditor = false;
|
||||
editorScrollTop += constants.scrollOffset;
|
||||
sectionDescList.some((sectionDesc) => {
|
||||
if (editorScrollTop > sectionDesc.editorDimension.endOffset) {
|
||||
return false;
|
||||
}
|
||||
const posInSection = (editorScrollTop - sectionDesc.editorDimension.startOffset)
|
||||
/ (sectionDesc.editorDimension.height || 1);
|
||||
scrollTo = (sectionDesc.previewDimension.startOffset
|
||||
+ (sectionDesc.previewDimension.height * posInSection)) - constants.scrollOffset;
|
||||
return true;
|
||||
});
|
||||
scrollTo = Math.min(
|
||||
scrollTo,
|
||||
previewScrollerElt.scrollHeight - previewScrollerElt.offsetHeight,
|
||||
);
|
||||
|
||||
throttle(() => {
|
||||
clearTimeout(previewFinishTimeoutId);
|
||||
animationSvc.animate(previewScrollerElt)
|
||||
.scrollTop(scrollTo)
|
||||
.duration(!localSkipAnimation && 100)
|
||||
.start(() => {
|
||||
previewFinishTimeoutId = setTimeout(() => {
|
||||
isPreviewMoving = false;
|
||||
}, 100);
|
||||
}, () => {
|
||||
isPreviewMoving = true;
|
||||
});
|
||||
}, localSkipAnimation ? 500 : 10);
|
||||
} else if (!store.state.layout.showEditor || isScrollPreview) {
|
||||
// Scroll the editor
|
||||
isScrollPreview = false;
|
||||
previewScrollTop += constants.scrollOffset;
|
||||
sectionDescList.some((sectionDesc) => {
|
||||
if (previewScrollTop > sectionDesc.previewDimension.endOffset) {
|
||||
return false;
|
||||
}
|
||||
const posInSection = (previewScrollTop - sectionDesc.previewDimension.startOffset)
|
||||
/ (sectionDesc.previewDimension.height || 1);
|
||||
scrollTo = (sectionDesc.editorDimension.startOffset
|
||||
+ (sectionDesc.editorDimension.height * posInSection)) - constants.scrollOffset;
|
||||
return true;
|
||||
});
|
||||
scrollTo = Math.min(
|
||||
scrollTo,
|
||||
editorScrollerElt.scrollHeight - editorScrollerElt.offsetHeight,
|
||||
);
|
||||
|
||||
throttle(() => {
|
||||
clearTimeout(editorFinishTimeoutId);
|
||||
animationSvc.animate(editorScrollerElt)
|
||||
.scrollTop(scrollTo)
|
||||
.duration(!localSkipAnimation && 100)
|
||||
.start(() => {
|
||||
editorFinishTimeoutId = setTimeout(() => {
|
||||
isEditorMoving = false;
|
||||
}, 100);
|
||||
}, () => {
|
||||
isEditorMoving = true;
|
||||
});
|
||||
}, localSkipAnimation ? 500 : 10);
|
||||
}
|
||||
};
|
||||
|
||||
let isPreviewRefreshing;
|
||||
let timeoutId;
|
||||
|
||||
const forceScrollSync = () => {
|
||||
if (!isPreviewRefreshing) {
|
||||
doScrollSync(!store.state.layout.showSidePreview);
|
||||
}
|
||||
};
|
||||
store.watch(state => state.editor.scrollSync, forceScrollSync);
|
||||
|
||||
editorSvc.$on('inited', () => {
|
||||
editorScrollerElt = editorSvc.editorElt.parentNode;
|
||||
previewScrollerElt = editorSvc.previewElt.parentNode;
|
||||
previewElt = editorSvc.previewElt;
|
||||
|
||||
editorScrollerElt.addEventListener('scroll', () => {
|
||||
if (isEditorMoving) {
|
||||
return;
|
||||
}
|
||||
isScrollEditor = true;
|
||||
isScrollPreview = false;
|
||||
doScrollSync(!store.state.layout.showSidePreview);
|
||||
});
|
||||
|
||||
previewScrollerElt.addEventListener('scroll', () => {
|
||||
if (isPreviewMoving || isPreviewRefreshing) {
|
||||
return;
|
||||
}
|
||||
isScrollPreview = true;
|
||||
isScrollEditor = false;
|
||||
doScrollSync(!store.state.layout.showSidePreview);
|
||||
});
|
||||
});
|
||||
|
||||
editorSvc.$on('sectionList', () => {
|
||||
clearTimeout(timeoutId);
|
||||
isPreviewRefreshing = true;
|
||||
sectionDescList = undefined;
|
||||
});
|
||||
|
||||
editorSvc.$on('conversionCtx', () => {
|
||||
// Set the preview height to prevent scrollbar from jumping
|
||||
previewElt.style.height = `${previewElt.offsetHeight}px`;
|
||||
});
|
||||
|
||||
editorSvc.$on('previewText', () => {
|
||||
// Remove height property once the preview as been refreshed
|
||||
previewElt.style.removeProperty('height');
|
||||
// Assume the user is writing in the editor
|
||||
isScrollEditor = store.state.layout.showEditor;
|
||||
// A preview scrolling event can occur if height is smaller
|
||||
timeoutId = setTimeout(() => {
|
||||
isPreviewRefreshing = false;
|
||||
}, 100);
|
||||
});
|
||||
|
||||
store.watch(state => state.layout.showSidePreview,
|
||||
(showSidePreview) => {
|
||||
if (showSidePreview) {
|
||||
isScrollEditor = true;
|
||||
isScrollPreview = false;
|
||||
skipAnimation = true;
|
||||
}
|
||||
});
|
||||
|
||||
editorSvc.$on('sectionDescMeasuredList', (sectionDescMeasuredList) => {
|
||||
sectionDescList = sectionDescMeasuredList;
|
||||
forceScrollSync();
|
||||
});
|
104
src/services/sectionUtils.js
Normal file
104
src/services/sectionUtils.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
function SectionDimension(startOffset, endOffset) {
|
||||
this.startOffset = startOffset;
|
||||
this.endOffset = endOffset;
|
||||
this.height = endOffset - startOffset;
|
||||
}
|
||||
|
||||
function dimensionNormalizer(dimensionName) {
|
||||
return (editorSvc) => {
|
||||
const dimensionList = editorSvc.sectionDescList.map(sectionDesc => sectionDesc[dimensionName]);
|
||||
let dimension;
|
||||
let i;
|
||||
let j;
|
||||
for (i = 0; i < dimensionList.length; i += 1) {
|
||||
dimension = dimensionList[i];
|
||||
if (dimension.height) {
|
||||
for (j = i + 1; j < dimensionList.length && dimensionList[j].height === 0; j += 1) {
|
||||
// Loop
|
||||
}
|
||||
const normalizeFactor = j - i;
|
||||
if (normalizeFactor !== 1) {
|
||||
const normalizedHeight = dimension.height / normalizeFactor;
|
||||
dimension.height = normalizedHeight;
|
||||
dimension.endOffset = dimension.startOffset + dimension.height;
|
||||
for (j = i + 1; j < i + normalizeFactor; j += 1) {
|
||||
const startOffset = dimension.endOffset;
|
||||
dimension = dimensionList[j];
|
||||
dimension.startOffset = startOffset;
|
||||
dimension.height = normalizedHeight;
|
||||
dimension.endOffset = dimension.startOffset + dimension.height;
|
||||
}
|
||||
i = j - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const normalizeEditorDimensions = dimensionNormalizer('editorDimension');
|
||||
const normalizePreviewDimensions = dimensionNormalizer('previewDimension');
|
||||
const normalizeTocDimensions = dimensionNormalizer('tocDimension');
|
||||
|
||||
function measureSectionDimensions(editorSvc) {
|
||||
let editorSectionOffset = 0;
|
||||
let previewSectionOffset = 0;
|
||||
let tocSectionOffset = 0;
|
||||
let sectionDesc = editorSvc.sectionDescList[0];
|
||||
let nextSectionDesc;
|
||||
let i = 1;
|
||||
for (; i < editorSvc.sectionDescList.length; i += 1) {
|
||||
nextSectionDesc = editorSvc.sectionDescList[i];
|
||||
|
||||
// Measure editor section
|
||||
let newEditorSectionOffset = nextSectionDesc.editorElt
|
||||
? nextSectionDesc.editorElt.offsetTop
|
||||
: editorSectionOffset;
|
||||
newEditorSectionOffset = newEditorSectionOffset > editorSectionOffset
|
||||
? newEditorSectionOffset
|
||||
: editorSectionOffset;
|
||||
sectionDesc.editorDimension = new SectionDimension(editorSectionOffset, newEditorSectionOffset);
|
||||
editorSectionOffset = newEditorSectionOffset;
|
||||
|
||||
// Measure preview section
|
||||
let newPreviewSectionOffset = nextSectionDesc.previewElt
|
||||
? nextSectionDesc.previewElt.offsetTop
|
||||
: previewSectionOffset;
|
||||
newPreviewSectionOffset = newPreviewSectionOffset > previewSectionOffset
|
||||
? newPreviewSectionOffset
|
||||
: previewSectionOffset;
|
||||
sectionDesc.previewDimension = new SectionDimension(
|
||||
previewSectionOffset, newPreviewSectionOffset);
|
||||
previewSectionOffset = newPreviewSectionOffset;
|
||||
|
||||
// Measure TOC section
|
||||
let newTocSectionOffset = nextSectionDesc.tocElt
|
||||
? nextSectionDesc.tocElt.offsetTop + (nextSectionDesc.tocElt.offsetHeight / 2)
|
||||
: tocSectionOffset;
|
||||
newTocSectionOffset = newTocSectionOffset > tocSectionOffset
|
||||
? newTocSectionOffset
|
||||
: tocSectionOffset;
|
||||
sectionDesc.tocDimension = new SectionDimension(tocSectionOffset, newTocSectionOffset);
|
||||
tocSectionOffset = newTocSectionOffset;
|
||||
|
||||
sectionDesc = nextSectionDesc;
|
||||
}
|
||||
|
||||
// Last section
|
||||
sectionDesc = editorSvc.sectionDescList[i - 1];
|
||||
if (sectionDesc) {
|
||||
sectionDesc.editorDimension = new SectionDimension(
|
||||
editorSectionOffset, editorSvc.editorElt.scrollHeight);
|
||||
sectionDesc.previewDimension = new SectionDimension(
|
||||
previewSectionOffset, editorSvc.previewElt.scrollHeight);
|
||||
sectionDesc.tocDimension = new SectionDimension(
|
||||
tocSectionOffset, editorSvc.tocElt.scrollHeight);
|
||||
}
|
||||
|
||||
normalizeEditorDimensions(editorSvc);
|
||||
normalizePreviewDimensions(editorSvc);
|
||||
normalizeTocDimensions(editorSvc);
|
||||
}
|
||||
|
||||
export default {
|
||||
measureSectionDimensions,
|
||||
};
|
11
src/services/utils.js
Normal file
11
src/services/utils.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
const crypto = window.crypto || window.msCrypto;
|
||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||
const radix = alphabet.length;
|
||||
const array = new Uint32Array(20);
|
||||
|
||||
export default {
|
||||
uid() {
|
||||
crypto.getRandomValues(array);
|
||||
return array.map(value => alphabet[value % radix]).join('');
|
||||
},
|
||||
};
|
22
src/store/index.js
Normal file
22
src/store/index.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import createLogger from 'vuex/dist/logger';
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import files from './modules/files';
|
||||
import layout from './modules/layout';
|
||||
import editor from './modules/editor';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const debug = process.env.NODE_ENV !== 'production';
|
||||
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
files,
|
||||
layout,
|
||||
editor,
|
||||
},
|
||||
strict: debug,
|
||||
plugins: debug ? [createLogger()] : [],
|
||||
});
|
||||
|
||||
export default store;
|
16
src/store/modules/editor.js
Normal file
16
src/store/modules/editor.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
const setter = propertyName => (state, value) => {
|
||||
state[propertyName] = value;
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
// Configuration
|
||||
inlineImages: true,
|
||||
scrollSync: true,
|
||||
},
|
||||
mutations: {
|
||||
setInlineImages: setter('inlineImages'),
|
||||
setScrollSync: setter('scrollSync'),
|
||||
},
|
||||
};
|
31
src/store/modules/files.js
Normal file
31
src/store/modules/files.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import mdSample from '../../markdown/sample.md';
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
files: [],
|
||||
currentFile: {
|
||||
name: null,
|
||||
folderId: null,
|
||||
isLoaded: true,
|
||||
content: {
|
||||
state: {},
|
||||
text: mdSample,
|
||||
properties: {},
|
||||
discussions: {},
|
||||
comments: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setCurrentFile: (state, value) => {
|
||||
state.currentFile = value;
|
||||
},
|
||||
setCurrentFileContentText: (state, value) => {
|
||||
state.currentFile.content.text = value;
|
||||
},
|
||||
setCurrentFileContentState: (state, value) => {
|
||||
state.currentFile.content.state = value;
|
||||
},
|
||||
},
|
||||
};
|
167
src/store/modules/layout.js
Normal file
167
src/store/modules/layout.js
Normal file
|
@ -0,0 +1,167 @@
|
|||
const navigationBarHeight = 44;
|
||||
const sideBarWidth = 280;
|
||||
const editorMinWidth = 280;
|
||||
const buttonBarWidth = 30;
|
||||
const statusBarHeight = 20;
|
||||
const outOfScreenMargin = 50;
|
||||
const minPadding = 20;
|
||||
|
||||
const setter = propertyName => (state, value) => {
|
||||
state[propertyName] = value;
|
||||
};
|
||||
|
||||
const toggler = (propertyName, setterName) => ({ state, commit, dispatch }, show) => {
|
||||
commit(setterName, show === undefined ? !state[propertyName] : show);
|
||||
dispatch('updateStyle');
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
// Configuration
|
||||
showNavigationBar: true,
|
||||
showEditor: true,
|
||||
showSidePreview: true,
|
||||
showSideBar: false,
|
||||
showStatusBar: false,
|
||||
editorWidthFactor: 1,
|
||||
fontSizeFactor: 1,
|
||||
// Style
|
||||
fontSize: 0,
|
||||
inner1Y: 0,
|
||||
inner1Height: 0,
|
||||
inner2Height: 0,
|
||||
inner3X: 0,
|
||||
inner3Width: 0,
|
||||
navigationBarY: 0,
|
||||
sideBarX: 0,
|
||||
statusBarY: 0,
|
||||
editorWidth: 0,
|
||||
editorPadding: 0,
|
||||
previewWidth: 0,
|
||||
previewPadding: 0,
|
||||
},
|
||||
mutations: {
|
||||
setShowNavigationBar: setter('showNavigationBar'),
|
||||
setShowEditor: setter('showEditor'),
|
||||
setShowSidePreview: setter('showSidePreview'),
|
||||
setShowSideBar: setter('showSideBar'),
|
||||
setShowStatusBar: setter('showStatusBar'),
|
||||
setEditorWidthFactor: setter('editorWidthFactor'),
|
||||
setFontSizeFactor: setter('fontSizeFactor'),
|
||||
setFontSize: setter('fontSize'),
|
||||
setInner1Y: setter('inner1Y'),
|
||||
setInner1Height: setter('inner1Height'),
|
||||
setInner2Height: setter('inner2Height'),
|
||||
setInner3X: setter('inner3X'),
|
||||
setInner3Width: setter('inner3Width'),
|
||||
setNavigationBarY: setter('navigationBarY'),
|
||||
setSideBarX: setter('sideBarX'),
|
||||
setStatusBarY: setter('statusBarY'),
|
||||
setEditorWidth: setter('editorWidth'),
|
||||
setEditorPadding: setter('editorPadding'),
|
||||
setPreviewWidth: setter('previewWidth'),
|
||||
setPreviewPadding: setter('previewPadding'),
|
||||
},
|
||||
actions: {
|
||||
toggleNavigationBar: toggler('showNavigationBar', 'setShowNavigationBar'),
|
||||
toggleEditor: toggler('showEditor', 'setShowEditor'),
|
||||
toggleSidePreview: toggler('showSidePreview', 'setShowSidePreview'),
|
||||
toggleSideBar: toggler('showSideBar', 'setShowSideBar'),
|
||||
toggleStatusBar: toggler('showStatusBar', 'setShowStatusBar'),
|
||||
updateStyle({ state, commit, dispatch }) {
|
||||
const bodyWidth = document.body.clientWidth;
|
||||
const bodyHeight = document.body.clientHeight;
|
||||
|
||||
const showNavigationBar = state.showEditor && state.showNavigationBar;
|
||||
const inner1Y = showNavigationBar
|
||||
? navigationBarHeight
|
||||
: 0;
|
||||
const inner1Height = bodyHeight - inner1Y;
|
||||
const inner2Height = state.showStatusBar
|
||||
? inner1Height - statusBarHeight
|
||||
: inner1Height;
|
||||
const navigationBarY = showNavigationBar
|
||||
? 0
|
||||
: -navigationBarHeight - outOfScreenMargin;
|
||||
const sideBarX = state.showSideBar
|
||||
? bodyWidth - sideBarWidth
|
||||
: bodyWidth + outOfScreenMargin;
|
||||
const statusBarY = state.showStatusBar
|
||||
? inner2Height
|
||||
: inner2Height + outOfScreenMargin;
|
||||
|
||||
let doublePanelWidth = bodyWidth - buttonBarWidth;
|
||||
if (state.showSideBar) {
|
||||
doublePanelWidth -= sideBarWidth;
|
||||
}
|
||||
if (doublePanelWidth < editorMinWidth) {
|
||||
doublePanelWidth = editorMinWidth;
|
||||
}
|
||||
const splitPanel = state.showEditor && state.showSidePreview;
|
||||
if (splitPanel && doublePanelWidth / 2 < editorMinWidth) {
|
||||
dispatch('toggleSidePreview', false);
|
||||
return;
|
||||
}
|
||||
if (state.showSideBar && bodyWidth < editorMinWidth + sideBarWidth) {
|
||||
dispatch('toggleSideBar', false);
|
||||
return;
|
||||
}
|
||||
|
||||
let fontSize = 18;
|
||||
let textWidth = 990;
|
||||
if (doublePanelWidth < 1120) {
|
||||
fontSize -= 1;
|
||||
textWidth = 910;
|
||||
}
|
||||
if (doublePanelWidth < 1040) {
|
||||
textWidth = 830;
|
||||
}
|
||||
if (textWidth < 640) {
|
||||
fontSize -= 1;
|
||||
}
|
||||
textWidth *= state.editorWidthFactor;
|
||||
fontSize *= state.fontSizeFactor;
|
||||
|
||||
const panelWidth = doublePanelWidth / 2;
|
||||
let inner3X = panelWidth;
|
||||
if (!splitPanel) {
|
||||
inner3X = state.showEditor
|
||||
? doublePanelWidth
|
||||
: -buttonBarWidth;
|
||||
}
|
||||
const inner3Width = splitPanel
|
||||
? panelWidth + buttonBarWidth
|
||||
: doublePanelWidth + buttonBarWidth;
|
||||
|
||||
const previewWidth = splitPanel
|
||||
? panelWidth
|
||||
: bodyWidth;
|
||||
let previewPadding = (previewWidth - textWidth) / 2;
|
||||
if (previewPadding < minPadding) {
|
||||
previewPadding = minPadding;
|
||||
}
|
||||
const editorWidth = splitPanel
|
||||
? panelWidth
|
||||
: doublePanelWidth;
|
||||
let editorPadding = (editorWidth - textWidth) / 2;
|
||||
if (editorPadding < minPadding) {
|
||||
editorPadding = minPadding;
|
||||
}
|
||||
|
||||
commit('setFontSize', fontSize);
|
||||
commit('setInner1Y', inner1Y);
|
||||
commit('setInner1Height', inner1Height);
|
||||
commit('setInner2Height', inner2Height);
|
||||
commit('setInner3X', inner3X);
|
||||
commit('setInner3Width', inner3Width);
|
||||
commit('setNavigationBarY', navigationBarY);
|
||||
commit('setSideBarX', sideBarX);
|
||||
commit('setStatusBarY', statusBarY);
|
||||
commit('setPreviewWidth', previewWidth);
|
||||
commit('setPreviewPadding', previewPadding);
|
||||
commit('setEditorWidth', editorWidth);
|
||||
commit('setEditorPadding', editorPadding);
|
||||
},
|
||||
},
|
||||
};
|
0
static/.gitkeep
Normal file
0
static/.gitkeep
Normal file
Loading…
Reference in New Issue
Block a user