First commit

This commit is contained in:
benweet 2017-07-23 19:42:08 +01:00
commit 4622e4842c
89 changed files with 14701 additions and 0 deletions

14
.babelrc Normal file
View 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
View 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
View File

@ -0,0 +1,3 @@
build/*.js
config/*.js
src/cledit/*.js

40
.eslintrc.js Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,7 @@
{
"processors": ["stylelint-processor-html"],
"extends": "stylelint-config-standard",
"rules": {
"no-empty-source": null
}
}

21
README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
})
}

View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
module.exports = {
NODE_ENV: '"production"'
}

11
index.html Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

482
src/cledit/cldiffutils.js Normal file
View 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
View 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
View 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, '&amp;').replace(/</g, '&lt;').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

View 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

View 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
})
]

View 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

View 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
View 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
View 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

View 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
View 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
View 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, "&lt;");
// 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, '&amp;').
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, '&lt;').
replace(/>/g, '&gt;');
}
/**
* 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
View 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

File diff suppressed because it is too large Load Diff

48
src/components/App.vue Normal file
View 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>

View 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
View 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
View 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>

View 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>

View 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>

View File

@ -0,0 +1,17 @@
<template>
<div class="side-bar">
<toc>
</toc>
</div>
</template>
<script>
import Toc from './Toc';
export default {
components: {
Toc,
},
};
</script>

View 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
View File

@ -0,0 +1,6 @@
<template>
<div class="toc">
<div class="toc__inner">
</div>
</div>
</template>

View 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;
}
}

View 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;
}

View 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;
}
}

View 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;
}
}

View 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
View File

@ -0,0 +1 @@
import './markdownExt';

View 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
View 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
View 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
View 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
View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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>

View 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>

View 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>

View 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>

View 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
View 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
View 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
View 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
View 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
View 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 |

View 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 };

View File

@ -0,0 +1,3 @@
export default {
scrollOffset: 20,
};

View 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
View 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;

View 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)));
},
};

View 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;

View 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;
},
};

View File

@ -0,0 +1 @@
import './scrollSync';

View 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();
});

View 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
View 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
View 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;

View 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'),
},
};

View 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
View 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
View File

5622
yarn.lock Normal file

File diff suppressed because it is too large Load Diff