@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 +21 -0
- package/README.md +74 -0
- package/index.js +4 -0
- package/package.json +48 -0
- package/src/index.js +47 -0
- package/src/lib/directory.js +15 -0
- package/src/lib/file.js +16 -0
- package/src/lib/file_watcher.js +59 -0
- package/src/lib/logger.js +23 -0
- package/src/lib/params.js +185 -0
- package/src/lib/params.test.js +92 -0
- package/src/lib/show.js +29 -0
- package/src/lib/socket_manager.js +30 -0
- package/src/lib/socket_manager.test.js +44 -0
- package/src/markdown.js +16 -0
- package/src/websocket.js +41 -0
- package/static/markdown-preview-websocket.js +15 -0
- package/template/default-dark.html +24 -0
- package/template/default.html +24 -0
- package/test/markdown/markdown1.md +1 -0
- package/test/template/template1.html +16 -0
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
|
+

|
|
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, '<').replace(/>/g, '>');
|
|
70
|
+
});
|
|
71
|
+
</script>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
|
74
|
+
```
|
package/index.js
ADDED
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
|
+
};
|
package/src/lib/file.js
ADDED
|
@@ -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
|
+
});
|
package/src/lib/show.js
ADDED
|
@@ -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
|
+
});
|
package/src/markdown.js
ADDED
|
@@ -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
|
package/src/websocket.js
ADDED
|
@@ -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, '<').replace(/</g, '>');
|
|
13
|
+
});
|
|
14
|
+
</script>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|