@mryhryki/markdown-preview 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Hiroyuki Moriya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # markdown-preview
2
+
3
+ Markdown realtime preview on browser with your favorite editor.
4
+
5
+ ## Demo
6
+
7
+ ![DEMO](./gif/demo.gif)
8
+
9
+ ## Usage
10
+
11
+ ### npx
12
+
13
+ ```shell
14
+ $ npx @mryhryki/markdown-preview --file README.md --template default --port 34567 --log-level info --no-opener
15
+ Root Directory : /current/dir
16
+ Default File : README.md
17
+ Extensions : md, markdown
18
+ Template File : /path/to/template/default.html
19
+ Preview URL : http://localhost:34567
20
+ ```
21
+
22
+ ### npm / yarn
23
+
24
+ ```shell
25
+ $ npm install -g @mryhryki/markdown-preview
26
+ # or
27
+ $ yarn install -g @mryhryki/markdown-preview
28
+
29
+ $ markdown-preview --file README.md --template default --port 34567 --log-level info --no-opener
30
+ Root Directory : /current/dir
31
+ Default File : README.md
32
+ Extensions : md, markdown
33
+ Template File : /path/to/template/default.html
34
+ Preview URL : http://localhost:34567
35
+ ```
36
+
37
+ ## Parameter
38
+
39
+ | short | long | environment variable | parameter | required | default |
40
+ |-------|-------------|----------------------------|--------------------------------------------------|----------|-----------|
41
+ | -f | --file | MARKDOWN_PREVIEW_FILE | ***relative*** file path | no | README.md |
42
+ | -t | --template | MARKDOWN_PREVIEW_TEMPLATE | defined template name (*1) or template file path | no | default |
43
+ | -p | --port | MARKDOWN_PREVIEW_PORT | port number | no | 34567 |
44
+ | | --log-level | MARKDOWN_PREVIEW_LOG_LEVEL | trace, debug, info<br>warn, error, fatal | no | info |
45
+ | | --no-opener | MARKDOWN_PREVIEW_NO_OPENER | true (only env var) | no | |
46
+ | -v | --version | | | no | |
47
+ | -h | --help | | | no | |
48
+
49
+ ### *1: Defined Template Names
50
+
51
+ - `default`
52
+ - `default-dark`
53
+
54
+ ## Minimum Customized Template
55
+
56
+ ```html
57
+ <!doctype html>
58
+ <html>
59
+ <head>
60
+ <title>Minimum Customized Template</title>
61
+ </head>
62
+ <body>
63
+ <pre id="raw-markdown"></pre>
64
+ <script src="/markdown-preview-websocket.js"></script>
65
+ <script type="text/javascript">
66
+ connectMarkdownPreview((changedEvent) => {
67
+ const { markdown } = changedEvent;
68
+ document.getElementById('raw-markdown').innerHTML =
69
+ markdown.replace(/</g, '&lt;').replace(/>/g, '&gt;');
70
+ });
71
+ </script>
72
+ </body>
73
+ </html>
74
+ ```
package/index.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ require('./src/index');
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@mryhryki/markdown-preview",
3
+ "description": "Markdown realtime preview on browser",
4
+ "version": "0.3.2",
5
+ "author": "mryhryki",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "keywords": [
11
+ "markdown",
12
+ "preview"
13
+ ],
14
+ "homepage": "https://github.com/mryhryki/markdown-preview#readme",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/mryhryki/markdown-preview.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/mryhryki/markdown-preview/issues"
21
+ },
22
+ "engines": {
23
+ "node": ">=12.0.0",
24
+ "npm": ">=6.0.0"
25
+ },
26
+ "main": "index.js",
27
+ "bin": {
28
+ "markdown-preview": "index.js"
29
+ },
30
+ "dependencies": {
31
+ "express": "^4.18.2",
32
+ "express-ws": "^5.0.2",
33
+ "log4js": "^6.7.0",
34
+ "opener": "^1.5.2",
35
+ "serve-index": "^1.9.1",
36
+ "ws": "^8.9.0"
37
+ },
38
+ "devDependencies": {
39
+ "jest": "^29.1.2",
40
+ "nodemon": "^2.0.20"
41
+ },
42
+ "scripts": {
43
+ "start": "node ./index.js",
44
+ "dev": "nodemon --watch ./src/ index.js --no-opener --log-level debug",
45
+ "test": "jest",
46
+ "test:watch": "jest --watchAll"
47
+ }
48
+ }
package/src/index.js ADDED
@@ -0,0 +1,47 @@
1
+ 'use strict'
2
+
3
+ const express = require('express')
4
+ const expressWs = require('express-ws')
5
+ const serveIndex = require('serve-index')
6
+ const opener = require('opener')
7
+ const getLogger = require('./lib/logger')
8
+ const { showUsage, showVersion } = require('./lib/show')
9
+ const MarkdownHandler = require('./markdown')
10
+ const WebSocketHandler = require('./websocket')
11
+ const { rootDir, staticDir } = require('./lib/directory')
12
+ const Params = require('./lib/params')
13
+
14
+ try {
15
+ const params = new Params(process.env, process.argv.slice(2))
16
+ if (params.help) showUsage()
17
+ if (params.version) showVersion()
18
+
19
+ const logger = getLogger(params.logLevel)
20
+ const previewUrl = `http://localhost:${params.port}`
21
+
22
+ console.log('Root Directory :', rootDir)
23
+ console.log('Default File :', params.filepath)
24
+ console.log('Extensions :', params.extensions.join(', '))
25
+ console.log('Template File :', params.template)
26
+ console.log(`Preview URL : ${previewUrl}`)
27
+
28
+ const app = express()
29
+ expressWs(app)
30
+ app.get('/', (_req, res) => res.redirect(params.filepath))
31
+ app.ws('/ws', WebSocketHandler(logger))
32
+ params.extensions.forEach((ext) => {
33
+ app.get(new RegExp(`^/.+\.${ext}$`), MarkdownHandler(params.template))
34
+ })
35
+ app.use(express.static(rootDir, { index: false }))
36
+ app.use(express.static(staticDir, { index: false }))
37
+ app.use(serveIndex(rootDir, { icons: true, view: 'details' }))
38
+ app.listen(params.port)
39
+
40
+ if (!params.noOpener) {
41
+ opener(previewUrl)
42
+ }
43
+
44
+ } catch (err) {
45
+ console.error(err.message)
46
+ showUsage(true)
47
+ }
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const rootDir = process.cwd();
6
+ const projectDir = path.resolve(__dirname, '..', '..');
7
+ const staticDir = path.resolve(projectDir, 'static');
8
+ const templateDir = path.resolve(projectDir, 'template');
9
+
10
+ module.exports = {
11
+ rootDir,
12
+ projectDir,
13
+ staticDir,
14
+ templateDir,
15
+ };
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+
5
+ const existsFile = (filepath) => {
6
+ try {
7
+ fs.statSync(filepath);
8
+ return true;
9
+ } catch (_) {
10
+ return false;
11
+ }
12
+ };
13
+
14
+ module.exports = {
15
+ existsFile,
16
+ };
@@ -0,0 +1,59 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const { rootDir } = require('./directory')
6
+
7
+ class FileWatcher {
8
+ constructor (logger) {
9
+ this.logger = logger
10
+ this._target = {}
11
+ setInterval(() => {
12
+ Object.keys(this._target).forEach((filepath) => {
13
+ try {
14
+ const fileinfo = this._target[filepath]
15
+ const currentLastModified = this.getFileLastModified(filepath)
16
+ if (fileinfo.lastModified !== currentLastModified) {
17
+ this.logger.info('File update:', path.resolve(rootDir, filepath))
18
+ fileinfo.lastModified = currentLastModified
19
+ if (this._onFileChanged != null) {
20
+ this._onFileChanged(this.getFileInfo(filepath))
21
+ }
22
+ }
23
+ } catch (err) {
24
+ console.error(err)
25
+ }
26
+ })
27
+ }, 250)
28
+ }
29
+
30
+ onFileChanged (callback) {
31
+ this._onFileChanged = callback
32
+ }
33
+
34
+ addTargetFile (filepath) {
35
+ if (this._target[filepath] != null) return
36
+ this.logger.debug('Add watch target:', filepath)
37
+ this._target[filepath] = {
38
+ lastModified: this.getFileLastModified(filepath),
39
+ }
40
+ }
41
+
42
+ removeTargetFile (filepath) {
43
+ if (this._target[filepath] == null) return
44
+ this.logger.debug('Remove watch target:', filepath)
45
+ delete this._target[filepath]
46
+ }
47
+
48
+ getFileLastModified (filepath) {
49
+ return fs.statSync(path.resolve(rootDir, filepath)).mtimeMs
50
+ }
51
+
52
+ getFileInfo (filepath) {
53
+ const absolutePath = path.resolve(rootDir, filepath)
54
+ const markdown = fs.readFileSync(absolutePath, 'utf-8')
55
+ return { filepath, markdown }
56
+ }
57
+ }
58
+
59
+ module.exports = FileWatcher
@@ -0,0 +1,23 @@
1
+ const log4js = require('log4js');
2
+
3
+ const getLogger = (logLevel) => {
4
+ log4js.configure({
5
+ appenders: {
6
+ console: {
7
+ type: 'console',
8
+ layout: {
9
+ type: 'basic',
10
+ },
11
+ },
12
+ },
13
+ categories: {
14
+ default: {
15
+ appenders: ['console'],
16
+ level: logLevel,
17
+ },
18
+ },
19
+ });
20
+ return log4js.getLogger();
21
+ };
22
+
23
+ module.exports = getLogger;
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { rootDir, templateDir } = require('./directory');
5
+ const { existsFile } = require('./file');
6
+
7
+ class Params {
8
+ constructor(env, argv) {
9
+ const obj = Object.assign(this.getDefaultParams(), this.parseEnv(env), this.parseArgv(argv));
10
+ this._params = {
11
+ filepath: this.checkFilepath(obj.filepath),
12
+ extensions: this.checkExtensions(obj.extensions),
13
+ template: this.checkTemplate(obj.template),
14
+ port: this.checkPort(obj.port),
15
+ logLevel: this.checkLogLevel(obj.logLevel),
16
+ noOpener: obj.noOpener,
17
+ version: obj.version,
18
+ help: obj.help,
19
+ };
20
+ }
21
+
22
+ getDefaultParams() {
23
+ return {
24
+ filepath: 'README.md',
25
+ extensions: 'md, markdown',
26
+ template: 'default',
27
+ port: 34567,
28
+ logLevel: 'info',
29
+ noOpener: false,
30
+ version: false,
31
+ help: false,
32
+ };
33
+ }
34
+
35
+ parseEnv(env) {
36
+ const params = {};
37
+ if (env.MARKDOWN_PREVIEW_FILE) {
38
+ params.filepath = env.MARKDOWN_PREVIEW_FILE;
39
+ }
40
+ if (env.MARKDOWN_PREVIEW_EXTENSIONS) {
41
+ params.extensions = env.MARKDOWN_PREVIEW_EXTENSIONS;
42
+ }
43
+ if (env.MARKDOWN_PREVIEW_TEMPLATE) {
44
+ params.template = env.MARKDOWN_PREVIEW_TEMPLATE;
45
+ }
46
+ if (env.MARKDOWN_PREVIEW_PORT) {
47
+ params.port = env.MARKDOWN_PREVIEW_PORT;
48
+ }
49
+ if (env.MARKDOWN_PREVIEW_NO_OPENER) {
50
+ params.noOpener = env.MARKDOWN_PREVIEW_NO_OPENER === 'true';
51
+ }
52
+ if (env.MARKDOWN_PREVIEW_LOG_LEVEL) {
53
+ params.logLevel = env.MARKDOWN_PREVIEW_LOG_LEVEL;
54
+ }
55
+ return params;
56
+ }
57
+
58
+ parseArgv(argv) {
59
+ const params = {};
60
+ for (let i = 0; i < argv.length; i++) {
61
+ switch (argv[i]) {
62
+ case '-f':
63
+ case '--file':
64
+ params.filepath = argv[i + 1];
65
+ i++;
66
+ break;
67
+ case '-e':
68
+ case '--extensions':
69
+ params.extensions = argv[i + 1];
70
+ i++;
71
+ break;
72
+ case '-t':
73
+ case '--template':
74
+ params.template = argv[i + 1];
75
+ i++;
76
+ break;
77
+ case '-p':
78
+ case '--port':
79
+ params.port = argv[i + 1];
80
+ i++;
81
+ break;
82
+ case '-l':
83
+ case '--log-level':
84
+ params.logLevel = argv[i + 1];
85
+ i++;
86
+ break;
87
+ case '--no-opener':
88
+ params.noOpener = true;
89
+ break;
90
+ case '-v':
91
+ case '--version':
92
+ params.version = true;
93
+ break;
94
+ case '-h':
95
+ case '--help':
96
+ params.help = true;
97
+ break;
98
+ default:
99
+ throw new Error(`Unknown option: ${argv[i]}`);
100
+ }
101
+ }
102
+ return params;
103
+ }
104
+
105
+
106
+ checkFilepath(filepath) {
107
+ if (path.isAbsolute(filepath)) {
108
+ throw new Error(`Absolute path is prohibited: ${filepath}`);
109
+ }
110
+ if (!existsFile(filepath)) {
111
+ throw new Error(`File not found: ${filepath}`);
112
+ }
113
+ if (path.relative(rootDir, filepath).match(/\.\./) != null) {
114
+ throw new Error(`Illegal file path: ${filepath}`);
115
+ }
116
+ return filepath;
117
+ }
118
+
119
+ checkExtensions(extensions) {
120
+ const extensionList = extensions.split(',').map(ext => ext.trim());
121
+ if (extensionList.length === 0) {
122
+ throw new Error(`Extensions is empty: ${extensions}`);
123
+ }
124
+ return extensionList;
125
+ }
126
+
127
+ checkTemplate(template) {
128
+ if (existsFile(path.resolve(templateDir, `${template}.html`))) {
129
+ return path.resolve(templateDir, `${template}.html`);
130
+ } else if (existsFile(path.resolve(rootDir, template))) {
131
+ return path.resolve(rootDir, template);
132
+ }
133
+ throw new Error(`Template file not found: ${template}`);
134
+ }
135
+
136
+ checkPort(port) {
137
+ const intPort = parseInt(port, 10);
138
+ if (!isNaN(intPort) && 0 < intPort && intPort <= 65535) {
139
+ return intPort;
140
+ }
141
+ throw new Error(`Invalid port: ${port}`);
142
+ };
143
+
144
+
145
+ checkLogLevel(logLevel) {
146
+ if (['trace', 'debug', 'info', 'warn', 'error', 'fatal'].includes(logLevel)) {
147
+ return logLevel;
148
+ }
149
+ throw new Error(`Invalid log level: ${logLevel}`);
150
+ }
151
+
152
+ get filepath() {
153
+ return this._params.filepath;
154
+ }
155
+
156
+ get extensions() {
157
+ return this._params.extensions;
158
+ }
159
+
160
+ get template() {
161
+ return this._params.template;
162
+ }
163
+
164
+ get port() {
165
+ return this._params.port;
166
+ }
167
+
168
+ get logLevel() {
169
+ return this._params.logLevel;
170
+ }
171
+
172
+ get noOpener() {
173
+ return this._params.noOpener;
174
+ }
175
+
176
+ get version() {
177
+ return this._params.version;
178
+ }
179
+
180
+ get help() {
181
+ return this._params.help;
182
+ }
183
+ }
184
+
185
+ module.exports = Params;
@@ -0,0 +1,92 @@
1
+ const path = require('path');
2
+ const { projectDir } = require('./directory');
3
+ const Params = require('./params');
4
+
5
+ const DEFAULT_VALUES = {
6
+ filepath: 'README.md',
7
+ extensions: ['md', 'markdown'],
8
+ template: path.resolve(projectDir, 'template/default.html'),
9
+ port: 34567,
10
+ logLevel: 'info',
11
+ noOpener: false,
12
+ version: false,
13
+ help: false,
14
+ };
15
+
16
+ describe('Params', () => {
17
+ it('not specify', () => {
18
+ const params = new Params({}, []);
19
+ expect(params._params).toEqual(DEFAULT_VALUES);
20
+ });
21
+
22
+ it('specify all short argument', () => {
23
+ const argv = [
24
+ '-f', 'test/markdown/markdown1.md',
25
+ '-e', 'ext1,ext2',
26
+ '-t', 'test/template/template1.html',
27
+ '-p', '100',
28
+ '-v',
29
+ '-h',
30
+ ];
31
+ const expectParams = {
32
+ filepath: 'test/markdown/markdown1.md',
33
+ extensions: ['ext1', 'ext2'],
34
+ template: path.resolve(projectDir, 'test/template/template1.html'),
35
+ port: 100,
36
+ logLevel: 'info',
37
+ noOpener: false,
38
+ version: true,
39
+ help: true,
40
+ };
41
+ const params = new Params({}, argv);
42
+ expect(params._params).toEqual(expectParams);
43
+ });
44
+
45
+ it('specify all long argument', () => {
46
+ const argv = [
47
+ '--file', 'test/markdown/markdown1.md',
48
+ '--extensions', 'ext1,ext2',
49
+ '--template', 'test/template/template1.html',
50
+ '--port', '100',
51
+ '--log-level', 'trace',
52
+ '--no-opener',
53
+ '--version',
54
+ '--help',
55
+ ];
56
+ const expectParams = {
57
+ filepath: 'test/markdown/markdown1.md',
58
+ extensions: ['ext1', 'ext2'],
59
+ template: path.resolve(projectDir, 'test/template/template1.html'),
60
+ port: 100,
61
+ logLevel: 'trace',
62
+ noOpener: true,
63
+ version: true,
64
+ help: true,
65
+ };
66
+ const params = new Params({}, argv);
67
+ expect(params._params).toEqual(expectParams);
68
+ });
69
+
70
+ it('specify all environment variable', () => {
71
+ const env = {
72
+ MARKDOWN_PREVIEW_FILE: 'test/markdown/markdown1.md',
73
+ MARKDOWN_PREVIEW_EXTENSIONS: 'ext1, ext2',
74
+ MARKDOWN_PREVIEW_TEMPLATE: 'test/template/template1.html',
75
+ MARKDOWN_PREVIEW_PORT: '100',
76
+ MARKDOWN_PREVIEW_LOG_LEVEL: 'trace',
77
+ MARKDOWN_PREVIEW_NO_OPENER: 'true',
78
+ };
79
+ const expectParams = {
80
+ filepath: 'test/markdown/markdown1.md',
81
+ extensions: ['ext1', 'ext2'],
82
+ template: path.resolve(projectDir, 'test/template/template1.html'),
83
+ port: 100,
84
+ logLevel: 'trace',
85
+ noOpener: true,
86
+ version: false,
87
+ help: false,
88
+ };
89
+ const params = new Params(env, []);
90
+ expect(params._params).toEqual(expectParams);
91
+ });
92
+ });
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+ const pkg = require('../../package');
3
+
4
+ const showUsage = (error = false) => {
5
+ const usage = `
6
+ Usage:
7
+ npx @mryhryki/markdown-preview [options]
8
+ markdown-preview [options]
9
+
10
+ Options:
11
+ -f,--file [relative_file_path] Default: README.md
12
+ -t,--template [template_name] Default: default
13
+ -p,--port [port_number] Default: 34567
14
+ -v,--version
15
+ -h,--help
16
+ `;
17
+ console.log(usage);
18
+ process.exit(error ? 1 : 0);
19
+ };
20
+
21
+ const showVersion = () => {
22
+ console.log(pkg.version);
23
+ process.exit(0);
24
+ };
25
+
26
+ module.exports = {
27
+ showUsage,
28
+ showVersion,
29
+ };
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ class SocketManager {
4
+ constructor() {
5
+ this._sockets = [];
6
+ }
7
+
8
+ addSocket(socket, filepath) {
9
+ this._sockets.push({ socket, filepath });
10
+ }
11
+
12
+ removeSocket(socket) {
13
+ this._sockets = this._sockets.filter(({ socket: s }) => s !== socket);
14
+ }
15
+
16
+ getSockets(filepath) {
17
+ return this._sockets
18
+ .filter(({ filepath: fp }) => fp === filepath)
19
+ .map(s => s.socket);
20
+ }
21
+
22
+ countSocket(filepath = null) {
23
+ if (filepath == null) {
24
+ return this._sockets.length;
25
+ }
26
+ return this.getSockets(filepath).length;
27
+ }
28
+ }
29
+
30
+ module.exports = SocketManager;
@@ -0,0 +1,44 @@
1
+ const SocketManager = require('./socket_manager');
2
+
3
+ const dummySocket1 = { name: 'socket1' };
4
+ const dummySocket2 = { name: 'socket2' };
5
+ const dummySocket3 = { name: 'socket3' };
6
+
7
+ const dummyFilepath1 = 'file1';
8
+ const dummyFilepath2 = 'file2';
9
+
10
+ const dummyInfo1 = { socket: dummySocket1, filepath: dummyFilepath1 };
11
+ const dummyInfo2 = { socket: dummySocket2, filepath: dummyFilepath2 };
12
+ const dummyInfo3 = { socket: dummySocket3, filepath: dummyFilepath2 };
13
+
14
+ describe('SocketManager', () => {
15
+ it('works normally', () => {
16
+ const socketManager = new SocketManager();
17
+ expect(socketManager._sockets).toEqual([]);
18
+
19
+ socketManager.addSocket(dummySocket1, dummyFilepath1);
20
+ expect(socketManager._sockets).toEqual([dummyInfo1]);
21
+
22
+ socketManager.addSocket(dummySocket2, dummyFilepath2);
23
+ expect(socketManager._sockets).toEqual([dummyInfo1, dummyInfo2]);
24
+
25
+ socketManager.addSocket(dummySocket3, dummyFilepath2);
26
+ expect(socketManager._sockets).toEqual([dummyInfo1, dummyInfo2, dummyInfo3]);
27
+
28
+ expect(socketManager.getSockets(dummyFilepath1)).toEqual([dummySocket1]);
29
+ expect(socketManager.getSockets(dummyFilepath2)).toEqual([dummySocket2, dummySocket3]);
30
+ expect(socketManager.countSocket()).toEqual(3);
31
+ expect(socketManager.countSocket(dummyFilepath1)).toEqual(1);
32
+ expect(socketManager.countSocket(dummyFilepath2)).toEqual(2);
33
+
34
+
35
+ socketManager.removeSocket(dummySocket2);
36
+ expect(socketManager._sockets).toEqual([dummyInfo1, dummyInfo3]);
37
+
38
+ socketManager.removeSocket(dummySocket1);
39
+ expect(socketManager._sockets).toEqual([dummyInfo3]);
40
+
41
+ socketManager.removeSocket(dummySocket3);
42
+ expect(socketManager._sockets).toEqual([]);
43
+ });
44
+ });
@@ -0,0 +1,16 @@
1
+ 'use strict'
2
+
3
+ const path = require('path')
4
+ const { rootDir } = require('./lib/directory')
5
+ const { existsFile } = require('./lib/file')
6
+
7
+ const MarkdownHandler = (template) => (req, res, next) => {
8
+ const filepath = path.resolve(rootDir, decodeURIComponent(req.path.substr(1)))
9
+ if (existsFile(filepath)) {
10
+ res.sendFile(template)
11
+ } else {
12
+ next()
13
+ }
14
+ }
15
+
16
+ module.exports = MarkdownHandler
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const FileWatcher = require('./lib/file_watcher');
5
+ const SocketManager = require('./lib/socket_manager');
6
+ const { rootDir } = require('./lib/directory');
7
+
8
+ const WebSocketHandler = (logger) => {
9
+ let socketSeqNo = 1;
10
+ const socketManager = new SocketManager();
11
+ const fileWatcher = new FileWatcher(logger);
12
+ fileWatcher.onFileChanged((fileinfo) => {
13
+ socketManager.getSockets(fileinfo.filepath).forEach((ws) => {
14
+ ws.send(JSON.stringify(fileinfo));
15
+ });
16
+ });
17
+
18
+ return (ws, req) => {
19
+ const wsSeqNo = socketSeqNo++;
20
+ try {
21
+ logger.debug('WebSocket connected:', wsSeqNo);
22
+ const filepath = path.resolve(rootDir, decodeURIComponent(req.query.path.substr(1)));
23
+ fileWatcher.addTargetFile(filepath);
24
+ socketManager.addSocket(ws, filepath);
25
+
26
+ ws.on('close', () => {
27
+ logger.debug('WebSocket close:', wsSeqNo);
28
+ socketManager.removeSocket(ws);
29
+ if (socketManager.countSocket(filepath) === 0) {
30
+ fileWatcher.removeTargetFile(filepath);
31
+ }
32
+ });
33
+
34
+ ws.send(JSON.stringify(fileWatcher.getFileInfo(filepath)));
35
+ } catch (e) {
36
+ logger.error(e);
37
+ }
38
+ };
39
+ };
40
+
41
+ module.exports = WebSocketHandler;
@@ -0,0 +1,15 @@
1
+ const connectMarkdownPreview = (onMarkdownFileChanged) => {
2
+ const protocol = location.protocol.replace('http', 'ws');
3
+ const url = `${protocol}//${location.host}/ws?path=${encodeURIComponent(location.pathname)}`;
4
+ const ws = new WebSocket(url);
5
+ ws.addEventListener('message', ({ data }) => {
6
+ try {
7
+ const payload = JSON.parse(data);
8
+ if (!('markdown' in payload)) return;
9
+ if (typeof (payload.markdown) !== 'string' || payload.markdown.length === 0) return;
10
+ onMarkdownFileChanged(payload);
11
+ } catch (err) {
12
+ console.error(err);
13
+ }
14
+ });
15
+ };
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Markdown Preview</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-dark.min.css">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/styles/github-dark-dimmed.min.css">
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
10
+ </head>
11
+ <body style="margin: 0 auto; max-width: 882px; padding: 32px; background-color: #0d1117;">
12
+ <div class="markdown-body">
13
+ <div id="content"></div>
14
+ </div>
15
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.1.1/marked.min.js"></script>
16
+ <script src="/markdown-preview-websocket.js"></script>
17
+ <script type="text/javascript">
18
+ connectMarkdownPreview(({ markdown }) => {
19
+ document.getElementById('content').innerHTML = marked.parse(markdown);
20
+ document.querySelectorAll('pre code').forEach(block => hljs.highlightBlock(block));
21
+ });
22
+ </script>
23
+ </body>
24
+ </html>
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Markdown Preview</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown-light.min.css">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/styles/github.min.css">
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
10
+ </head>
11
+ <body style="margin: 0 auto; max-width: 882px; padding: 32px;">
12
+ <div class="markdown-body">
13
+ <div id="content"></div>
14
+ </div>
15
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.1.1/marked.min.js"></script>
16
+ <script src="/markdown-preview-websocket.js"></script>
17
+ <script type="text/javascript">
18
+ connectMarkdownPreview(({ markdown }) => {
19
+ document.getElementById('content').innerHTML = marked.parse(markdown);
20
+ document.querySelectorAll('pre code').forEach(block => hljs.highlightBlock(block));
21
+ });
22
+ </script>
23
+ </body>
24
+ </html>
@@ -0,0 +1 @@
1
+ # markdown1.md
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Minimum Customized Template</title>
5
+ </head>
6
+ <body>
7
+ <pre id="raw-markdown"></pre>
8
+ <script src="/markdown-preview-websocket.js"></script>
9
+ <script type="text/javascript">
10
+ connectMarkdownPreview((changedEvent) => {
11
+ const { markdown } = changedEvent;
12
+ document.getElementById('raw-markdown').innerHTML = markdown.replace(/</g, '&lt;').replace(/</g, '&gt;');
13
+ });
14
+ </script>
15
+ </body>
16
+ </html>