diff --git a/.gitignore b/.gitignore index dce4c90..4db6739 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ test.* dist temp !test -*.tgz \ No newline at end of file +*.tgz +packages/app/main.js +packages/app/index.html \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index d70c5e9..926ca3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,28 +1,22 @@ language: node_js - node_js: - - node - +- node env: global: - - secure: ikFQOw+feac3+WhVb1QtCeAqp9OpQprD4oXmG4+q3MVAHFiP2pLcfZbrExr0bu472eEHLpRDjjlxHEGxcnJoSool4LUCkgTnlpN8/iGzug0gnv1/XoLZiwKVbonOQHaksJ8vGBIXFMPbXRYfc1DjSyWQhwxkdhrIGUEyPs0IqrBZ4nzbpCG/hsSbkKG6xMu5sXXwM/4Nmklx8kZRMWCQXB09gMysmt1TmPwxogyxXg1xm8GaSfA5GQHJZyW3oQpvGMKaXyDLymjLw66rlrUhZ9u0TKklkBqtVlhBnGm/eXxNFnDTfz+D8K3v2MSoSRuFb8bsofLHDFDm9suBuVV83nmINhkIcsmxEbS5B+Cv916za0NgCKxKJW/DRuCCoOdjRrXk36oC+7DCvEIVH8Hsraj7oC/WgBWSHCkI12UAXhxoSHrS3OiNxJEVZJXqIUzzzbyJJE5DTHElmBm1T6ZM7WEydzAPfhtzs/d5SsjwP0w6n+8SrnKifuBaFNw7wv40sdwey9TKJEXD84AeBsbTmJQCljJuKZntQryfoCVYB82OasEoEF0ryCnc0to1/lszXLAKJBv7gOTxzQ62tKo3wJo2L6FhGIOYG5n0NVXz7kC+5rKh5ZIwKx42oT9P4OAAwpM3p+LxzS7lkn6WLamFlioCFnzmvItO9yOFR/pIiKY= - + - secure: iQS7C6BZcBlFCfj4a/1scDVO6qOCMCb2lYfZSTVS1QRnMns3wNBaDARXKrjl2LLzVPqz5puhj7+z7grUZ/rAUIAsAQUJtHqpTcpHY6kksQsh5yqNYgfwACAx0VNPicLEv2f/ekjabxaA1+7FhbCR+DFa/ZUuXGC+RpVm/EWhDDEkmGVHvXoJN92dW5lfPy/kJwpus03Mr+tPVbDJwO83sWhulUTxRuf1bEAg2/fUiPf+qZzRy6swEnfS7GlQrpD0/KHYGoa0Dt3TmqJ1oP03CRvdo/g45VZutonjLNAu7oJ/lrSTNLU68JGOnv+WCGL9RwPgfBcjTldxmSer4fwYLh+oj1IJHexNA2G0dStYVwKfkj8jEd7DLJxOrOdTYXovl3tRpyGN7oCDoWepvH1nYPBvZno9Kgt32VW/xwhqZgia5tmzra0ouyxJ/GzOXph2NaHTe9jzuF2/i814q1r3VfC5Wm3kai5R2RoNtv6bk9vS4QyEwum5JhQRJpO9O8/z393fTxMN1ZOC7D307eUQLPiLSjb/MkEfEuWtqvuEieYKs7UelC9SzrM83urJUc94FFhGfLuhqriLTJSd8gdlm5iBPjz6WrhW8n6JCa0t7iI90Cxn4DIr7KB7SROW66KSC8HPi9srPcJKVb1dx/Z8PMSP4y6ZmkTMXpbryTFK9Vo= before_install: - - node ./build/check.js - +- node ./build/check.js install: - - npm i - - npm run bootstrap - +- npm i +- npm run bootstrap script: - - npm run lint - - npm run build:prod - - npm run test - +- npm run lint +- npm run build:prod +- npm run test deploy: - - skip_cleanup: true - provider: script - script: node ./build/release.js - on: - repo: obstudio/Marklet - branch: master +- skip_cleanup: true + provider: script + script: node ./build/release.js + on: + repo: obstudio/Marklet + branch: master diff --git a/.vscode/launch.json b/.vscode/launch.json index f1a290f..ffbf1c8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,8 +6,11 @@ "request": "launch", "name": "Debug Main Process", "cwd": "${workspaceRoot}", - "runtimeExecutable": "npm", - "args": [ "start" ], + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", + "windows": { + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" + }, + "args" : ["packages/app/main.dev.js"] } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 42b0849..691d6e0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,10 +5,12 @@ "vue" ], "files.exclude": { + "**/node_modules": true, "**/package-lock.json": true, "package.json.lerna_backup": true, - "packages/**/node_modules": true, - // "packages/**/tsconfig.json": true + "packages/**/tsconfig.json": true, + "packages/**/dist": true, + "packages/**/temp": true, }, "editor.wordSeparators": "`~!@#%^&*()-=+[{]}\\|;:'\",.<>/?", } \ No newline at end of file diff --git a/README.md b/README.md index 6c8251c..460db34 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Marklet + [![Build Status](https://travis-ci.com/obstudio/Marklet.svg?branch=dev)](https://travis-ci.com/obstudio/Marklet) [![dependency](https://img.shields.io/david/obstudio/Marklet.svg)](https://github.com/obstudio/Marklet/blob/master/package.json) [![npm](https://img.shields.io/npm/v/markletjs.svg)](https://www.npmjs.com/package/markletjs) @@ -6,18 +7,47 @@ A markup language designed for API manual pages. -## Node +## Packages + +- [markletjs](https://github.com/obstudio/Marklet/tree/master/packages/marklet): [![npm](https://img.shields.io/npm/v/markletjs.svg)](https://www.npmjs.com/package/markletjs) +- [@marklet/cli](https://github.com/obstudio/Marklet/tree/master/packages/cli): [![npm](https://img.shields.io/npm/v/@marklet/cli.svg)](https://www.npmjs.com/package/@marklet/cli) +- [@marklet/core](https://github.com/obstudio/Marklet/tree/master/packages/core): [![npm](https://img.shields.io/npm/v/@marklet/core.svg)](https://www.npmjs.com/package/@marklet/core) +- [@marklet/dev-server](https://github.com/obstudio/Marklet/tree/master/packages/dev-server): [![npm](https://img.shields.io/npm/v/@marklet/dev-server.svg)](https://www.npmjs.com/package/@marklet/dev-server) +- [@marklet/monaco](https://github.com/obstudio/Marklet/tree/master/packages/monaco): [![npm](https://img.shields.io/npm/v/@marklet/monaco.svg)](https://www.npmjs.com/package/@marklet/monaco) +- [@marklet/parser](https://github.com/obstudio/Marklet/tree/master/packages/parser): [![npm](https://img.shields.io/npm/v/@marklet/parser.svg)](https://www.npmjs.com/package/@marklet/parser) +- [@marklet/renderer](https://github.com/obstudio/Marklet/tree/master/packages/renderer): [![npm](https://img.shields.io/npm/v/@marklet/renderer.svg)](https://www.npmjs.com/package/@marklet/renderer) + +## Usage: CLI + +``` +Usage: marklet [filepath|dirpath] [options] + +Options: + + -v, --version output the version number + -m, --mode [mode] Choose between parse, watch and edit mode (default: parse) + -s, --source [path] Read text from file + -i, --input [text] Read text directly from stdin + -d, --dest [path] Write parsed data to file instead of stdin + -p, --port [port] Port for the development server + -l, --default-language [language] Default language in code block + -H, --no-header-align Disable header to align at center + -S, --no-section Disallow section syntax + -h, --help output usage information +``` + +## Usage: Node ```shell -npm i marklet +npm i markletjs ``` -```javascript +```js const Marklet = require('marklet') Marklet.watch({ source: 'path/to/file' }) ``` -## Web +## Usage: Web ```html diff --git a/build/clear.js b/build/clear.js deleted file mode 100644 index 00c84c6..0000000 --- a/build/clear.js +++ /dev/null @@ -1,11 +0,0 @@ -const fs = require('fs') -const path = require('path') - -const BASE_DIR = path.join(__dirname, '../packages') - -fs.readdirSync(BASE_DIR).forEach((dir) => { - fs.readdirSync(path.join(BASE_DIR, dir)) - .forEach((file) => { - if (/[\w.-]+\.tgz/.test(file)) fs.unlinkSync(path.join(BASE_DIR, dir, file)) - }) -}) diff --git a/build/publish.js b/build/publish.js index caa4999..7c4e22f 100644 --- a/build/publish.js +++ b/build/publish.js @@ -50,6 +50,7 @@ program .option('-1, --major') .option('-2, --minor') .option('-3, --patch') + .option('-o, --only') .option('-p, --publish') .parse(process.argv) @@ -58,6 +59,7 @@ if (program.all) program.args = packageNames function bump(name, flag) { packages[name].bump(flag || 'patch') + if (program.only) return const npmName = packages[name].current.name packageNames.forEach((next) => { if (npmName in (packages[next].current.devDependencies || {})) { diff --git a/build/util.js b/build/util.js index 9785f28..10a5e85 100644 --- a/build/util.js +++ b/build/util.js @@ -26,8 +26,51 @@ function resolve(...names) { return path.join(__dirname, '../packages', ...names) } +const timers = {} + +function start(label = '') { + if (!timers[label]) timers[label] = { total: 0 } + timers[label].start = Date.now() + return _getTime(label) +} + +function pause(label = '') { + timers[label].total += Date.now() - timers[label].start + timers[label].start = Date.now() + return _getTime(label) +} + +function finish(label = '') { + pause(label) + const result = _getTime(label) + timers[label].total = 0 + return `Finished in ${result.toFixed(1)}s.` +} + +function _getTime(label = '') { + return label in timers ? timers[label].total / 1000 : 0 +} + +function timing(label = '', callback) { + start(label) + const result = callback() + pause(label) + return result +} + +function isElectron() { + return typeof process !== 'undefined' + && typeof process.versions !== 'undefined' + && typeof process.versions.electron !== 'undefined' +} + module.exports = { exec, execSync, resolve, + start, + pause, + finish, + timing, + isElectron, } \ No newline at end of file diff --git a/package.json b/package.json index c731339..8578516 100644 --- a/package.json +++ b/package.json @@ -1,40 +1,39 @@ { "scripts": { "start": "npm run build && node packages/cli -m edit", - "publish": "lerna publish --no-git-tag-version", "bootstrap": "lerna bootstrap --hoist --no-ci", "build": "tsc -b && node build/build -sr", "build:prod": "tsc -b && node build/build -psr", "build:renderer": "node build/build -r", "build:server": "node build/build -s", "build:tsc": "tsc -b", - "clear": "node build/clear", - "test": "node packages/test/runner.js", + "test": "node packages/test", "lint": "tslint packages/**/src/*.ts && eslint ." }, "devDependencies": { - "@octokit/rest": "^15.12.0", + "@octokit/rest": "^15.13.0", "@sfc2js/clean-css": "^1.1.1", "@sfc2js/sass": "^1.0.1", "@types/cheerio": "^0.22.9", "@types/js-yaml": "^3.11.2", - "@types/node": "^10.10.1", + "@types/node": "^10.11.4", "@types/ws": "^6.0.1", - "ajv": "^6.5.3", + "ajv": "^6.5.4", "chalk": "^2.4.1", "cheerio": "^1.0.0-rc.2", "commander": "^2.18.0", - "eslint": "^5.6.0", + "electron": "^3.0.2", + "eslint": "^5.6.1", "eslint-plugin-vue": "^5.0.0-beta.3", "fast-deep-equal": "^2.0.1", "html-minifier": "^3.5.20", - "lerna": "^3.4.0", + "lerna": "^3.4.1", "node-sass": "^4.9.3", - "prettier": "^1.14.3", + "sass": "^1.14.1", "semver": "^5.5.1", - "sfc2js": "^3.3.0", + "sfc2js": "^3.3.2", "tslint": "^5.11.0", - "typescript": "^3.0.3", - "webpack": "^4.19.1" + "typescript": "^3.1.1", + "webpack": "^4.20.2" } } diff --git a/packages/app/README.md b/packages/app/README.md new file mode 100644 index 0000000..a5f13a7 --- /dev/null +++ b/packages/app/README.md @@ -0,0 +1,3 @@ +# @marklet/app + +An electron app for marklet. diff --git a/packages/app/build/transpile.js b/packages/app/build/transpile.js new file mode 100644 index 0000000..80d914e --- /dev/null +++ b/packages/app/build/transpile.js @@ -0,0 +1,68 @@ +const util = require('../../../build/util') +const sfc2js = require('sfc2js') +const sass = require('sass') +const path = require('path') +const fs = require('fs') + +util.start() + +sfc2js.install({ + name: 'sass-plugin', + version: '1.0', + target: 'style', + lang: [ + 'sass', + 'scss', + 'css', + ], + default: { + includePaths: [], + }, + updated(options) { + const dirPath = path.dirname(options.srcPath) + this.options.includePaths.push(dirPath) + }, + render(style) { + return sass.renderSync({ ...this.options, data: style.content }).css.toString() + }, +}) + +module.exports = sfc2js.transpile({ + baseDir: util.resolve(), + srcDir: 'app/comp', + outDir: 'app/temp', + enterance: util.isElectron() ? 'app.vue' : '', +}) + +let indexCache = '' +try { + indexCache = fs.readFileSync(util.resolve('app/temp/index.cache.scss')).toString() +} catch (error) { /**/ } + +const indexData = fs.readFileSync(util.resolve('app/comp/index.scss')).toString() + +if (indexData === indexCache && fs.existsSync(util.resolve('app/temp/index.css'))) { + module.exports.css += fs.readFileSync(util.resolve('app/temp/index.css')).toString() +} else { + const indexCSS = sass.renderSync({ data: indexData }).css.toString() + fs.writeFileSync(util.resolve('app/temp/index.css'), indexCSS) + fs.writeFileSync(util.resolve('app/temp/index.cache.scss'), indexCache) + module.exports.css += indexCSS +} + +if (util.isElectron()) { + const result = sfc2js.transpile({ + baseDir: util.resolve(), + srcDir: 'renderer/comp', + outDir: 'renderer/temp', + enterance: '../src', + outCSSFile: '../dist/marklet.min.css', + defaultScript: { + props: ['node'], + }, + }) + module.exports.css += result.css + module.exports.plugin = result.app +} + +console.log('Transpile Succeed.', util.finish()) diff --git a/packages/app/comp/.eslintrc.yml b/packages/app/comp/.eslintrc.yml new file mode 100644 index 0000000..98ddf5e --- /dev/null +++ b/packages/app/comp/.eslintrc.yml @@ -0,0 +1,5 @@ +extends: + - plugin:vue/essential + +globals: + Vue: true diff --git a/packages/app/comp/app.vue b/packages/app/comp/app.vue new file mode 100644 index 0000000..684e3cd --- /dev/null +++ b/packages/app/comp/app.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/packages/app/comp/index.scss b/packages/app/comp/index.scss new file mode 100644 index 0000000..ef3896f --- /dev/null +++ b/packages/app/comp/index.scss @@ -0,0 +1,49 @@ +body { + overflow: hidden; + font-family: + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Helvetica, + Arial, + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol"; +} + +.no-transition { + transition: none !important; +} + +.monaco-editor { + .monaco-scrollable-element { + > .scrollbar { + transition: opacity 0.3s ease; + + > .slider { + opacity: 0.5; + cursor: pointer; + border-radius: 6px; + background-color: #c0c4cc; + transition: opacity 0.3s ease; + + &:hover { + opacity: 1; + } + } + + &.vertical { + margin: 4px 0; + } + + &.horizontal { + margin: 0 4px; + } + + &.invisible.fade { + transition: opacity 0.8s ease; + } + } + } +} diff --git a/packages/app/index.dev.html b/packages/app/index.dev.html new file mode 100644 index 0000000..8463489 --- /dev/null +++ b/packages/app/index.dev.html @@ -0,0 +1,21 @@ + + + + + Marklet + + + + + +
+ + + diff --git a/packages/app/index.prod.html b/packages/app/index.prod.html new file mode 100644 index 0000000..658f0b0 --- /dev/null +++ b/packages/app/index.prod.html @@ -0,0 +1,12 @@ + + + + + Marklet + + + +
+ + + diff --git a/packages/app/main.dev.js b/packages/app/main.dev.js new file mode 100644 index 0000000..f852d37 --- /dev/null +++ b/packages/app/main.dev.js @@ -0,0 +1,31 @@ +const { app, BrowserWindow } = require('electron') +const path = require('path') + +let mainWindow + +function createMain() { + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + minWidth: 800, + minHeight: 600, + useContentSize: true, + autoHideMenuBar: false, + }) + + mainWindow.loadFile(path.join(__dirname, 'index.dev.html')) + + mainWindow.on('closed', () => { + mainWindow = null + }) +} + +app.on('ready', createMain) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit() +}) + +app.on('activate', () => { + if (mainWindow === null) createMain() +}) diff --git a/packages/app/main.prod.js b/packages/app/main.prod.js new file mode 100644 index 0000000..7ba670c --- /dev/null +++ b/packages/app/main.prod.js @@ -0,0 +1,31 @@ +const { app, BrowserWindow } = require('electron') +const path = require('path') + +let mainWindow + +function createMain() { + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + minWidth: 800, + minHeight: 600, + useContentSize: true, + autoHideMenuBar: false, + }) + + mainWindow.loadFile(path.join(__dirname, 'index.prod.html')) + + mainWindow.on('closed', () => { + mainWindow = null + }) +} + +app.on('ready', createMain) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit() +}) + +app.on('activate', () => { + if (mainWindow === null) createMain() +}) diff --git a/packages/lexer/package.json b/packages/app/package.json similarity index 53% rename from packages/lexer/package.json rename to packages/app/package.json index 5cf1771..113f2fc 100644 --- a/packages/lexer/package.json +++ b/packages/app/package.json @@ -1,26 +1,26 @@ { - "name": "@marklet/lexer", + "name": "@marklet/app", "version": "1.0.10", - "description": "A document lexer for marklet.", + "private": true, + "main": "main.js", "author": "shigma <1700011071@pku.edu.cn>", - "contributors": [ - "jjyyxx <1449843302@qq.com>" - ], "homepage": "https://github.com/obstudio/Marklet", "license": "MIT", - "main": "dist/index.js", - "typings": "dist/index.d.ts", - "files": [ - "dist" - ], "repository": { "type": "git", "url": "git+https://github.com/obstudio/Marklet.git" }, + "scripts": { + "test": "echo \"Error: run tests from root\" && exit 1" + }, "bugs": { "url": "https://github.com/obstudio/Marklet/issues" }, "dependencies": { - "@marklet/core": "^2.0.0" + "@marklet/monaco": "^1.2.1", + "@marklet/parser": "^1.5.1", + "@marklet/renderer": "^1.3.1", + "neat-scroll": "^2.0.1", + "vue": "^2.5.17" } } \ No newline at end of file diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 0000000..d58cf6b --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,8 @@ +# @marklet/cli + +[![Build Status](https://travis-ci.com/obstudio/Marklet.svg?branch=dev)](https://travis-ci.com/obstudio/Marklet) +[![dependency](https://img.shields.io/david/obstudio/Marklet.svg?path=packages%2Fcli)](https://github.com/obstudio/Marklet/blob/master/packages/cli/package.json) +[![npm](https://img.shields.io/npm/v/@marklet/cli.svg)](https://www.npmjs.com/package/@marklet/cli) +[![npm bundle size (minified)](https://img.shields.io/bundlephobia/min/@marklet/cli.svg)](https://www.npmjs.com/package/@marklet/cli) + +A command line interface for marklet. diff --git a/packages/cli/package.json b/packages/cli/package.json index 0acb1ba..609cf87 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,12 +1,12 @@ { "name": "@marklet/cli", - "version": "1.1.5", + "version": "1.1.15", "description": "A command line interface for marklet.", "author": "jjyyxx <1449843302@qq.com>", "contributors": [ "shigma <1700011071@pku.edu.cn>" ], - "homepage": "https://github.com/obstudio/Marklet", + "homepage": "https://github.com/obstudio/Marklet/tree/dev/packages/cli", "license": "MIT", "repository": { "type": "git", @@ -19,8 +19,8 @@ "url": "https://github.com/obstudio/Marklet/issues" }, "dependencies": { - "@marklet/dev-server": "^1.0.12", - "@marklet/parser": "^1.1.0", + "@marklet/dev-server": "^1.0.22", + "@marklet/parser": "^1.5.1", "chalk": "^2.4.1", "commander": "^2.18.0", "js-yaml": "^3.12.0" diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..4110d71 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,8 @@ +# @marklet/core + +[![Build Status](https://travis-ci.com/obstudio/Marklet.svg?branch=dev)](https://travis-ci.com/obstudio/Marklet) +[![dependency](https://img.shields.io/david/obstudio/Marklet.svg?path=packages%2Fcore)](https://github.com/obstudio/Marklet/blob/master/packages/core/package.json) +[![npm](https://img.shields.io/npm/v/@marklet/core.svg)](https://www.npmjs.com/package/@marklet/core) +[![npm bundle size (minified)](https://img.shields.io/bundlephobia/min/@marklet/core.svg)](https://www.npmjs.com/package/@marklet/core) + +Some core conceptions of marklet. diff --git a/packages/core/package.json b/packages/core/package.json index c294c16..391ede9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,12 +1,12 @@ { "name": "@marklet/core", - "version": "2.0.0", + "version": "3.2.2", "description": "Some core conceptions of marklet.", "author": "shigma <1700011071@pku.edu.cn>", "contributors": [ "jjyyxx <1449843302@qq.com>" ], - "homepage": "https://github.com/obstudio/Marklet", + "homepage": "https://github.com/obstudio/Marklet/tree/dev/packages/core", "license": "MIT", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/core/src/document.ts b/packages/core/src/document.ts new file mode 100644 index 0000000..407ee89 --- /dev/null +++ b/packages/core/src/document.ts @@ -0,0 +1,187 @@ +import { + Lexer, + parseRule, + StringLike, + TokenLike, + MacroMap, + LexerMacros, + LexerConfig, + LexerRule, + LexerRegexRule, +} from './lexer' + +import { InlineLexer } from './inline' + +export interface DocumentOptions { + /** lexer rule regex macros */ + macros?: LexerMacros + /** entrance context */ + entrance?: string + /** default inline context */ + inlineEntrance?: string + /** assign start/end to tokens */ + requireBound?: boolean + /** other configurations */ + config?: LexerConfig +} + +type DocumentLexerRule = LexerRegexRule +type NativeLexerContext = DocumentLexerRule[] | InlineLexer +export type DocumentContexts = Record[] | InlineLexer> + +enum ContextOperation { + INCLUDE, + PUSH, + INLINE, + INITIAL, +} + +interface ContextLog { + name: string + operation: ContextOperation +} + +export class DocumentLexer extends Lexer { + private stackTrace: ContextLog[] + private contexts: Record[] | InlineLexer> = {} + private entrance: string + private inlineEntrance: string + private requireBound: boolean + private macros: MacroMap + + constructor(contexts: DocumentContexts, options: DocumentOptions = {}) { + super(options.config) + this.entrance = options.entrance || 'main' + this.inlineEntrance = options.inlineEntrance || 'text' + this.requireBound = !!options.requireBound + + this.macros = new MacroMap(options.macros || {}) + for (const key in contexts) { + const context = contexts[key] + if (context instanceof Array) { + this.contexts[key] = context.map((rule) => parseRule(rule, this.macros)) + } else { + this.contexts[key] = context + } + } + } + + getContext( + context: string | InlineLexer | LexerRule | LexerRule[], + operation: ContextOperation, + prefixRegex?: RegExp, + postfixRegex?: RegExp, + ) { + const name = typeof context === 'string' ? context : 'anonymous' + if (operation === ContextOperation.INITIAL) { + this.stackTrace = [{ name, operation }] + } else if (operation !== ContextOperation.INCLUDE) { + this.stackTrace.push({ name, operation }) + } else { + this.stackTrace[this.stackTrace.length - 1].name = name + } + let rules = typeof context === 'string' ? this.contexts[context] : context + if (!rules) throw new Error(`Context '${context}' was not found. (context-not-found)`) + if (rules instanceof InlineLexer) { + return rules.fork(prefixRegex, postfixRegex) + } else { + if (!(rules instanceof Array)) rules = [rules] + for (let i = rules.length - 1; i >= 0; i -= 1) { + const rule: LexerRule = rules[i] + if ('include' in rule) { + const includes = this.getContext(rule.include, ContextOperation.INCLUDE) + if (includes instanceof Array) { + rules.splice(i, 1, ...includes) + } else { + throw new Error('Including a inline context is illegal. (no-include-inline)') + } + } + } + const result = rules.slice() + if (prefixRegex) result.unshift({ regex: prefixRegex, pop: true, test: true }) + if (postfixRegex) result.push({ regex: postfixRegex, pop: true, test: true }) + return result + } + } + + initialize(context: NativeLexerContext) { + if (!(context instanceof Array)) { + const result = context.run(this.meta.source) + return { + index: result.index, + output: [result.output], + } + } + this.meta.output = [] + this.meta.context = context + } + + getContent(rule: DocumentLexerRule, capture: RegExpExecArray) { + let prefixRegex = rule.prefix_regex + let postfixRegex = rule.strict ? /^(?=[\s\S])/ : null + if (prefixRegex instanceof Function) { + prefixRegex = new RegExp(`^(?:${this.macros.resolve(prefixRegex.call(this, capture))})`) + } + const context = this.getContext(rule.push, ContextOperation.PUSH, prefixRegex, postfixRegex) + const result = this.run(this.meta.source, false, context) + const content = result.output.map((token) => { + if (this.requireBound && typeof token === 'object') { + token.start += this.meta.index + token.end += this.meta.index + } + return token + }) + this.stackTrace.pop() + this.meta.source = this.meta.source.slice(result.index) + this.meta.index += result.index + return content + } + + pushUnmatch() { + this.meta.output.push(this.meta.unmatch) + } + + pushToken(rule: DocumentLexerRule, capture: RegExpExecArray, content: TokenLike[]) { + let token = rule.token + if (typeof token === 'function') { + token = token.call(this, capture, content, this.config) + } else if (token === undefined) { + if (rule.push) { + token = { content } + } else if (!rule.pop) { + token = capture[0] + } + } + if (token) { + if (typeof token === 'object') { + token.type = token.type || rule.type + if (this.requireBound) { + token.start = this.meta.start + token.end = this.meta.index + } + } + this.meta.output.push(token) + } + } + + inline(source: string, context: string = this.inlineEntrance): string { + const inlineContext = this.getContext(context, ContextOperation.INLINE) + if (inlineContext instanceof Array) { + throw new Error(`'${context}' is not a inline context. (not-inline-context)`) + } + const result = inlineContext.run(source).output + this.stackTrace.pop() + return result + } + + parse(source: string, context: string = this.entrance): TokenLike[] { + const initialContext = this.getContext(context, ContextOperation.INITIAL) + source = source.replace(/\r\n/g, '\n') + try { + return this.run(source, true, initialContext).output + } catch (error) { + console.log(this.stackTrace) + console.error(error) + } + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9f71c60..69216ca 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,118 +1,34 @@ -export type StringLike = string | RegExp - -export type LexerConfig = Record -export type LexerMacros = Record - -export type TokenLike = string | LexerToken -export interface LexerToken { - type?: string - text?: string - content?: TokenLike[] - start?: number - end?: number - [key: string]: any -} - -export type LexerRule< - S extends StringLike = RegExp, - T extends LexerInstance = LexerInstance, - R extends RegExpExecArray = RegExpExecArray, -> = LexerIncludeRule | LexerRegexRule - -export interface LexerIncludeRule { include: string } -export interface LexerRegexRule< - S extends StringLike = RegExp, - T extends LexerInstance = LexerInstance, - R extends RegExpExecArray = RegExpExecArray, -> { - /** the regular expression to execute */ - regex?: S - /** - * a string containing all the rule flags: - * - `b`: match when the context begins - * - `e`: match end of line - * - `i`: ignore case - * - `p`: pop from the current context - * - `s`: pop when no match is found - * - `t`: match top level context - */ - flags?: string - /** default type of the token */ - type?: string - /** whether the rule is to be executed */ - test?: string | boolean | ((this: T, config: LexerConfig) => boolean) - /** a result token */ - token?: TokenLike | TokenLike[] | (( - this: T, capture: R, content: TokenLike[] - ) => TokenLike | TokenLike[]) - /** the inner context */ - push?: string | LexerRule[] - /** pop from the current context */ - pop?: boolean - /** pop when no match is found */ - strict?: boolean - /** match when the context begins */ - context_begins?: boolean - /** match top level context */ - top_level?: boolean - /** whether to ignore case */ - ignore_case?: boolean - /** match end of line */ - eol?: boolean -} - -/** Transform a string-like object into a raw string. */ -export function getString(string: StringLike): string { - return string instanceof RegExp ? string.source : string -} - -export function parseRule(rule: LexerRule, macros: LexerMacros = {}): LexerRule { - if (!('include' in rule)) { - if (rule.regex === undefined) { - rule.regex = /(?=[\s\S])/ - if (!rule.type) rule.type = 'default' - } - if (rule.test === undefined) rule.test = true - let src = getString(rule.regex) - let flags = '' - for (const key in macros) { - src = src.replace(new RegExp(`{{${key}}}`, 'g'), `(?:${macros[key]})`) - } - rule.flags = rule.flags || '' - if (rule.flags.replace(/[biepst]/g, '')) { - throw new Error(`'${rule.flags}' contains invalid rule flags.`) - } - if (rule.flags.includes('p')) rule.pop = true - if (rule.flags.includes('s')) rule.strict = true - if (rule.flags.includes('b')) rule.context_begins = true - if (rule.flags.includes('t')) rule.top_level = true - if (rule.flags.includes('e') || rule.eol) src += ' *(?:\\n+|$)' - if (rule.flags.includes('i') || rule.ignore_case) flags += 'i' - rule.regex = new RegExp('^(?:' + src + ')', flags) - if (rule.push instanceof Array) rule.push.forEach(_rule => parseRule(_rule, macros)) - } - return rule as LexerRule -} - -export interface LexerInstance { - config: LexerConfig - parse(source: string): any -} - -export interface InlineLexerResult { - index: number - output: string -} - -export interface InlineLexerInstance extends LexerInstance { - parse(source: string): InlineLexerResult -} - -export enum MatchStatus { - /** No match was found */ - NO_MATCH, - /** Found match and continue */ - CONTINUE, - /** Found match and pop */ - POP, -} +export { + Lexer, + parseRule, + getString, + isStringLike, + StringLike, + TokenLike, + MacroMap, + LexerRule, + LexerMeta, + LexerToken, + LexerConfig, + LexerMacros, + LexerResult, + LexerMetaRule, + LexerRegexRule, + LexerIncludeRule, +} from './lexer' + +export { + DocumentOptions, + DocumentContexts, + DocumentLexer, +} from './document' + +export { + InlineContext, + InlineLexer, +} from './inline' + +export { + SyntaxOptions, + SyntaxLexer, +} from './syntax' diff --git a/packages/core/src/inline.ts b/packages/core/src/inline.ts new file mode 100644 index 0000000..cc313ce --- /dev/null +++ b/packages/core/src/inline.ts @@ -0,0 +1,73 @@ +import { + Lexer, + parseRule, + StringLike, + LexerResult, + LexerConfig, + LexerRegexRule, +} from './lexer' + +type InlineLexerRule = LexerRegexRule +export type InlineContext = LexerRegexRule[] + +class InlineCapture extends Array implements RegExpExecArray { + public index: number + public input: string + private lexer: InlineLexer + + constructor(lexer: InlineLexer, array: RegExpExecArray) { + super(...array) + this.lexer = lexer + this.index = array.index + this.input = array.input + } + + get inner(): string { + const match = this.reverse().find(item => !!item) + return match ? this.lexer.run(match).output : '' + } +} + +export class InlineLexer extends Lexer { + private rules: InlineLexerRule[] + + constructor(context: InlineContext, config?: LexerConfig) { + super(config) + this.rules = context.map(rule => parseRule(rule) as InlineLexerRule) + } + + initialize() { + this.meta.output = '' + this.meta.context = this.rules + } + + getCapture(rule: InlineLexerRule, capture: RegExpExecArray) { + return new InlineCapture(this, capture) + } + + pushToken(rule: InlineLexerRule, capture: InlineCapture) { + let token = rule.token + if (typeof token === 'function') { + token = token.call(this, capture, null, this.config) + } else if (token === undefined) { + token = capture[0] + } + this.meta.output += token + } + + pushUnmatch() { + this.meta.output += this.meta.unmatch + } + + parse(source: string): LexerResult { + return this.run(source.replace(/\r\n/g, '\n'), true) + } + + fork(prefix?: RegExp, postfix?: RegExp): InlineLexer { + const fork = new InlineLexer([]) + fork.rules = this.rules.slice() + if (prefix) fork.rules.unshift({ regex: prefix, pop: true, test: true }) + if (postfix) fork.rules.push({ regex: postfix, pop: true, test: true }) + return fork + } +} diff --git a/packages/core/src/lexer.ts b/packages/core/src/lexer.ts new file mode 100644 index 0000000..69e9067 --- /dev/null +++ b/packages/core/src/lexer.ts @@ -0,0 +1,273 @@ +export type StringLike = string | RegExp +export type TokenLike = string | LexerToken + +export type LexerConfig = Record +export type LexerMacros = Record + +export interface LexerToken { + type?: string + text?: string + content?: TokenLike[] + start?: number + end?: number + [key: string]: any +} + +export type LexerRule< + S extends StringLike = StringLike, + T extends Lexer = Lexer, + R extends RegExpExecArray = RegExpExecArray, +> = LexerIncludeRule | LexerMetaRule | LexerRegexRule + +export interface LexerMetaRule { meta: string } + +export interface LexerIncludeRule { include: string } + +export interface LexerRegexRule< + S extends StringLike = RegExp, + T extends Lexer = Lexer, + R extends RegExpExecArray = RegExpExecArray, +> { + /** the regular expression to execute */ + regex?: S + /** an regex placed at the beginning of inner context */ + prefix_regex?: S | ((this: T, capture: R) => StringLike) + /** + * a string containing all the rule flags: + * - `b`: match when the context begins + * - `e`: match end of line + * - `i`: ignore case + * - `p`: pop from the current context + * - `s`: strict mode + * - `t`: match top level context + */ + flags?: string + /** default type of the token */ + type?: string + /** whether the rule is to be executed */ + test?: string | boolean | ((this: T, config: LexerConfig) => boolean) + /** a result token */ + token?: TokenLike | ((this: T, capture: R, content: TokenLike[], config: LexerConfig) => TokenLike) + /** token scope */ + scope?: string + /** token scope mapped with captures */ + captures?: Record + /** the inner context */ + push?: string | LexerRule | LexerRule[] + /** pop from the current context */ + pop?: boolean | ((this: T, capture: R) => boolean) + /** strict mode: pop when no match is found */ + strict?: boolean + /** match when the context begins */ + context_begins?: boolean + /** match top level context */ + top_level?: boolean + /** whether to ignore case */ + ignore_case?: boolean + /** match end of line */ + eol?: boolean +} + +export class MacroMap { + private data: Record = {} + + constructor(macros: Record = {}) { + for (const key in macros) { + this.data[key] = { + regex: new RegExp(`{{${key}}}`, 'g'), + macro: `(?:${getString(macros[key])})`, + } + } + } + + resolve(source: StringLike): string { + source = getString(source) + for (const key in this.data) { + source = source.replace(this.data[key].regex, this.data[key].macro) + } + return source + } +} + +const noMacro = new MacroMap() + +/** transform a string-like object into a raw string */ +export function getString(source: StringLike): string { + return source instanceof RegExp ? source.source : source +} + +export function isStringLike(source: any): boolean { + return typeof source === 'string' || source instanceof RegExp +} + +/** transform lexer rules with string into ones with regexp */ +export function parseRule(rule: LexerRule, macros: MacroMap = noMacro): LexerRule { + if (!('include' in rule || 'meta' in rule)) { + if (rule.regex === undefined) { + rule.regex = /(?=[\s\S])/ + if (!rule.type) rule.type = 'default' + } + if (rule.test === undefined) rule.test = true + let source = macros.resolve(rule.regex) + let flags = '' + rule.flags = rule.flags || '' + if (rule.flags.replace(/[biepst]/g, '')) { + throw new Error(`'${rule.flags}' contains invalid rule flags. (invalid-flags)`) + } + if (rule.flags.includes('s')) rule.strict = true + if (rule.flags.includes('b')) rule.context_begins = true + if (rule.flags.includes('t')) rule.top_level = true + if (rule.flags.includes('p') && !rule.pop) rule.pop = true + if (rule.flags.includes('e') || rule.eol) source += '[ \t]*(?:\n+|$)' + if (rule.flags.includes('i') || rule.ignore_case) flags += 'i' + rule.regex = new RegExp('^(?:' + source + ')', flags) + const prefix = rule.prefix_regex + if (isStringLike(prefix)) { + rule.prefix_regex = new RegExp(`^(?:${macros.resolve(prefix as StringLike)})`) + } + const push = rule.push + if (push instanceof Array) { + push.forEach(_rule => parseRule(_rule, macros)) + } else if (typeof push === 'object') { + rule.push = parseRule(push, macros) + } + } + return rule as LexerRule +} + +enum MatchStatus { + /** No match was found */ + NO_MATCH, + /** Found match and continue */ + CONTINUE, + /** Found match and pop */ + POP, +} + +export interface LexerResult { + /** current index of the source string */ + index: number + /** output string or array */ + output: R +} + +export interface LexerMeta extends Partial> { + /** record where the match starts */ + start?: number + /** a copy of source string */ + source?: string + /** a string collecting unmatch chars */ + unmatch?: string + /** whether running at top level */ + isTopLevel?: boolean + /** current lexing context */ + context?: LexerRegexRule[] +} + +export abstract class Lexer { + public meta: LexerMeta + public config: LexerConfig + + constructor(config?: LexerConfig) { + this.config = config || {} + } + + initialize?(...args: any[]): void | LexerResult + getCapture?(rule: LexerRegexRule, capture: RegExpExecArray): RegExpExecArray + getContent?(rule: LexerRegexRule, capture: RegExpExecArray): TokenLike[] + pushToken?(rule: LexerRegexRule, capture: RegExpExecArray, content: TokenLike[]): void + pushUnmatch?(): void + + run(source: string, isTopLevel?: boolean, ...args: any[]): LexerResult { + // store meta data from lower level + const _meta = this.meta + this.meta = { + source, + isTopLevel, + index: 0, + unmatch: '', + } + + // initialize or simply get the result + const final = this.initialize(...args) + if (final) return this.meta = _meta, final + + // walk through the source string + while (this.meta.source) { + let status: MatchStatus = MatchStatus.NO_MATCH + for (const rule of this.meta.context) { + // Step 1: test before matching + if (rule.top_level && !this.meta.isTopLevel) continue + if (rule.context_begins && this.meta.index) continue + + let test = rule.test + if (typeof test === 'string') { + if (test.charAt(0) === '!') { + test = !this.config[test.slice(1)] + } else { + test = !!this.config[test] + } + } else if (typeof test === 'function') { + test = !!test.call(this, this.config) + } + if (!test) continue + + // Step 2: exec regex and get capture + const match = rule.regex.exec(this.meta.source) + if (!match) continue + this.meta.source = this.meta.source.slice(match[0].length) + this.meta.start = this.meta.index + this.meta.index += match[0].length + const capture = this.getCapture ? this.getCapture(rule, match) : match + + // Step 3: reset match status + let pop = rule.pop + if (typeof pop === 'function') pop = pop.call(this, capture) + status = pop ? MatchStatus.POP : MatchStatus.CONTINUE + + // Step 4: get inner tokens + const content = rule.push && this.getContent ? this.getContent(rule, capture) : [] + + // Step 5: detect endless loop + if (!rule.pop && this.meta.start === this.meta.index) { + throw new Error(`Endless loop at '${ + this.meta.source.slice(0, 10) + } ${ + this.meta.source.length > 10 ? '...' : '' + }'. (endless-loop)`) + } + + // Step 6: handle unmatched chars + if (this.pushUnmatch && this.meta.unmatch) { + this.pushUnmatch() + this.meta.unmatch = '' + } + + // Step 7: push generated token + this.pushToken(rule, capture, content) + + // Step 8: break loop + break + } + + if (status === MatchStatus.POP) break + if (status === MatchStatus.NO_MATCH) { + this.meta.unmatch += this.meta.source.charAt(0) + this.meta.source = this.meta.source.slice(1) + this.meta.index += 1 + } + } + + // handle ramaining unmatched chars + if (this.pushUnmatch && this.meta.unmatch) this.pushUnmatch() + + const result: LexerResult = { + index: this.meta.index, + output: this.meta.output, + } + + // restore meta data for lower level + this.meta = _meta + return result + } +} diff --git a/packages/core/src/syntax.ts b/packages/core/src/syntax.ts new file mode 100644 index 0000000..f9a3e7e --- /dev/null +++ b/packages/core/src/syntax.ts @@ -0,0 +1,36 @@ +import { + Lexer, + StringLike, + TokenLike, + parseRule, + getString, + LexerRule, + LexerMacros, + MacroMap, +} from './lexer' + +export interface SyntaxOptions { + name?: string + alias?: string[] + macros?: Record + contexts?: Record +} + +export class SyntaxLexer extends Lexer { + public name: string + public alias: string[] + private macros: MacroMap + private contexts: Record[]> = {} + + constructor(options: SyntaxOptions) { + super() + this.name = options.name || '' + this.alias = options.alias || [] + this.macros = new MacroMap(options.macros || {}) + + for (const key in options.contexts) { + const context = options.contexts[key] + this.contexts[key] = context.map(rule => parseRule(rule, this.macros)) + } + } +} diff --git a/packages/detok/README.md b/packages/detok/README.md new file mode 100644 index 0000000..78c98a7 --- /dev/null +++ b/packages/detok/README.md @@ -0,0 +1,8 @@ +# @marklet/detok + +[![Build Status](https://travis-ci.com/obstudio/Marklet.svg?branch=dev)](https://travis-ci.com/obstudio/Marklet) +[![dependency](https://img.shields.io/david/obstudio/Marklet.svg?path=packages%2Fdetok)](https://github.com/obstudio/Marklet/blob/master/packages/detok/package.json) +[![npm](https://img.shields.io/npm/v/@marklet/detok.svg)](https://www.npmjs.com/package/@marklet/detok) +[![npm bundle size (minified)](https://img.shields.io/bundlephobia/min/@marklet/detok.svg)](https://www.npmjs.com/package/@marklet/detok) + +A detokenizer for marklet. diff --git a/packages/detok/index.js b/packages/detok/index.js new file mode 100644 index 0000000..94b8c48 --- /dev/null +++ b/packages/detok/index.js @@ -0,0 +1 @@ +module.exports = require('./dist/document').default \ No newline at end of file diff --git a/packages/detok/package.json b/packages/detok/package.json index dfdb3af..a155548 100644 --- a/packages/detok/package.json +++ b/packages/detok/package.json @@ -1,15 +1,16 @@ { "name": "@marklet/detok", - "version": "1.0.10", + "version": "1.1.3", "description": "A detokenizer for marklet.", "author": "jjyyxx <1449843302@qq.com>", "contributors": [ "shigma <1700011071@pku.edu.cn>" ], - "homepage": "https://github.com/obstudio/Marklet", + "homepage": "https://github.com/obstudio/Marklet/tree/dev/packages/detok", "license": "MIT", - "main": "dist/index.js", - "typings": "dist/index.d.ts", + "main": "index.js", + "module": "dist/document.js", + "typings": "dist/document.d.ts", "files": [ "dist" ], @@ -24,6 +25,7 @@ "cheerio": "^1.0.0-rc.2" }, "devDependencies": { - "@marklet/core": "^2.0.0" + "@marklet/core": "^3.2.2", + "@marklet/parser": "^1.5.1" } } \ No newline at end of file diff --git a/packages/detok/src/document.ts b/packages/detok/src/document.ts new file mode 100644 index 0000000..f4946d5 --- /dev/null +++ b/packages/detok/src/document.ts @@ -0,0 +1,85 @@ +import { TokenLike, LexerToken } from '@marklet/core' +import { Tokens } from '@marklet/parser' +import inline from './inline' + +type DocumentDetokenizer = (tok: LexerToken) => string + +const alignMap = { + left: '<', + center: '=', + right: '>' +} + +let listLevel = 0 + +function toCamel(string: string) { + return string.replace(/-[a-z]/g, match => match.slice(1).toUpperCase()) +} + +const detokenizers: Record = { + heading: (token: Tokens.Heading) => + '#'.repeat(token.level) + + ' ' + + inline(token.text) + + (token.center ? ' #' : ''), + section: (token: Tokens.Section) => + '^'.repeat(token.level) + + ' ' + + inline(token.text) + + (token.initial === 'closed' ? ' ^' : '') // FIXME: currently not taking `section_default` into consideration + + '\n' + + detokenize(token.content), + quote: (token: Tokens.Quote) => + '>' + + token.style + + ' ' + + detokenize(token.content), + separator(token: Tokens.Separator) { + const sep = token.thick ? '=' : '-' + switch (token.style) { + case 'normal': return sep.repeat(3) + case 'dashed': return sep + (' ' + sep).repeat(2) + case 'dotted': return sep + ('.' + sep).repeat(2) + } + }, + codeblock: (token: Tokens.CodeBlock) => + '```' + + token.lang + + '\n' + + token.text + + '\n```', + usage: (token: Tokens.Usage) => + '? ' + + inline(token.text) + + '\n' + + detokenize(token.content), + usages: (token: Tokens.Usages) => detokenize(token.content), + list: (token: Tokens.List) => token.children.map(detokenize).join(''), + listItem(token: Tokens.ListItem) { + let result = ' '.repeat(listLevel * 2) + + (token.order ? token.order + '. ' : '- ') + + detokenize(token.text) + listLevel += 1 + result += (token.children || []).map(detokenize).join('') + listLevel -= 1 + return result + }, + inlinelist: (token: Tokens.InlineList) => + '+ ' + + token.content.map(inline).join(' + '), + table: (token: Tokens.Table) => + token.columns.map(col => (col.bold ? '*' : '') + alignMap[col.align]).join('\t') + + '\n' + + token.data.map(row => row.map(inline).join('\t')).join('\n'), + paragraph: (token: Tokens.Paragraph) => inline(token.text), +} + +export default function detokenize(input: TokenLike[] | TokenLike): string { + if (Array.isArray(input)) { + return input.map(item => detokenize(item)).join('\n\n') + } else { + return typeof input === 'string' + ? inline(input) + : detokenizers[toCamel(input.type)](input) + } +} diff --git a/packages/detok/src/index.ts b/packages/detok/src/index.ts deleted file mode 100644 index 1ba5b73..0000000 --- a/packages/detok/src/index.ts +++ /dev/null @@ -1,117 +0,0 @@ -import cheerio from 'cheerio' -import { TokenLike, LexerToken } from '@marklet/core' -type InlineTokenTypes = 'br' | 'code' | 'span' | 'em' | 'strong' | 'del' -type BlockTokenTypes = 'text' | 'heading' | 'section' | 'quote' | 'separator' | 'codeblock' | 'usage' | 'usages' | 'list' | 'inlinelist' | 'table' | 'paragraph' - -function iterate(el: CheerioElement) { - let result = '' - for (const child of el.children) { - if (child.type === 'text') { - result += child.nodeValue - } else if (child.type === 'tag') { - result += textDetokenizers[child.tagName](child) - } - } - return result -} - -function makeSimpleWrap(leftWrap: string, rightWrap = leftWrap) { - return (el: CheerioElement) => leftWrap + iterate(el) + rightWrap -} - - -export const textDetokenizers: Record string> = { - br() { - return '\n' - }, - code(el) { - const code = el.firstChild.nodeValue - if (el.attribs.class === 'package') { - return '{{' + code + '}}' - } - const backticks = code.match(/`+/g) - const wrap = '`'.repeat(backticks === null ? 1 : Math.max(...backticks.map(b => b.length)) + 1) - return wrap + code + wrap - }, - span(el) { - const content = iterate(el) - return el.attribs.class === 'comment' ? '((' + content + '))' : '_' + content + '_' - }, - - em: makeSimpleWrap('*'), - strong: makeSimpleWrap('**'), - del: makeSimpleWrap('-') -} - -export const detokenizers: Record string> = { - text(token: string) { - const $ = cheerio.load(token) - const root = $('body') - let result = '' - root.each((_, el) => result += iterate(el)) - - return result - }, - heading(token: LexerToken) { - const prefix = '#'.repeat(token.level) - return prefix + ' ' + detokenize(token.text) - + (token.center ? ' ' + prefix : '') - }, - section(token: LexerToken) { - return '^'.repeat(token.level) + ' ' + detokenize(token.text) - }, - quote(token: LexerToken) { - return '>' + token.style + ' ' + detokenize(token.content) - }, - separator(token: LexerToken) { - const sep = token.thick ? '=' : '-' - switch (token.style) { - case 'normal': - return sep.repeat(3) - case 'dashed': - return sep + (' ' + sep).repeat(2) - case 'dotted': - return sep + ('.' + sep).repeat(2) - } - }, - codeblock(token: LexerToken) { - return '```' + token.lang + '\n' + token.text + '\n```' - }, - usage(token: LexerToken) { - return '? ' + detokenize(token.text) + '\n' + detokenize(token.content) - }, - usages(token: LexerToken) { - return detokenize(token.content) - }, - list(token: LexerToken) { - let result = '' - let count = 0 - for (const item of token.content) { - const bullet = (item).ordered ? ++count : '-' - result += ' '.repeat(token.indent) + bullet + ' ' + detokenize((item).content) - } - return result - }, - inlinelist(token: LexerToken) { - return '+' + token.content.join('+') + '+' - }, - table(/* token */) { - // TODO: add detok when lexer implement this - return '' - }, - paragraph(token: LexerToken) { - return detokenize(token.text) - } -} - -export function detokenize(input: TokenLike[] | TokenLike) { - if (Array.isArray(input)) { - let result = '' - for (const token of input) { - result += typeof token === 'string' ? detokenizers.text(token) : detokenizers[token.type](token) + '\n\n' - } - return result - } else { - return typeof input === 'string' ? detokenizers.text(input) : detokenizers[input.type](input) - } -} diff --git a/packages/detok/src/inline.ts b/packages/detok/src/inline.ts new file mode 100644 index 0000000..f1523ef --- /dev/null +++ b/packages/detok/src/inline.ts @@ -0,0 +1,47 @@ +import cheerio from 'cheerio' + +type InlineDetokenizer = (el: CheerioElement) => string + +export function iterate(el: CheerioElement) { + let result = '' + for (const child of el.children) { + if (child.type === 'text') { + result += child.nodeValue + } else if (child.type === 'tag') { + result += detokenizers[child.tagName](child) + } + } + return result +} + +function makeWrap(leftWrap: string, rightWrap: string = leftWrap): InlineDetokenizer { + return (el: CheerioElement) => leftWrap + iterate(el) + rightWrap +} + +const detokenizers: Record = { + em: makeWrap('*'), + strong: makeWrap('**'), + del: makeWrap('-'), + br: () => '\n', + code(el) { + const code = el.firstChild.nodeValue + if (el.attribs.class === 'package') { + return '{{' + code + '}}' + } + const backticks = code.match(/`+/g) + const wrap = '`'.repeat(backticks === null ? 1 : Math.max(...backticks.map(b => b.length)) + 1) + return wrap + code + wrap + }, + span(el) { + const content = iterate(el) + return el.attribs.class === 'comment' ? '((' + content + '))' : '_' + content + '_' + }, + a: (el) => `[${el.firstChild.nodeValue}|${el.attribs['data-raw-url']}]`, + img: (el) => `[${el.firstChild.nodeValue}|!${el.attribs.src}]` +} + +export default function detokenize(token: string) { + let result = '' + cheerio.load(token)('body').each((_, el) => result += iterate(el)) + return result +} diff --git a/packages/dev-server/README.md b/packages/dev-server/README.md new file mode 100644 index 0000000..905fe1d --- /dev/null +++ b/packages/dev-server/README.md @@ -0,0 +1,8 @@ +# @marklet/dev-server + +[![Build Status](https://travis-ci.com/obstudio/Marklet.svg?branch=dev)](https://travis-ci.com/obstudio/Marklet) +[![dependency](https://img.shields.io/david/obstudio/Marklet.svg?path=packages%2Fdev-server)](https://github.com/obstudio/Marklet/blob/master/packages/dev-server/package.json) +[![npm](https://img.shields.io/npm/v/@marklet/dev-server.svg)](https://www.npmjs.com/package/@marklet/dev-server) +[![npm bundle size (minified)](https://img.shields.io/bundlephobia/min/@marklet/dev-server.svg)](https://www.npmjs.com/package/@marklet/dev-server) + +A develop server for marklet. diff --git a/packages/dev-server/comp/edit.vue b/packages/dev-server/comp/edit.vue index 5ebb62c..8fe1e49 100644 --- a/packages/dev-server/comp/edit.vue +++ b/packages/dev-server/comp/edit.vue @@ -17,7 +17,7 @@ module.exports = {