diff --git a/.gitignore b/.gitignore index 1e5f855..7c6dc14 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,4 @@ temp *.tgz *.cache.* packages/app/main.js -packages/app/index.html -packages/monaco/index.css -packages/dev-server/themes/*.json \ No newline at end of file +packages/app/index.html \ No newline at end of file diff --git a/build/build.js b/build/build.js index 682d6a5..b33be0f 100644 --- a/build/build.js +++ b/build/build.js @@ -37,13 +37,14 @@ const bundle = (name, options) => new Promise((resolve, reject) => { entry: util.resolve(name, options.entry), resolve: { alias: { - '@': util.resolve(name, 'temp') + '@': util.resolve(name, 'temp'), + '@@': util.resolve('..'), } }, output: { path: util.resolve(name, 'dist'), filename: options.output, - library: 'Marklet', + library: 'marklet', libraryTarget: 'umd', libraryExport: options.libraryExport, globalObject: 'typeof self !== \'undefined\' ? self : this' @@ -51,6 +52,10 @@ const bundle = (name, options) => new Promise((resolve, reject) => { }) new webpack.ProgressPlugin().apply(compiler) + + new webpack.DefinePlugin({ + 'process.env.MARKLET_ENV': JSON.stringify(env), + }).apply(compiler) compiler.run((error, stat) => { if (error) { @@ -104,14 +109,15 @@ Promise.resolve().then(() => { }).then(() => { if (!program.server) return mkdirIfNotExists('dev-server/dist') + mkdirIfNotExists('dev-server/dist/themes') if (program.tsc) { util.exec('tsc -p packages/dev-server') } - function minifyHTML(type) { - const srcPath = util.resolve(`dev-server/src/${type}.html`) - const distPath = util.resolve(`dev-server/dist/${type}.html`) + function minifyHTML(src, dist) { + const srcPath = util.resolve(`dev-server/${src}/index.html`) + const distPath = util.resolve(`dev-server/${dist}/index.html`) if (program.prod) { fs.writeFileSync( distPath, @@ -125,14 +131,9 @@ Promise.resolve().then(() => { } } - minifyHTML('edit') - minifyHTML('watch') + minifyHTML('server', 'dist') let css = '' - themes.forEach(({ key }) => { - const options = yaml.safeLoad(fs.readFileSync(util.resolve(`dev-server/themes/${key}.yaml`))) - fs.writeFileSync(util.resolve(`dev-server/themes/${key}.json`), JSON.stringify(options)) - }) return sfc2js.transpile({ ...sfc2jsOptions, @@ -143,10 +144,11 @@ Promise.resolve().then(() => { if (result.errors.length) throw result.errors.join('\n') return Promise.all(themes.map(({ key }) => new Promise((resolve, reject) => { const filepath = util.resolve('dev-server/themes/' + key) + const distpath = util.resolve('dev-server/dist/themes/' + key) try { const options = yaml.safeLoad(fs.readFileSync(filepath + '.yaml')) - fs.writeFileSync(filepath + '.json', JSON.stringify(options)) + fs.writeFileSync(distpath + '.json', JSON.stringify(options)) } catch (error) { reject(error) } @@ -167,7 +169,7 @@ Promise.resolve().then(() => { fs.writeFileSync(util.resolve('dev-server/dist/themes.min.css'), css) return new Promise((resolve, reject) => { sass.render({ - data: fs.readFileSync(util.resolve('dev-server/src/monaco.scss')).toString(), + data: fs.readFileSync(util.resolve('dev-server/client/monaco.scss')).toString(), outputStyle: 'compressed', }, (error, result) => { if (error) reject(error) @@ -176,9 +178,9 @@ Promise.resolve().then(() => { }) }) }).then(() => bundle('dev-server', { - entry: 'dist/client.js', + entry: 'dist/client/index.js', output: 'client.min.js', - libraryExport: 'Marklet', + libraryExport: 'default', })) }).catch((error) => { console.log(error) diff --git a/package.json b/package.json index 8050427..a9c05c1 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@sfc2js/sass": "^2.0.0", "@types/cheerio": "^0.22.9", "@types/js-yaml": "^3.11.2", + "@types/lodash.debounce": "^4.0.4", "@types/node": "^10.12.0", "@types/ws": "^6.0.1", "ajv": "^6.5.4", diff --git a/packages/app/package.json b/packages/app/package.json index 558d4ca..7ee9c79 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@marklet/app", - "version": "1.0.11", + "version": "1.0.12", "private": true, "main": "main.js", "author": "shigma <1700011071@pku.edu.cn>", @@ -18,8 +18,8 @@ }, "dependencies": { "@marklet/monaco": "^1.3.0", - "@marklet/parser": "^1.5.1", - "@marklet/renderer": "^1.3.1", + "@marklet/parser": "^1.5.2", + "@marklet/renderer": "^1.3.2", "js-yaml": "^3.12.0", "neat-scroll": "^2.0.1", "vue": "^2.5.17" diff --git a/packages/cli/README.md b/packages/cli/README.md index dc2b1ea..a9fa284 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -50,7 +50,10 @@ Parse a marklet file into marklet AST. Options: (support config options) + -B, --no-bound prevent from recording token bounds -f, --format [format] the output format (default: "json") + -i, --indent [length] set the indent length (default: 2) + -p, --pretty pretty printed (it overrides all other options) ``` ### edit diff --git a/packages/cli/commands/build.js b/packages/cli/commands/build.js index de3fac8..83cf9f6 100644 --- a/packages/cli/commands/build.js +++ b/packages/cli/commands/build.js @@ -1,3 +1,5 @@ +const path = require('path') + module.exports = program => program .command('build [filepath|dirpath]') .description('Build a marklet project into a website.') @@ -6,6 +8,7 @@ module.exports = program => program // .option('-d, --dest [path]', 'Write parsed data to file instead of stdin') .allowConfig() .action(function(filepath = '') { - const options = this.getOptions(filepath) + const options = this.getOptions() + options.filepath = path.resolve(process.cwd(), filepath) console.log(options) }) diff --git a/packages/cli/commands/edit.js b/packages/cli/commands/edit.js index 49f59dd..aa07888 100644 --- a/packages/cli/commands/edit.js +++ b/packages/cli/commands/edit.js @@ -1,4 +1,5 @@ const open = require('opn') +const util = require('../util') const server = require('@marklet/dev-server') module.exports = program => program @@ -6,8 +7,11 @@ module.exports = program => program .description('Edit a marklet file or project.') .allowPort() .allowConfig() - .action(function(filepath = '') { - const options = this.getOptions(filepath, false) + .action(function(filepath) { + const options = this.getOptions() + if (filepath) { + options.filepath = util.fullPath(filepath) + } server.edit(options) if (this.open) { open(`http://localhost:${options.port || 8080}`) diff --git a/packages/cli/commands/parse.js b/packages/cli/commands/parse.js index c182286..27020e1 100644 --- a/packages/cli/commands/parse.js +++ b/packages/cli/commands/parse.js @@ -4,46 +4,42 @@ const yaml = require('js-yaml') const chalk = require('chalk') const parser = require('@marklet/parser') -function printNode(node, indent = 2, back = 1) { - const space = ' '.repeat(indent - 2 * back) - const prefix = '- '.repeat(back) - if (typeof node === 'string') { - return console.log(prefix + chalk.yellow(node)) +function isObject(node) { + return node === null || typeof node !== 'object' +} + +function prettyPrint(node, indent = 0) { + const space = ' '.repeat(indent) + if (isObject(node)) { + if (typeof node === 'string') { + console.log(chalk.yellowBright(node)) + } else { + console.log(chalk.cyanBright(node)) + } } else if (node instanceof Array) { - return node.forEach((node, index) => { - if (node instanceof Array) { - printNode(node, indent + 2, back + 1) - } else { - process.stdout.write(space + ' '.repeat(index ? back : 1)) - printNode(node, indent + 2, index ? 1 : back) - } + node.forEach((node, index) => { + if (index) process.stdout.write(space) + process.stdout.write('- ') + prettyPrint(node, indent + 1) }) - } - let firstLine = !node.type - if (node.type) { - console.log(chalk.greenBright('# ' + node.type)) - } - for (const key in node) { - if (key === 'type') continue - if (firstLine) { - process.stdout.write(prefix) - firstLine = false - } else { - process.stdout.write(space + ' ') + } else { + let firstLine = !node.type + if (node.type) { + console.log(chalk.redBright('\b\b# ' + node.type)) } - process.stdout.write(chalk`{cyanBright ${key}}: `) - if (typeof node[key] === 'string') { - console.log(chalk.yellow(node[key])) - } else if (typeof node[key] !== 'object') { - console.log(chalk.magentaBright(node[key])) - } else { - process.stdout.write('\n') - if (node[key] instanceof Array) { - printNode(node[key], indent + 2 * back, 1) + for (const key in node) { + if (key === 'type') continue + if (!firstLine) { + process.stdout.write(space) } else { - process.stdout.write(space + ' ') - printNode(node[key], indent + 2 * back - 2, 0) + firstLine = false } + process.stdout.write(chalk.magentaBright(key) + ': ') + if (!isObject(node[key])) { + process.stdout.write('\n' + space) + if (node[key] instanceof Array) process.stdout.write(' ') + } + prettyPrint(node[key], indent + 1) } } } @@ -69,17 +65,12 @@ module.exports = program => program }, }) if (this.pretty) { - result.forEach(node => printNode(node)) - return - } - switch (this.format || 'json') { - case 'json': + prettyPrint(result) + } else if (this.format === 'json' || !this.format) { console.log(JSON.stringify(result, null, indent)) - break - case 'yaml': + } else if (this.format === 'yaml') { console.log(yaml.safeDump(result, { indent })) - break - default: + } else { util.handleError(this.format + ' is not a supported format.') } }) diff --git a/packages/cli/commands/watch.js b/packages/cli/commands/watch.js index 66c975a..98a3ac5 100644 --- a/packages/cli/commands/watch.js +++ b/packages/cli/commands/watch.js @@ -1,4 +1,5 @@ const open = require('opn') +const util = require('../util') const server = require('@marklet/dev-server') module.exports = program => program @@ -6,8 +7,9 @@ module.exports = program => program .description('Watch a marklet file or project.') .allowPort() .allowConfig() - .action(function(filepath = '') { - const options = this.getOptions(filepath) + .action(function(filepath) { + const options = this.getOptions() + options.filepath = util.fullPath(filepath) server.watch(options) if (this.open) { open(`http://localhost:${options.port || 8080}`) diff --git a/packages/cli/package.json b/packages/cli/package.json index 1e639c2..8e471fa 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@marklet/cli", - "version": "2.0.1", + "version": "2.0.2", "description": "A command line interface for marklet.", "author": "jjyyxx <1449843302@qq.com>", "contributors": [ @@ -19,11 +19,10 @@ "url": "https://github.com/obstudio/Marklet/issues" }, "dependencies": { - "@marklet/dev-server": "^1.1.2", - "@marklet/parser": "^1.5.1", + "@marklet/dev-server": "^1.1.3", + "@marklet/parser": "^1.5.2", "chalk": "^2.4.1", "commander": "^2.18.0", - "js-yaml": "^3.12.0", "opn": "^5.4.0" } } \ No newline at end of file diff --git a/packages/cli/program.js b/packages/cli/program.js index 699b6ef..5dbb5ce 100644 --- a/packages/cli/program.js +++ b/packages/cli/program.js @@ -1,31 +1,6 @@ -const fs = require('fs') -const path = require('path') -const yaml = require('js-yaml') -const util = require('./util') const program = require('commander') const { DEFAULT_PORT } = require('@marklet/dev-server') -const JS_TYPES = ['.json', '.js'] -const YAML_TYPES = ['.yml', '.yaml'] -const MARK_TYPES = ['.mkl', '.md'] -const ALL_TYPES = [ - ...JS_TYPES, - ...YAML_TYPES, -] - -function loadFromFile(filepath) { - const ext = path.extname(filepath) - if (YAML_TYPES.includes(ext)) { - return yaml.safeLoad(fs.readFileSync(filepath).toString()) - } else if (JS_TYPES.includes(ext)) { - return require(filepath) - } else if (ext) { - throw new Error(`error: cannot recognize file extension '${ext}'.`) - } else { - throw new Error('error: cannot recognize file with no extenstion.') - } -} - Object.assign(Object.getPrototypeOf(program), { apply(install) { install(program) @@ -41,7 +16,7 @@ Object.assign(Object.getPrototypeOf(program), { allowPort() { this._allowPort = true return this - .option('-o, --open', 'open in the default browser') + .option('-o, --open', 'show in browser when connection is established') .option('-p, --port [port]', 'port for the development server', parseInt, DEFAULT_PORT) }, getConfig() { @@ -51,46 +26,19 @@ Object.assign(Object.getPrototypeOf(program), { if (this.language) config.default_language = this.language return config }, - getOptions(filepath = '', forced = true) { - filepath = path.resolve(filepath) - let basePath = path.resolve(process.cwd(), filepath) - let options = { sourceType: 'project' } - try { - util.tryFindFile(filepath) - if (fs.statSync(basePath).isFile()) { - if (!MARK_TYPES.includes(path.extname(basePath))) { - Object.assign(options, loadFromFile(basePath)) - } else { - options.sourceType = 'file' - } - } else { - let matchFound = false - basePath = path.join(basePath, 'marklet') - for (const type of ALL_TYPES) { - const filename = basePath + type - if (!fs.existsSync(filename)) continue - if (fs.statSync(filename).isFile()) { - Object.assign(options, loadFromFile(filename)) - matchFound = true - break - } - } - if (!matchFound && forced) { - throw new Error(`no config file was found in ${basePath}.`) - } - } - if (this._allowPort) { - if (this.port) options.port = this.port - } - if (this._allowConfig) { - if (!options.config) options.config = {} - Object.assign(options.config, this.getConfig()) - } - } catch (error) { - util.handleError(error.message) + getOptions() { + let options = {} + if (this._allowPort) { + if (this.port) options.port = this.port + } + if (this._allowConfig) { + if (!options.parseOptions) options.parseOptions = {} + Object.assign(options.parseOptions, this.getConfig()) } return options - } + }, }) +program._optionHooks = [] + module.exports = program diff --git a/packages/core/package.json b/packages/core/package.json index 391ede9..a52140c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@marklet/core", - "version": "3.2.2", + "version": "3.2.4", "description": "Some core conceptions of marklet.", "author": "shigma <1700011071@pku.edu.cn>", "contributors": [ diff --git a/packages/core/src/lexer.ts b/packages/core/src/lexer.ts index eba9603..113c8a3 100644 --- a/packages/core/src/lexer.ts +++ b/packages/core/src/lexer.ts @@ -69,23 +69,27 @@ export interface LexerRegexRule< eol?: boolean } +interface MacroRegExp extends RegExp { + target?: RegExp + macro?: string +} + export class MacroMap { - private data: Record = {} + private data: Record = {} constructor(macros: LexerMacros = {}, config: LexerConfig = {}) { if (typeof macros === 'function') macros = macros(config) for (const key in macros) { - this.data[key] = { - regex: new RegExp(`{{${key}}}`, 'g'), - macro: `(?:${getString(macros[key])})`, - } + this.data[key] = getRegExp(macros[key]) + this.data[key].target = new RegExp(`{{${key}}}`, 'g') + this.data[key].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) + source = source.replace(this.data[key].target, this.data[key].macro) } return source } @@ -98,6 +102,11 @@ export function getString(source: StringLike): string { return source instanceof RegExp ? source.source : source } +/** transform a string-like object into a raw regexp */ +export function getRegExp(source: StringLike): RegExp { + return source instanceof RegExp ? source : new RegExp(source) +} + export function isStringLike(source: any): boolean { return typeof source === 'string' || source instanceof RegExp } diff --git a/packages/detok/package.json b/packages/detok/package.json index a155548..6c8a7d3 100644 --- a/packages/detok/package.json +++ b/packages/detok/package.json @@ -1,6 +1,6 @@ { "name": "@marklet/detok", - "version": "1.1.3", + "version": "1.1.4", "description": "A detokenizer for marklet.", "author": "jjyyxx <1449843302@qq.com>", "contributors": [ @@ -25,7 +25,7 @@ "cheerio": "^1.0.0-rc.2" }, "devDependencies": { - "@marklet/core": "^3.2.2", - "@marklet/parser": "^1.5.1" + "@marklet/core": "^3.2.3", + "@marklet/parser": "^1.5.2" } } \ No newline at end of file diff --git a/packages/dev-server/client/files.ts b/packages/dev-server/client/files.ts new file mode 100644 index 0000000..d61217f --- /dev/null +++ b/packages/dev-server/client/files.ts @@ -0,0 +1,115 @@ +import Monaco from 'monaco-editor' +import Marklet from './index' + +let count = 0 + +export interface FileOptions { + title?: string + value?: string + path?: string + origin?: string + id?: string + changed?: boolean +} + +class MarkletModel { + public value: string + public title: string + public path: string + public origin: string + public changed: boolean + public id: string + + private marklet: typeof Marklet + public model: Monaco.editor.ITextModel + public viewState: Monaco.editor.ICodeEditorViewState + + constructor(marklet: typeof Marklet, options: FileOptions = {}) { + this.value = options.value + this.path = options.path + this.origin = options.origin + this.changed = options.changed + this.title = options.title || `Untitled ${++count}` + this.id = options.id || Math.floor(Math.random() * 36 ** 6).toString(36).padStart(6, '0') + Object.defineProperty(this, 'marklet', { + configurable: false, + value: marklet, + }) + marklet.$on('monaco.loaded', (monaco: typeof Monaco) => { + Object.defineProperty(this, 'model', { + configurable: false, + value: monaco.editor.createModel(this.value, 'marklet') + }) + this.model.onDidChangeContent(() => this.checkChange()) + }) + } + + dispose() { + this.model.dispose() + } + + checkChange(data?: string) { + if (data !== undefined) this.origin = data + this.value = this.model.getValue( + this.marklet.editOptions.line_ending === 'LF' ? 1 : 2 + ) + this.changed = this.origin !== this.value + } + + isEmpty() { + return this.path === null && this.origin === '' && this.model.getValue(1) === '' + } + + toJSON() { + return { + title: this.title, + value: this.value, + path: this.path, + id: this.id, + } + } +} + +export default class ContentManager { + private data: MarkletModel[] = [] + private marklet: typeof Marklet + + constructor(marklet: typeof Marklet) { + // FIXME: get data from local storage + Object.defineProperty(this, 'marklet', { + configurable: false, + value: marklet, + }) + this.add({ + path: '__untitled__', + title: 'untitled', + }) + + addEventListener('beforeunload', () => { + // FIXME: save to local storage + }) + } + + add(options: FileOptions) { + const instance = this.get(options.path) + if (instance) { + // FIXME: handle other properties updating + instance.value = options.value + if (instance.model) instance.model.setValue(options.value) + } else { + this.data.push(new MarkletModel(this.marklet, options)) + } + } + + get(path: string) { + return this.data.find(instance => path === instance.path) + } + + each(callback: (file: MarkletModel, index: number) => void) { + this.data.forEach(callback) + } + + toJSON() { + return this.data + } +} diff --git a/packages/dev-server/client/index.ts b/packages/dev-server/client/index.ts new file mode 100644 index 0000000..dcf8137 --- /dev/null +++ b/packages/dev-server/client/index.ts @@ -0,0 +1,139 @@ +import VueConstructor from 'vue' +import Monaco from 'monaco-editor' +import ContentManager from './files' +import defineLanguage from './language' +import * as renderer from '@marklet/renderer' +import { LexerConfig } from '@marklet/parser' +import { SourceType, ServerType, EditorConfig } from '../server' + +declare global { + export const Vue: typeof VueConstructor +} + +Vue.use(renderer) + +const typeMap = { + edit: '编辑', + watch: '监视', +} + +const app = require('@/app.vue') + +export default new class MarkletClient { + public app: VueConstructor + public serverType: ServerType + public sourceType: SourceType + public editOptions: EditorConfig + public parseOptions: LexerConfig + public files: ContentManager + + private url: string + private retry: boolean + private timeout: number + private _interval: number + private msgQueue: string[] + private timerRef: number + private socket: WebSocket + private events: VueConstructor + + constructor({ + url = `ws://${location.host}/`, + retry = true, + timeout = 5000, + interval = 1000, + } = {}) { + this.url = url + this.retry = retry + this.timeout = timeout + this.msgQueue = [] + this.events = new Vue() + this.files = new ContentManager(this) + this.createWebSocket() + this.interval = interval + + this.$on('monaco.loaded', (monaco: typeof Monaco) => { + monaco.languages.register({ + id: 'marklet', + extensions: ['mkl', 'md'], + }) + monaco.languages.setMonarchTokensProvider('marklet', defineLanguage()) + this.$emit('monaco.language.loaded', monaco) + monaco.editor.defineTheme('dark', require('../themes/dark')) + monaco.editor.defineTheme('simple', require('../themes/simple')) + if (!Vue.prototype.$colorize) { + Vue.prototype.$colorize = function(code: string, lang: string) { + return monaco.editor.colorize(code, lang, {}) + } + } + this.$emit('monaco.theme.loaded', monaco) + }) + + addEventListener('beforeunload', () => this.dispose()) + } + + public start(el: string | HTMLElement) { + document.title = 'Marklet - ' + typeMap[this.serverType] + return this.app = new Vue(app).$mount(el) + } + + public $on(event: string | string[], callback: Function) { + this.events.$on(event, callback) + } + + public $once(event: string, callback: Function) { + this.events.$once(event, callback) + } + + public $emit(event: string, ...args: any[]) { + this.events.$emit(event, ...args) + } + + public get interval() { + return this._interval + } + + public set interval(value: number) { + this._interval = value + window.clearInterval(this.timerRef) + this.timerRef = window.setInterval(() => { + while (this.msgQueue.length > 0 && this.socket.readyState === 1) { + this.socket.send(this.msgQueue.shift()) + } + }, this._interval) + } + + private createWebSocket() { + this.socket = new WebSocket(this.url) + this.socket.addEventListener('close', (event) => { + if (event.code !== 1000 && this.retry) { + this.$emit('ws.reconnect') + setTimeout(() => this.createWebSocket(), this.timeout) + } else { + this.$emit('ws.close') + } + }) + this.socket.addEventListener('error', () => { + this.$emit('ws.error') + }) + this.socket.addEventListener('message', (event) => { + try { + const data = JSON.parse(event.data) + this.$emit('server.message', data) + this.$emit('server.' + data.type, data) + } catch (error) { + this.$emit('server.error', error) + } + }) + this.socket.addEventListener('open', () => { + this.$emit('ws.open') + }) + this.$on('client.message', (data: object) => { + this.msgQueue.push(JSON.stringify(data)) + }) + } + + private dispose() { + if (this.socket.readyState < 2) this.socket.close() + clearInterval(this.timerRef) + } +} diff --git a/packages/dev-server/src/language.ts b/packages/dev-server/client/language.ts similarity index 82% rename from packages/dev-server/src/language.ts rename to packages/dev-server/client/language.ts index 4f2cdac..d08f181 100644 --- a/packages/dev-server/src/language.ts +++ b/packages/dev-server/client/language.ts @@ -2,7 +2,7 @@ import { LexerConfig } from '@marklet/parser' import Monaco from 'monaco-editor' export default function(config: LexerConfig = {}) { - const language: Monaco.languages.IMonarchLanguage = { + return { tokenizer: { root: [ { include: 'topLevel' }, @@ -16,7 +16,5 @@ export default function(config: LexerConfig = {}) { } ], }, - } - - return language + } as Monaco.languages.IMonarchLanguage } diff --git a/packages/dev-server/src/monaco.scss b/packages/dev-server/client/monaco.scss similarity index 91% rename from packages/dev-server/src/monaco.scss rename to packages/dev-server/client/monaco.scss index 047ae73..f6e0dd8 100644 --- a/packages/dev-server/src/monaco.scss +++ b/packages/dev-server/client/monaco.scss @@ -1,4 +1,8 @@ .monaco-editor { + .margin-view-overlays { + user-select: none; + } + .decorationsOverviewRuler { display: none; } diff --git a/packages/dev-server/comp/.eslintrc.yml b/packages/dev-server/comp/.eslintrc.yml index c3d454b..a36bd71 100644 --- a/packages/dev-server/comp/.eslintrc.yml +++ b/packages/dev-server/comp/.eslintrc.yml @@ -1,3 +1,7 @@ extends: - plugin:vue/essential + +globals: + monaco: true + marklet: true \ No newline at end of file diff --git a/packages/dev-server/comp/app.vue b/packages/dev-server/comp/app.vue index 7a7932b..e545951 100644 --- a/packages/dev-server/comp/app.vue +++ b/packages/dev-server/comp/app.vue @@ -6,40 +6,38 @@ const MIN_WIDTH = 0.1 module.exports = { mixins: [ - require('./menu'), require('./editor'), ], + components: { + fileTree: require('./tree.vue') + }, + + provide() { + return { $editor: this } + }, + data: () => ({ + themes, theme: 'dark', dragging: false, display: { document: { show: true, - width: 0.35, + width: 0.4, }, editor: { show: true, - width: 0.35, + width: 0.4, }, explorer: { show: false, - width: 0.3, + width: 0.2, }, }, }), computed: { - lists() { - return { - themes: { - data: themes, - current: this.theme, - switch: 'setTheme', - prefix: '主题:', - }, - } - }, totalWidth() { return this.display.editor.width * this.display.editor.show + this.display.explorer.width * this.display.explorer.show @@ -135,6 +133,14 @@ module.exports = { }, }, + created() { + this.$set(this.display.editor, 'show', this._enableEdit) + this.$set(this.display.explorer, 'show', this._isProject) + + this.$registerCommands(require('./command')) + this.$registerMenus(require('./menus')) + }, + mounted() { window.vm = this @@ -195,6 +201,9 @@ module.exports = { window.open(url) }, triggerArea(area) { + if (area === 'explorer' && !this._isProject) return + if (area === 'editor' && !this._enableEdit) return + this.display[area].show = !this.display[area].show if (this.display.editor.show) { this.$nextTick(() => this.layout(300)) @@ -207,7 +216,7 @@ module.exports = { } }, startDrag(position, event) { - this.hideContextMenus() + this.$menuManager.hideAllMenus() this.dragging = position this.$refs[position].classList.add('active') this.draggingLastX = event.clientX @@ -218,16 +227,12 @@ module.exports = { diff --git a/packages/dev-server/comp/menu/command.json b/packages/dev-server/comp/command.json similarity index 88% rename from packages/dev-server/comp/menu/command.json rename to packages/dev-server/comp/command.json index 04bf83c..b047142 100644 --- a/packages/dev-server/comp/menu/command.json +++ b/packages/dev-server/comp/command.json @@ -2,7 +2,8 @@ { "method": "openFile", "bind": "ctrl+o", - "name": "打开 ..." + "name": "打开", + "ellipsis": true }, { "method": "save", @@ -12,7 +13,8 @@ { "method": "saveAs", "bind": "ctrl+shift+s", - "name": "另存为 ..." + "name": "另存为", + "ellipsis": true }, { "method": "saveAll", @@ -24,6 +26,8 @@ "arguments": "explorer", "key": "trigger-explorer", "name": "资源管理器", + "enabled": "$_isProject", + "bind": "ctrl+k ctrl+r", "checked": "$display.explorer.show" }, { @@ -31,6 +35,8 @@ "arguments": "editor", "key": "trigger-editor", "name": "编辑器视图", + "enabled": "$_enableEdit", + "bind": "ctrl+k ctrl+e", "checked": "$display.editor.show" }, { @@ -38,6 +44,7 @@ "arguments": "document", "key": "trigger-document", "name": "预览视图", + "bind": "ctrl+k ctrl+d", "checked": "$display.document.show" }, { @@ -86,7 +93,8 @@ "arguments": "editor.action.quickCommand", "key": "command-palette", "bind": "!f1", - "name": "命令面板" + "name": "命令面板", + "enabled": "$display.editor.show" }, { "method": "executeAction", diff --git a/packages/dev-server/comp/editor.js b/packages/dev-server/comp/editor.js index 2dc2aa2..c9bde48 100644 --- a/packages/dev-server/comp/editor.js +++ b/packages/dev-server/comp/editor.js @@ -1,67 +1,67 @@ const { DocumentLexer, defaultConfig } = require('@marklet/parser') +const saveAs = require('file-saver') module.exports = { data: () => ({ + tree: [], nodes: [], - origin: '', - source: '', - loading: 1, - changed: false, - config: defaultConfig, + files: marklet.files, + path: '__untitled__', + config: { + ...defaultConfig, + ...marklet.parseOptions, + }, }), + computed: { + current() { + return this.files.get(this.path) + }, + }, + watch: { - source(value) { - this.nodes = this._lexer.parse(value) + 'current.value': 'parse', + path() { + if (!this._editor) return + this._editor.setModel(this.current.model) }, config: { deep: true, - handler(value) { - this._lexer = new DocumentLexer(value) - this.nodes = this._lexer.parse(this.source) + handler(config) { + this._lexer = new DocumentLexer(config) + this.parse() }, }, - loading(value) { - if (!value) { - this._editor.setModel(this._model) - this.$nextTick(() => this.layout()) - } - }, }, created() { - this._lexer = new DocumentLexer(defaultConfig) - - const source = localStorage.getItem('source') - if (typeof source === 'string') this.source = source - - this.$eventBus.$on('server.config', (config) => { - this.config = Object.assign(defaultConfig, config) - }) + this._enableEdit = marklet.serverType === 'edit' + this._isProject = marklet.sourceType !== 'file' + this._lexer = new DocumentLexer(this.config) }, mounted() { - this.$eventBus.$on('server.document', (doc) => { - this.openFile(doc) + marklet.$on('server.entries', ({ tree }) => { + this.tree = tree }) - this.$eventBus.$on('monaco.theme.loaded', (monaco) => { - monaco.editor.setTheme(this.theme) + marklet.$on('server.document', ({ value, path }) => { + this.files.add({ value, path }) + if (this.path === '__untitled__') { + this.path = path + } }) - this.$eventBus.$on('monaco.loaded', (monaco) => { - const model = monaco.editor.createModel(this.source, 'marklet') - model.onDidChangeContent(() => this.checkChange()) - const nodes = this.nodes - this.nodes = [] - this.$nextTick(() => this.nodes = nodes) - this._model = model + marklet.$on('monaco.theme.loaded', (monaco) => { + monaco.editor.setTheme(this.theme) + }) + marklet.$on('monaco.loaded', (monaco) => { if (this._editor) return this._editor = monaco.editor.create(this.$refs.editor, { model: null, language: 'marklet', - lineDecorationsWidth: 4, + lineDecorationsWidth: 8, scrollBeyondLastLine: false, minimap: { enabled: false }, scrollbar: { @@ -73,40 +73,62 @@ module.exports = { this.row = event.position.lineNumber this.column = event.position.column }) - this.loading = 0 - }) - - addEventListener('beforeunload', () => { - localStorage.setItem('source', this.source) + this.$nextTick(() => this.activate()) }) }, methods: { - openFile(doc) { - if (this.loading) { - this.source = doc - } else { - this._model.setValue(doc) - } + parse(source = this.current.value) { + this.nodes = this._lexer.parse(source) + }, + switchTo(path) { + if (this.path === path) return + if (!this.files.get(path)) return + this.current.viewState = this._editor.saveViewState() + this.path = path + this.activate() + }, + openFile() { + // }, save() { - this.$eventBus.$emit('client.message', 'save', this.source) // TODO: maybe file name is needed. depend on backend impl + marklet.$emit('client.message', { + type: 'save', + path: this.path, + value: this.current.value, + }) }, saveAs() { - this.$eventBus.$emit('client.message', 'saveAs', { - source: this.source, - name: '' // TODO: read file name + const blob = new Blob([this.current.value], { + type: 'text/plain;charset=utf-8' }) + saveAs(blob, this.path.match(/[^/]*$/)[0] || 'download.mkl') }, saveAll() { - // TODO: unclear requirement + this.files.each((file) => { + if (!file.changed) return + marklet.$emit('client.message', { + type: 'save', + path: file.path, + value: file.value, + }) + }) }, - checkChange(data) { - if (data !== undefined) this.origin = data - this.source = this._model.getValue() - this.changed = this.origin !== this.source + activate() { + if (!this._editor) return + monaco.editor.setTheme(this.theme) + this._editor.setModel(this.current.model) + if (this.current.viewState) { + this._editor.restoreViewState(this.current.viewState) + } + const position = this._editor.getPosition() + this.row = position.lineNumber + this.column = position.column + this.parse() + this.layout() }, layout(deltaTime = 0) { + if (!this._editor) return const now = performance.now(), self = this this._editor._configuration.observeReferenceElement() this._editor._view._actualRender() diff --git a/packages/dev-server/comp/menu/index.js b/packages/dev-server/comp/menu/index.js deleted file mode 100644 index 9fe9b7a..0000000 --- a/packages/dev-server/comp/menu/index.js +++ /dev/null @@ -1,157 +0,0 @@ -const Mousetrap = require('mousetrap') - -// TODO: improve this pattern maybe. -// Cause: firing event inside textarea is blocked by mousetrap by default. -Mousetrap.prototype.stopCallback = () => false - -function toKebab(camel) { - return camel.replace(/[A-Z]/g, char => '-' + char.toLowerCase()) -} - -const commands = {} -for (const command of require('./command.json')) { - const key = command.key ? command.key : toKebab(command.method) - commands[key] = command -} - -const menus = require('./menus.json') -const menuData = {} -const menuKeys = Object.keys(menus) -for (const key of menuKeys) { - menuData[key] = { - show: false, - content: menus[key], - embed: new Array(menus[key].length).fill(false) - } -} - -module.exports = { - components: { - MarkletMenu: require('./menu-manager.vue'), - }, - - data() { - return { - menuData, - menubarMove: 0, - menubarActive: false, - altKey: false, - } - }, - - provide() { - return { - menuKeys, - commands, - $menu: this, - } - }, - - mounted() { - for (const key in commands) { - const command = commands[key] - if (!command.bind || command.bind.startsWith('!')) continue - Mousetrap.bind(command.bind, () => { - this.executeCommand(command) - return false - }) - } - this.menuReference = {} - for (let index = 0; index < menuKeys.length; index++) { - this.menuReference[menuKeys[index]] = this.$refs.menus.$el.children[index] - } - }, - - methods: { - executeMethod(key, ...args) { - const method = this[key] - if (method instanceof Function) method(...args) - }, - executeCommand(command) { - const method = this[command.method] - if (!(method instanceof Function)) { - console.error(`No method ${command.method} was found!`) - return - } - let args = command.arguments - if (args === undefined) args = [] - if (!(args instanceof Array)) args = [args] - method(...args.map(arg => this.parseArgument(arg))) - }, - parseArgument(arg) { - if (typeof arg === 'string' && arg.startsWith('$')) { - return arg.slice(1).split('.').reduce((prev, curr) => prev[curr], this) - } else { - return arg - } - }, - hideContextMenus() { - this.menubarActive = false - for (const key in this.menuData) { - this.menuData[key].show = false - for (let index = 0; index < this.menuData[key].embed.length; index++) { - this.menuData[key].embed.splice(index, 1, false) - } - } - }, - showContextMenu(key, event) { - const style = this.menuReference[key].style - this.hideContextMenus() - this.locateMenuAtClient(event, style) - this.menuData[key].show = true - }, - locateMenuAtClient(event, style) { - if (event.clientX + 200 > this.width) { - style.left = event.clientX - 200 - this.left + 'px' - } else { - style.left = event.clientX - this.left + 'px' - } - if (event.clientY - this.top > this.height / 2) { - style.top = '' - style.bottom = this.top + this.height - event.clientY + 'px' - } else { - style.top = event.clientY - this.top + 'px' - style.bottom = '' - } - }, - hoverMenu(index, event) { - if (this.menubarActive && !this.menuData.menubar.embed[index]) { - this.showMenu(index, event) - } - }, - showButtonMenu(key, event) { - const style = this.menuReference[key].style - this.hideContextMenus() - this.locateMenuAtButton(event, style) - this.menuData[key].show = true - }, - showMenu(index, event) { - const style = this.menuReference.menubar.style - const last = this.menuData.menubar.embed.indexOf(true) - if (last === index) { - this.menubarActive = false - this.menuData.menubar.show = false - this.menuData.menubar.embed.splice(index, 1, false) - return - } else if (last === -1) { - this.menubarMove = 0 - } else { - this.menubarMove = index - last - } - this.hideContextMenus() - this.locateMenuAtButton(event, style) - this.menubarActive = true - this.menuData.menubar.show = true - this.menuData.menubar.embed.splice(index, 1, true) - }, - locateMenuAtButton(event, style) { - const rect = event.currentTarget.getBoundingClientRect() - if (rect.left + 200 > this.width) { - style.left = rect.left + rect.width - 200 + 'px' - } else { - style.left = rect.left + 'px' - } - style.top = rect.top + rect.height + 'px' - }, - }, -} diff --git a/packages/dev-server/comp/menu/menu-item.vue b/packages/dev-server/comp/menu/menu-item.vue deleted file mode 100644 index 2305989..0000000 --- a/packages/dev-server/comp/menu/menu-item.vue +++ /dev/null @@ -1,51 +0,0 @@ - - - - - diff --git a/packages/dev-server/comp/menu/menu-list.vue b/packages/dev-server/comp/menu/menu-list.vue deleted file mode 100644 index 2abfeff..0000000 --- a/packages/dev-server/comp/menu/menu-list.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/packages/dev-server/comp/menu/menu-manager.vue b/packages/dev-server/comp/menu/menu-manager.vue deleted file mode 100644 index 0d3c409..0000000 --- a/packages/dev-server/comp/menu/menu-manager.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - - - diff --git a/packages/dev-server/comp/menu/menu-view.vue b/packages/dev-server/comp/menu/menu-view.vue deleted file mode 100644 index 6ce7275..0000000 --- a/packages/dev-server/comp/menu/menu-view.vue +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - diff --git a/packages/dev-server/comp/menu/menus.json b/packages/dev-server/comp/menu/menus.json deleted file mode 100644 index f27d715..0000000 --- a/packages/dev-server/comp/menu/menus.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "menubar": [ - { - "key": "file", - "name": "文件", - "bind": "F", - "content": [ - "open-file", - "@separator", - "save", - "save-as", - "save-all" - ] - }, - { - "key": "edit", - "name": "编辑", - "bind": "E", - "content": [ - "undo", - "redo", - "@separator", - "cut", - "copy", - "paste", - "@separator", - "find", - "find-next", - "find-previous" - ] - }, - { - "key": "view", - "name": "视图", - "bind": "V", - "content": [ - "trigger-explorer", - "trigger-editor", - "trigger-document", - "@separator", - "@themes" - ] - }, - { - "key": "link", - "name": "链接", - "bind": "L", - "content": [ - "open-repository" - ] - } - ] -} diff --git a/packages/dev-server/comp/menus.json b/packages/dev-server/comp/menus.json new file mode 100644 index 0000000..960b625 --- /dev/null +++ b/packages/dev-server/comp/menus.json @@ -0,0 +1,64 @@ +[{ + "ref": "menubar", + "children": [ + { + "caption": "文件", + "mnemonic": "F", + "children": [ + { "command": "open-file", "mnemonic": "O", "show": false }, + { "caption": "-" }, + { "command": "save", "mnemonic": "S" }, + { "command": "save-as", "mnemonic": "A", "show": false }, + { "command": "save-all", "mnemonic": "L" } + ] + }, + { + "caption": "编辑", + "show": "$_enableEdit", + "mnemonic": "E", + "children": [ + { "command": "undo", "mnemonic": "U" }, + { "command": "redo", "mnemonic": "R" }, + { "caption": "-" }, + { "command": "cut", "mnemonic": "T" }, + { "command": "copy", "mnemonic": "C" }, + { "command": "paste", "mnemonic": "P" }, + { "caption": "-" }, + { "command": "find", "mnemonic": "F" }, + { "command": "find-next" }, + { "command": "find-previous" } + ] + }, + { + "caption": "视图", + "mnemonic": "V", + "children": [ + { "command": "trigger-explorer", "mnemonic": "R", "show": "$_isProject" }, + { "command": "trigger-editor", "mnemonic": "E", "show": "$_enableEdit" }, + { "command": "trigger-document", "mnemonic": "D" }, + { "caption": "-", "show": "$_enableEdit" }, + { "command": "command-palette", "mnemonic": "C", "show": "$_enableEdit" }, + { "caption": "-" }, + { + "caption": "主题", + "mnemonic": "T", + "children": [ + { + "role": "list", + "data": "$themes", + "current": "$theme", + "switch": "setTheme" + } + ] + } + ] + }, + { + "caption": "链接", + "mnemonic": "L", + "children": [ + { "command": "open-repository", "mnemonic": "R" } + ] + } + ] +}] diff --git a/packages/dev-server/comp/tree.vue b/packages/dev-server/comp/tree.vue new file mode 100644 index 0000000..84f9174 --- /dev/null +++ b/packages/dev-server/comp/tree.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index e4b48cd..8f28c8e 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@marklet/dev-server", - "version": "1.1.2", + "version": "1.1.3", "description": "A develop server for marklet.", "author": "jjyyxx <1449843302@qq.com>", "contributors": [ @@ -8,15 +8,16 @@ ], "homepage": "https://github.com/obstudio/Marklet/tree/dev/packages/dev-server", "license": "MIT", - "main": "dist/server.js", - "typings": "dist/server.d.ts", + "main": "dist/server/index.js", + "typings": "dist/server/index.d.ts", "files": [ - "dist/server.js", - "dist/server.d.ts", + "dist/server/index.js", + "dist/server/index.d.ts", "dist/client.min.js", "dist/client.min.css", "dist/themes.min.css", - "dist/monaco.min.css" + "dist/monaco.min.css", + "dist/index.html" ], "repository": { "type": "git", @@ -26,10 +27,16 @@ "url": "https://github.com/obstudio/Marklet/issues" }, "dependencies": { - "@marklet/parser": "^1.5.1", - "@marklet/renderer": "^1.3.1", + "@marklet/parser": "^1.5.2", + "@marklet/renderer": "^1.3.2", + "fast-deep-equal": "^2.0.1", + "file-saver": "^2.0.0-rc.4", + "lodash.debounce": "^4.0.8", + "js-yaml": "^3.12.0", + "minimatch": "^3.0.4", "monaco-editor": "^0.14.3", "mousetrap": "^1.6.2", + "obui": "^1.1.3", "vue": "^2.5.17", "ws": "^6.0.0" } diff --git a/packages/dev-server/server/file.ts b/packages/dev-server/server/file.ts new file mode 100644 index 0000000..2f187c0 --- /dev/null +++ b/packages/dev-server/server/file.ts @@ -0,0 +1,52 @@ +import { WatchEventType } from './index' +import MarkletManager from './manager' +import * as fs from 'fs' + +export const MARKUP_EXTENSIONS = ['.md', '.mkl'] + +export default class FileManager extends MarkletManager { + static EXTENSIONS = MARKUP_EXTENSIONS + + private content: string + private watcher: fs.FSWatcher + public generalFilepath: string + public unsentMessage: string + + constructor(private filepath: string) { + super() + + this.generalFilepath = filepath.replace(/\\/g, '/') + + this.initialize() + + this.watcher = fs.watch(this.filepath, (type: WatchEventType) => { + if (type === 'rename') { + this.dispose('Source file has been removed.') + } else { + this.debouncedUpdate() + } + }) + + this.on('client.save', ({ value }) => { + fs.writeFileSync(this.filepath, value) + }) + } + + public update() { + const temp = fs.readFileSync(this.filepath, 'utf8') + if (this.content !== temp) { + this.content = temp + this.unsentMessage = JSON.stringify({ + type: 'document', + value: this.content, + path: this.generalFilepath, + }) + this.emit('update', this.unsentMessage) + } + } + + public dispose(reason = '') { + this.watcher.close() + this.emit('close', reason) + } +} diff --git a/packages/dev-server/server/filter.ts b/packages/dev-server/server/filter.ts new file mode 100644 index 0000000..02e5943 --- /dev/null +++ b/packages/dev-server/server/filter.ts @@ -0,0 +1,32 @@ +import { makeRe } from 'minimatch' +import { extname } from 'path' + +export type StringLike = string | RegExp + +export default class FileFilter { + private extensions: string[] + private globs: RegExp[] = [] + + constructor(extensions: string[], ignores: StringLike[]) { + this.extensions = extensions.map(ext => ext.toLowerCase()) + ignores.forEach((pattern) => { + if (typeof pattern === 'string') { + this.globs.push(makeRe(pattern, { flipNegate: true })) + } else { + this.globs.push(pattern) + } + }) + } + + public test(filename: string): boolean { + if (this.extensions.every(ext => extname(filename) !== ext)) return false + let result = true + for (const regexp of this.globs) { + if (regexp.test(filename)) { + result = false + break + } + } + return result + } +} diff --git a/packages/dev-server/src/edit.html b/packages/dev-server/server/index.html similarity index 62% rename from packages/dev-server/src/edit.html rename to packages/dev-server/server/index.html index 350a7f8..aff83e5 100644 --- a/packages/dev-server/src/edit.html +++ b/packages/dev-server/server/index.html @@ -6,24 +6,27 @@ + + +
\ No newline at end of file diff --git a/packages/dev-server/server/index.ts b/packages/dev-server/server/index.ts new file mode 100644 index 0000000..dad6217 --- /dev/null +++ b/packages/dev-server/server/index.ts @@ -0,0 +1,180 @@ +import * as path from 'path' +import * as http from 'http' +import * as url from 'url' +import * as fs from 'fs' +import ws from 'ws' + +import { LexerConfig } from '@marklet/parser' +import { getContentType } from './util' +import MarkletManager from './manager' +import ProjectManager from './project' +import FileManager from './file' + +export const DEFAULT_PORT = 10826 + +declare module 'ws' { + interface Server { + broadcast(this: this, data: string): void + } +} + +ws.Server.prototype.broadcast = function (data) { + this.clients.forEach((client) => { + if (client.readyState === ws.OPEN) { + client.send(data) + } + }) +} + +export interface EditorConfig { + line_ending?: 'LF' | 'CRLF' +} + +export type ServerType = 'watch' | 'edit' +export type SourceType = 'file' | 'project' +export type WatchEventType = 'rename' | 'change' + +interface ServerOptions { + port?: number + editOptions?: EditorConfig + parseOptions?: LexerConfig +} + +function assignment(dataMap: Record) { + return `\ +(function(marklet) { +${Object.keys(dataMap).map((key) => `\ + marklet.${key} = ${JSON.stringify(dataMap[key])} +`).join('')}\ +})(marklet)` +} + +class MarkletServer { + public port: number + public manager: MarkletManager + public sourceType: SourceType + public editOptions: EditorConfig + public parseOptions: LexerConfig + public wsServer: ws.Server + public httpServer: http.Server + + constructor(public serverType: T, public filepath = '', options: ServerOptions = {}) { + this.parseOptions = options.parseOptions || {} + this.editOptions = options.editOptions || {} + this.port = options.port || DEFAULT_PORT + this.createServer() + this.setupManager() + } + + private createServer() { + this.httpServer = http.createServer((request, response) => { + function handleError(error: Error) { + console.error(error) + response.writeHead(404, { 'Content-Type': 'text/html' }) + response.end() + } + + function handleData(data: any, type = 'text/javascript') { + response.writeHead(200, { 'Content-Type': type }) + response.write(data) + response.end() + } + + let pathname = url.parse(request.url).pathname.slice(1) + let filepath: string + if (pathname.startsWith('~/')) { + try { + filepath = require.resolve(pathname.slice(2)) + } catch (error) { + handleError(error) + return + } + } else if (pathname === 'initialize.js') { + handleData(assignment({ + filepath: this.filepath, + serverType: this.serverType, + sourceType: this.sourceType, + editOptions: this.editOptions, + parseOptions: this.parseOptions, + })) + return + } else { + filepath = path.join(__dirname, '..', pathname || 'index.html') + } + fs.readFile(filepath, (error, data) => { + if (error) { + handleError(error) + return + } + handleData(data.toString(), getContentType(filepath)) + }) + }).listen(this.port) + this.wsServer = new ws.Server({ server: this.httpServer }) + console.log(`Server running at http://localhost:${this.port}/`) + } + + private setupManager() { + if (!this.filepath) { + this.sourceType = 'file' + // FIXME: Is a file manager is needed here so as to handle client requests? + return + } else if (!fs.existsSync(this.filepath)) { + throw new Error(`${this.filepath} does not exist.`) + } + if (fs.statSync(this.filepath).isFile()) { + const ext = path.extname(this.filepath).toLowerCase() + if (ProjectManager.EXTENSIONS.includes(ext)) { + this.sourceType = 'project' + this.manager = new ProjectManager(this.filepath).bind(this) + } else if (FileManager.EXTENSIONS.includes(ext)) { + this.sourceType = 'file' + this.manager = new FileManager(this.filepath).bind(this) + } else { + throw new Error('Cannot recognize the file extension.') + } + } else { + const configPathWithoutExt = path.join(this.filepath, 'marklet') + for (const ext of ProjectManager.EXTENSIONS) { + const configPath = configPathWithoutExt + ext + if (!fs.existsSync(configPath)) continue + if (fs.statSync(configPath).isFile()) { + this.sourceType = 'project' + this.filepath = configPath + this.manager = new ProjectManager(configPath).bind(this) + return + } + } + throw new Error(`No config file was found in ${this.filepath}.`) + } + } + + public dispose(reason: string = '') { + this.wsServer.close() + this.httpServer.close() + console.log(reason) + } +} + +export { MarkletServer as Server } + +export interface WatchOptions extends ServerOptions { + filepath: string + editOptions: never +} + +export function watch(options: WatchOptions): MarkletServer<'watch'> { + if (!options.filepath) { + throw new Error('Filepath is required in watch mode.') + } + return new MarkletServer('watch', options.filepath, options) +} + +export interface EditOptions extends ServerOptions { + filepath?: string +} + +export function edit(options: EditOptions): MarkletServer<'edit'> { + const server = new MarkletServer('edit', options.filepath, options) + // FIXME: handle saving files and other client requests. + return server +} diff --git a/packages/dev-server/server/manager.ts b/packages/dev-server/server/manager.ts new file mode 100644 index 0000000..56a1029 --- /dev/null +++ b/packages/dev-server/server/manager.ts @@ -0,0 +1,49 @@ +import debounce from 'lodash.debounce' +import EventEmitter from 'events' +import { Server } from './index' + +interface ServerMessage { + type: string + [ key: string ]: any +} + +export default class MarkletManager extends EventEmitter { + private messageQueue: ServerMessage[] = [] + private server: Server + + public update?(): void + public dispose?(reason: string): void + public debouncedUpdate: () => void + + public initialize() { + this.on('update', (data) => { + if (!this.server) { + this.messageQueue.push(data) + } else { + this.server.wsServer.broadcast(JSON.stringify(data)) + } + }) + + this.debouncedUpdate = debounce(() => this.update(), 200) + this.update() + } + + public bind(server: Server) { + this.server = server + this.once('close', reason => server.dispose(reason)) + server.wsServer.on('connection', (ws) => { + this.messageQueue.forEach(msg => ws.send(JSON.stringify(msg))) + if (server.serverType !== 'edit') return + ws.on('message', (message: string) => { + try { + const data = JSON.parse(message) + this.emit('client.message', data) + this.emit('client.' + data.type, data) + } catch (error) { + this.emit('client.error', error) + } + }) + }) + return this + } +} diff --git a/packages/dev-server/server/project.ts b/packages/dev-server/server/project.ts new file mode 100644 index 0000000..39a997f --- /dev/null +++ b/packages/dev-server/server/project.ts @@ -0,0 +1,164 @@ +import { WatchEventType, EditorConfig } from './index' +import FileFilter, { StringLike } from './filter' +import { LexerConfig } from '@marklet/parser' +import MarkletManager from './manager' +import equal from 'fast-deep-equal' +import DirTree from './tree' +import * as yaml from 'js-yaml' +import * as path from 'path' +import * as fs from 'fs' + +export interface ProjectConfig { + baseDir?: string + extensions?: string[] + ignore?: StringLike[] + editOptions?: EditorConfig + parseOptions?: LexerConfig +} + +const JSON_EXTENSIONS = ['.js', '.json'] +const YAML_EXTENSIONS = ['.yml', '.yaml'] + +export default class ProjectManager extends MarkletManager { + static EXTENSIONS = [...JSON_EXTENSIONS, ...YAML_EXTENSIONS] + + private tree: DirTree + private filter: FileFilter + private rawConfig: string + private config: ProjectConfig + private oldConfig: ProjectConfig + private folderWatcher: fs.FSWatcher + private configWatcher: fs.FSWatcher + private delSet: Set = new Set() + private addSet: Set = new Set() + private configUpdated: boolean + private basepath: string + private basename: string + private extension: string + + constructor(private filepath: string) { + super() + + this.basepath = path.dirname(filepath) + this.basename = path.basename(filepath) + this.extension = path.extname(filepath).toLowerCase() + + const config = this.getConfig() + this.filter = new FileFilter(config.extensions, config.ignore) + this.tree = new DirTree(config.baseDir, this.filter) + this.tree.entryList.forEach(filepath => this.addSet.add(filepath)) + + this.initialize() + + this.folderWatcher = fs.watch(this.config.baseDir, { + recursive: true + }, (type: WatchEventType, filename) => { + if (!this.filter.test(filename)) return + if (type === 'rename' && this.tree.has(filename)) { + this.delSet.add(filename) + } else { + this.addSet.add(filename) + } + this.debouncedUpdate() + }) + + this.configWatcher = fs.watch(this.filepath, (type: WatchEventType) => { + if (type === 'rename') { + this.dispose('Configuration file has been removed.') + } else { + this.getConfig() + this.debouncedUpdate() + } + }) + + this.on('client.save', ({ path: filepath, value }) => { + fs.writeFileSync(path.resolve(this.config.baseDir, filepath), value) + }) + } + + private getConfig() { + this.oldConfig = this.config || {} + const beforeCreate = !this.config + function takeTry(task: Function) { + try { + return task() + } catch (error) { + if (beforeCreate) throw error + } + } + + let config: ProjectConfig + this.rawConfig = fs.readFileSync(this.filepath).toString() + if (JSON_EXTENSIONS.includes(this.extension)) { + require.cache[this.filepath] = null + takeTry(() => config = require(this.filepath)) + } else if (YAML_EXTENSIONS.includes(this.extension)) { + takeTry(() => config = yaml.safeLoad(this.rawConfig)) + } + config.baseDir = path.resolve(this.basepath, config.baseDir || '') + if (!config.extensions) config.extensions = ['.mkl'] + if (!config.ignore) config.ignore = [] + if (!config.editOptions) config.editOptions = {} + if (!config.parseOptions) config.parseOptions = {} + if (this.filepath.startsWith(config.baseDir)) { + config.ignore.push(this.filepath.slice(config.baseDir.length + 1)) + } + this.configUpdated = true + return this.config = config + } + + public update() { + for (const item of this.delSet) { + this.tree.del(item) + } + this.delSet.clear() + for (const item of this.addSet) { + try { + const content = fs.readFileSync(path.join(this.config.baseDir, item), 'utf8') + this.tree.set(item, content) + this.emit('update', { + type: 'document', + value: content, + path: item.replace(/\\/g, '/'), + }) + } catch (_) {} + } + this.addSet.clear() + this.emit('update', { + type: 'entries', + path: this.config.baseDir, + tree: this.tree.entryTree, + }) + if (this.configUpdated) { + this.emit('update', { + type: 'config', + config: this.config, + content: this.rawConfig, + }) + if (!equal(this.config.parseOptions, this.oldConfig.parseOptions)) { + this.emit('update', { + type: 'config.parseOptions', + options: this.config.parseOptions, + }) + } + if (!equal(this.config.editOptions, this.oldConfig.editOptions)) { + this.emit('update', { + type: 'config.editOptions', + options: this.config.editOptions, + }) + } + this.configUpdated = false + } + // FIXME: events when a file content was changed + } + + public getContent(path: string) { + return this.tree.get(path) + } + + public dispose(reason = '') { + this.folderWatcher.close() + this.configWatcher.close() + this.emit('close', reason) + } +} diff --git a/packages/dev-server/server/tree.ts b/packages/dev-server/server/tree.ts new file mode 100644 index 0000000..00095da --- /dev/null +++ b/packages/dev-server/server/tree.ts @@ -0,0 +1,107 @@ +import * as fs from 'fs' +import * as path from 'path' +import FileFilter from './filter' + +export interface FileTree { + [key: string]: string | FileTree +} + +interface EntryTree extends Array {} +type SubEntry = EntryTree | string + +export default class DirTree { + static separator = /[\/\\]/ + public tree: FileTree + + constructor(private filepath: string, private filter: FileFilter) { + this.tree = this.init(filepath) + } + + private init(filepath: string): FileTree { + const children = fs.readdirSync(filepath, { withFileTypes: true }) + const subtree: FileTree = {} + for (const child of children) { + const newPath = path.join(filepath, child.name) + if (child.isFile()) { + if (this.filter.test(newPath)) { + subtree[child.name] = fs.readFileSync(newPath, 'utf8') + } + } else { + subtree[child.name] = this.init(newPath) + } + } + return subtree + } + + public get(path: string) { + const segments = path.split(DirTree.separator) + let result = this.tree + for (const segment of segments) { + result = result[segment] + } + return result + } + + public set(path: string, value: string | FileTree) { + const segments = path.split(DirTree.separator) + const last = segments.pop() + let tree = this.tree + for (const segment of segments) { + tree = (tree[segment] = tree[segment] || {}) + } + tree[last] = value + } + + public has(path: string) { + const segments = path.split(DirTree.separator) + let result = this.tree + for (const segment of segments) { + if (result = result[segment]) + continue + return false + } + return true + } + + public del(path: string) { + const segments = path.split(DirTree.separator) + const last = segments.pop() + let tree = this.tree + for (const segment of segments) { + tree = tree[segment] + } + delete tree[last] + } + + get entryTree() { + const rootTree = (function generateEntryTree(tree: FileTree) { + const entries: EntryTree = Object.keys(tree) + for (let i = 0; i < entries.length; i++) { + const element = entries[i] + const content = tree[element] + if (typeof content === 'object') { + const sub = generateEntryTree(content) + sub.unshift(element) + entries.splice(i, 1, sub) + } + } + return entries + })(this.tree) + rootTree.unshift(path.basename(this.filepath)) + return rootTree + } + + get entryList() { + return Array.from(function* entries(tree: FileTree, basename = ''): IterableIterator { + for (const key of Object.keys(tree)) { + const content = tree[key] + const filename = basename + key + if (typeof content === 'string') { + yield filename + } else { + yield* entries(content, filename + '/') + } + } + }(this.tree)) + } +} diff --git a/packages/dev-server/server/util.ts b/packages/dev-server/server/util.ts new file mode 100644 index 0000000..3b9232a --- /dev/null +++ b/packages/dev-server/server/util.ts @@ -0,0 +1,13 @@ +import { extname } from 'path' + +const mimeMap: Record = { + '.css': 'text/css', + '.js': 'text/javascript', + '.html': 'text/html' +} +const fallbackMime = 'application/octet-stream' + +export function getContentType(filepath: string): string { + const ext = extname(filepath) + return mimeMap[ext] || fallbackMime +} diff --git a/packages/dev-server/src/client.ts b/packages/dev-server/src/client.ts deleted file mode 100644 index 82c9ff6..0000000 --- a/packages/dev-server/src/client.ts +++ /dev/null @@ -1,106 +0,0 @@ -import VueConstructor from 'vue' -import Monaco from 'monaco-editor' -import * as renderer from '@marklet/renderer' - -declare global { - export const Vue: typeof VueConstructor -} - -Vue.use(renderer) -Vue.component('mkl-checkbox', require('@/checkbox.vue')) - -const eventBus = new Vue() -Vue.prototype.$eventBus = eventBus - -eventBus.$on('monaco.loaded', (monaco: typeof Monaco) => { - monaco.editor.defineTheme('dark', require('../themes/dark')) - monaco.editor.defineTheme('simple', require('../themes/simple')) - eventBus.$emit('monaco.theme.loaded', monaco) -}) - -const client = new class MarkletClient { - private url: string - private retry: boolean - private _timeout: number - private _interval: number - private ws: WebSocket - private msgQueue: string[] - private timerRef: number - - constructor({ - url = `ws://${location.host}/`, - retry = true, - timeout = 5000, - interval = 1000, - } = {}) { - this.url = url - this.retry = retry - this._timeout = timeout - this.msgQueue = [] - this.createWebSocket() - this.interval = interval - } - - get interval() { - return this._interval - } - - set interval(value: number) { - this._interval = value - window.clearInterval(this.timerRef) - this.timerRef = window.setInterval(() => { - while (this.msgQueue.length > 0 && this.ws.readyState === 1) { - this.ws.send(this.msgQueue.shift()) - } - }, this._interval) - } - - createWebSocket() { - this.ws = new WebSocket(this.url) - this.ws.addEventListener('close', (event) => { - if (event.code !== 1000 && this.retry) { - eventBus.$emit('ws.reconnect') - setTimeout(() => this.createWebSocket(), this._timeout) - } else { - eventBus.$emit('ws.close') - } - }) - this.ws.addEventListener('error', () => { - eventBus.$emit('ws.error') - }) - this.ws.addEventListener('message', (event) => { - try { - const { type, data } = JSON.parse(event.data) - eventBus.$emit('server.' + type, data) - } catch (error) { - eventBus.$emit('server.error', error) - } - }) - this.ws.addEventListener('open', () => { - eventBus.$emit('ws.open') - }) - eventBus.$on('client.message', (type: string, data: string | Object) => { - this.msgQueue.push(JSON.stringify({ type, data })) - }) - } - - dispose() { - if (this.ws.readyState < 2) this.ws.close() - clearInterval(this.timerRef) - } -}() - -addEventListener('beforeunload', () => client.dispose()) - -const typeMap = { - edit: '编辑', - watch: '监视', -} - -export const Marklet = { - app: require('@/app.vue'), - start(type: keyof typeof typeMap) { - document.title = 'Marklet - ' + typeMap[type] - return new Vue(this.app) - } -} diff --git a/packages/dev-server/src/server.ts b/packages/dev-server/src/server.ts deleted file mode 100644 index 7bf48c2..0000000 --- a/packages/dev-server/src/server.ts +++ /dev/null @@ -1,129 +0,0 @@ -import * as path from 'path' -import * as http from 'http' -import * as url from 'url' -import * as fs from 'fs' -import ws from 'ws' - -import { LexerConfig } from '@marklet/parser' - -export const DEFAULT_PORT = 10826 - -declare module 'ws' { - interface Server { - broadcast(this: this, data: string): void - } -} - -ws.Server.prototype.broadcast = function (data) { - this.clients.forEach((client) => { - if (client.readyState === ws.OPEN) { - client.send(data) - } - }) -} - -function sendMsg(this: ws, type: string, data: object) { - this.send(JSON.stringify({ type, data })) -} - -function toDocMessage(filename: string) { - return JSON.stringify({ - type: 'document', - data: fs.readFileSync(filename).toString() - }) -} - -export type ServerType = 'watch' | 'edit' - -interface ServerOptions { - port?: number - config?: LexerConfig -} - -class MarkletServer { - type: T - port: number - wsServer: ws.Server - httpServer: http.Server - - constructor(type: T, options: ServerOptions = {}) { - this.type = type - this.port = options.port || DEFAULT_PORT - - this.httpServer = http.createServer((request, response) => { - function handleError(error: Error) { - console.error(error) - response.writeHead(404, { 'Content-Type': 'text/html' }) - response.end() - } - - function handleData(data: any, type: string) { - response.writeHead(200, { 'Content-Type': type }) - response.write(data) - response.end() - } - - let pathname = url.parse(request.url).pathname.slice(1) - let filepath: string - if (pathname.startsWith('~/')) { - try { - filepath = require.resolve(pathname.slice(2)) - } catch (error) { - handleError(error) - return - } - } else { - filepath = path.join(__dirname, pathname || type + '.html') - } - fs.readFile(filepath, (error, data) => { - if (error) { - handleError(error) - return - } - - const ext = path.extname(filepath) - let contentType: string - switch (ext) { - case '.css': contentType = 'text/css'; break - case '.js': contentType = 'text/javascript'; break - case '.html': contentType = 'text/html'; break - default: contentType = 'application/octet-stream' - } - handleData(data.toString(), contentType) - }) - }).listen(this.port) - - this.wsServer = new ws.Server({ server: this.httpServer }) - this.wsServer.on('connection', (ws) => { - sendMsg.call(ws, 'config', options.config) - }) - - console.log(`Server running at http://localhost:${this.port}/`) - } -} - -export { MarkletServer as Server } - -export interface WatchOptions extends ServerOptions { - source: string -} - -export function watch(options: WatchOptions): MarkletServer<'watch'> { - const server = new MarkletServer('watch', options) - server.wsServer.on('connection', (ws) => { - ws.send(toDocMessage(options.source)) - }) - fs.watch(options.source, () => { - server.wsServer.broadcast(toDocMessage(options.source)) - }) - return server -} - -export interface EditOptions extends ServerOptions { - source?: string -} - -export function edit(options: EditOptions): MarkletServer<'edit'> { - const server = new MarkletServer('edit', options) - return server -} diff --git a/packages/dev-server/src/watch.html b/packages/dev-server/src/watch.html deleted file mode 100644 index 8772b2d..0000000 --- a/packages/dev-server/src/watch.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - Marklet - - - - - - - - - -
- - - \ No newline at end of file diff --git a/packages/dev-server/themes/dark.scss b/packages/dev-server/themes/dark.scss index 8e25583..b983640 100644 --- a/packages/dev-server/themes/dark.scss +++ b/packages/dev-server/themes/dark.scss @@ -3,6 +3,7 @@ $menu-shadow: #000000; $active-color: #acdeff; &, > .explorer { + color: #fafaf5; background-color: #272822; } @@ -24,7 +25,7 @@ $active-color: #acdeff; &:hover, &.active { background-color: #999999 } } -.marklet-menu { +.ob-menu > .standalone { background-color: $menu-bgcolor; box-shadow: 0 2px 8px $menu-shadow; @@ -35,7 +36,7 @@ $active-color: #acdeff; .binding i:hover { color: #fafaf5 } .separator { border-bottom-color: #888888 } - &:hover { + &:hover, &.chosen { &:not(.disabled) { background-color: #3A3A3A } .label { color: #e1e4e8 } .binding { color: #c6cbd1 } @@ -55,7 +56,7 @@ $active-color: #acdeff; } } -.marklet-checkbox { +.ob-checkbox { color: #666666; > .box { diff --git a/packages/dev-server/themes/dark.yaml b/packages/dev-server/themes/dark.yaml index ca9add3..127dadd 100644 --- a/packages/dev-server/themes/dark.yaml +++ b/packages/dev-server/themes/dark.yaml @@ -1,5 +1,4 @@ base: vs-dark -rules: [] inherit: true colors: @@ -74,3 +73,7 @@ colors: peekViewResult.selectionBackground: '#414339' peekViewResult.matchHighlightBackground: '#75715E' peekViewEditor.matchHighlightBackground: '#75715E' + +rules: + - token: heading + foreground: 'a6e22e' diff --git a/packages/dev-server/themes/simple.scss b/packages/dev-server/themes/simple.scss index 99fec64..acbee5d 100644 --- a/packages/dev-server/themes/simple.scss +++ b/packages/dev-server/themes/simple.scss @@ -3,6 +3,7 @@ $menu-shadow: #888888; $active-color: #409eff; &, > .explorer { + color: #24292e; background-color: #ffffff; } @@ -24,7 +25,7 @@ $active-color: #409eff; &:hover, &.active { background-color: #aaaaaa } } -.marklet-menu { +.ob-menu > .standalone { background-color: $menu-bgcolor; box-shadow: 0 2px 8px $menu-shadow; @@ -35,7 +36,7 @@ $active-color: #409eff; .binding i:hover { color: #000000 } .separator { border-bottom-color: #b0b0b0 } - &:hover { + &:hover, &.chosen { &:not(.disabled) { background-color: #e8e8e8 } .label { color: #24292e } .binding { color: #f5eaea } @@ -55,7 +56,7 @@ $active-color: #409eff; } } -.marklet-checkbox { +.ob-checkbox { color: #606266; > .box { diff --git a/packages/dev-server/tsconfig.json b/packages/dev-server/tsconfig.json index 30874db..3e81fcf 100644 --- a/packages/dev-server/tsconfig.json +++ b/packages/dev-server/tsconfig.json @@ -1,11 +1,12 @@ { "extends": "../../tsconfig.base.json", "include": [ - "src" + "server", + "client" ], "compilerOptions": { "outDir": "dist", - "rootDir": "src" + "rootDir": "." }, "references": [ { "path": "../parser" } diff --git a/packages/marklet/package.json b/packages/marklet/package.json index 437fc02..55cfcb8 100644 --- a/packages/marklet/package.json +++ b/packages/marklet/package.json @@ -1,6 +1,6 @@ { "name": "markletjs", - "version": "1.3.0", + "version": "1.3.1", "description": "A markup language designed for API manual pages.", "author": "jjyyxx <1449843302@qq.com>", "contributors": [ @@ -28,8 +28,8 @@ "url": "https://github.com/obstudio/Marklet/issues" }, "dependencies": { - "@marklet/cli": "^2.0.1", - "@marklet/parser": "^1.5.1", - "@marklet/renderer": "^1.3.1" + "@marklet/cli": "^2.0.2", + "@marklet/parser": "^1.5.2", + "@marklet/renderer": "^1.3.2" } } \ No newline at end of file diff --git a/packages/parser/package.json b/packages/parser/package.json index 0a3149b..8e0a9c6 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -1,6 +1,6 @@ { "name": "@marklet/parser", - "version": "1.5.1", + "version": "1.5.2", "description": "A document lexer for marklet.", "author": "shigma <1700011071@pku.edu.cn>", "contributors": [ @@ -21,6 +21,6 @@ "url": "https://github.com/obstudio/Marklet/issues" }, "dependencies": { - "@marklet/core": "^3.2.2" + "@marklet/core": "^3.2.3" } } \ No newline at end of file diff --git a/packages/renderer/package.json b/packages/renderer/package.json index cbf0bf5..7ac9926 100644 --- a/packages/renderer/package.json +++ b/packages/renderer/package.json @@ -1,6 +1,6 @@ { "name": "@marklet/renderer", - "version": "1.3.1", + "version": "1.3.2", "description": "A html renderer for marklet.", "author": "jjyyxx <1449843302@qq.com>", "contributors": [ @@ -21,7 +21,7 @@ "url": "https://github.com/obstudio/Marklet/issues" }, "dependencies": { - "@marklet/core": "^3.2.2", + "@marklet/core": "^3.2.3", "neat-scroll": "^2.0.1" }, "devDependencies": { diff --git a/packages/syntax/package.json b/packages/syntax/package.json index 03caee6..710c6f9 100644 --- a/packages/syntax/package.json +++ b/packages/syntax/package.json @@ -1,6 +1,6 @@ { "name": "@marklet/syntax", - "version": "1.0.16", + "version": "1.0.17", "description": "A common language lexer for marklet.", "author": "shigma <1700011071@pku.edu.cn>", "homepage": "https://github.com/obstudio/Marklet/tree/dev/packages/syntax", @@ -18,7 +18,7 @@ "url": "https://github.com/obstudio/Marklet/issues" }, "dependencies": { - "@marklet/core": "^3.2.2", + "@marklet/core": "^3.2.3", "js-yaml": "^3.12.0" } } \ No newline at end of file diff --git a/packages/test/data/marklet.yml b/packages/test/data/marklet.yml new file mode 100644 index 0000000..b491ef7 --- /dev/null +++ b/packages/test/data/marklet.yml @@ -0,0 +1,9 @@ +extensions: + - .md + - .mkl + +editOptions: + line_ending: LF + +parseOptions: + default_language: marklet \ No newline at end of file diff --git a/packages/test/data/codeblock.mkl b/packages/test/data/syntax/codeblock.mkl similarity index 100% rename from packages/test/data/codeblock.mkl rename to packages/test/data/syntax/codeblock.mkl diff --git a/packages/test/data/inlinelist.mkl b/packages/test/data/syntax/inlinelist.mkl similarity index 100% rename from packages/test/data/inlinelist.mkl rename to packages/test/data/syntax/inlinelist.mkl diff --git a/packages/test/data/list.mkl b/packages/test/data/syntax/list.mkl similarity index 100% rename from packages/test/data/list.mkl rename to packages/test/data/syntax/list.mkl diff --git a/packages/test/data/section.mkl b/packages/test/data/syntax/section.mkl similarity index 100% rename from packages/test/data/section.mkl rename to packages/test/data/syntax/section.mkl diff --git a/packages/test/data/table.mkl b/packages/test/data/syntax/table.mkl similarity index 100% rename from packages/test/data/table.mkl rename to packages/test/data/syntax/table.mkl diff --git a/packages/test/data/text.mkl b/packages/test/data/syntax/text.mkl similarity index 100% rename from packages/test/data/text.mkl rename to packages/test/data/syntax/text.mkl diff --git a/packages/test/index.js b/packages/test/index.js index 6309ba3..71461e6 100644 --- a/packages/test/index.js +++ b/packages/test/index.js @@ -5,10 +5,25 @@ const fs = require('fs') const lexer = new DocumentLexer() -const testFiles = fs.readdirSync(path.join(__dirname, 'data')).map((file) => ({ - name: file.slice(0, -4), - content: fs.readFileSync(path.join(__dirname, 'data', file)).toString() -})) +function traverse(basedir) { + return Array.from(function* walk(filename = '') { + const filepath = path.join(basedir, filename) + if (fs.statSync(filepath).isFile()) { + yield filename + } else { + for (const name of fs.readdirSync(filepath)) { + yield* walk(path.join(filename, name)) + } + } + }()) +} + +const testFiles = traverse(path.join(__dirname, 'data')) + .filter(file => path.extname(file) === '.mkl') + .map((file) => ({ + name: file.slice(0, -4), + content: fs.readFileSync(path.join(__dirname, 'data', file)).toString() + })) const parsedFiles = testFiles.map((file) => ({ name: file.name, diff --git a/packages/test/package.json b/packages/test/package.json index faccb99..1f99788 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@marklet/test", - "version": "2.1.2", + "version": "2.1.3", "private": true, "main": "index.js", "author": "jjyyxx <1449843302@qq.com>", @@ -20,8 +20,8 @@ "url": "https://github.com/obstudio/Marklet/issues" }, "dependencies": { - "@marklet/detok": "^1.1.3", - "@marklet/parser": "^1.5.1", + "@marklet/detok": "^1.1.4", + "@marklet/parser": "^1.5.2", "chalk": "^2.4.1" } } \ No newline at end of file