markserv 1.17.4 → 1.19.1
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/README.md +33 -10
- package/lib/cli-defs.js +23 -3
- package/lib/cli-help.txt +6 -3
- package/lib/cli.js +1 -15
- package/lib/server.js +276 -49
- package/lib/templates/directory.html +158 -2
- package/lib/templates/error.html +159 -3
- package/lib/templates/github-markdown-dark.css +1124 -0
- package/lib/templates/github-markdown-light.css +1124 -0
- package/lib/templates/github-markdown-synthwave.css +987 -0
- package/lib/templates/highlight-js-github-gist.css +243 -1
- package/lib/templates/markdown.html +157 -1
- package/lib/templates/markserv.css +191 -519
- package/package.json +6 -4
- package/CHANGELOG.md +0 -155
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img alt="Markserv Logo" src="https://markserv.github.io/markserv/media/markserv-readme-banner.svg">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
> :checkered_flag: serve markdown as html (GitHub style), index directories, and
|
|
5
|
+
> :checkered_flag: serve markdown as html (GitHub style), index directories, and hot-reload as you edit
|
|
6
6
|
|
|
7
7
|
[](https://gitter.im/markserv)
|
|
8
8
|
[](CHANGELOG.md)
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
- Markdown content rendered as HTML
|
|
21
21
|
- GitHub flavor CSS and Syntax Highlighting
|
|
22
22
|
- [Just in Time Templating](#stopwatch-just-in-time-templating): Markdown, HTML & LESS
|
|
23
|
-
-
|
|
23
|
+
- Hot-reload as you edit (no browser plugin required)
|
|
24
24
|
- Directory indexes
|
|
25
25
|
- MIME Type file support
|
|
26
26
|
|
|
@@ -75,20 +75,37 @@ Start Markserv and open the closest README.md file in the browser:
|
|
|
75
75
|
$ readme
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
## :zap:
|
|
78
|
+
## :zap: Hot Reload
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
Markserv includes built-in hot-reload via WebSocket. When you save a Markdown file, the page content updates instantly without a full page reload and without any browser plugin.
|
|
81
81
|
|
|
82
|
-
-
|
|
83
|
-
- [Firefox](https://addons.mozilla.org/addon/livereload-web-extension/)
|
|
84
|
-
- [Internet Explorer](https://github.com/dvdotsenko/livereload_ie_extension)
|
|
82
|
+
To disable hot-reload, use `--no-hotreload`:
|
|
85
83
|
|
|
86
|
-
|
|
84
|
+
```shell
|
|
85
|
+
markserv --no-hotreload README.md
|
|
86
|
+
```
|
|
87
87
|
|
|
88
88
|
<p align="center">
|
|
89
89
|
<img alt="Markserv Live Reload" src="media/markserv-live-reload.gif" width="100%">
|
|
90
90
|
</p>
|
|
91
91
|
|
|
92
|
+
## :art: Themes
|
|
93
|
+
|
|
94
|
+
Markserv ships with four themes: **dark** (default), **light**, **synthwave**, and **solarized**. You can set the theme from the CLI:
|
|
95
|
+
|
|
96
|
+
```shell
|
|
97
|
+
# Use light theme
|
|
98
|
+
markserv --light README.md
|
|
99
|
+
|
|
100
|
+
# Use synthwave theme
|
|
101
|
+
markserv --synthwave README.md
|
|
102
|
+
|
|
103
|
+
# Or use the --theme flag for any theme
|
|
104
|
+
markserv --theme solarized README.md
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
You can also toggle between themes in the browser using the theme button in the bottom-right corner. Your choice is saved in `localStorage` and persists across hot-reloads.
|
|
108
|
+
|
|
92
109
|
## :link: Markdown Links
|
|
93
110
|
|
|
94
111
|
You can link to an external Markdown file in the same way that you use GitHub Wiki links. You can use the example code here to see how external links work.
|
|
@@ -105,7 +122,13 @@ Example link:
|
|
|
105
122
|
|
|
106
123
|
## :stopwatch: Just in Time Templating
|
|
107
124
|
|
|
108
|
-
Markserv allows you to include nested content
|
|
125
|
+
Markserv allows you to include nested content using `--templates`. Templates are fetched and rendered when you request them in your browser. The `maxDepth` of includes is set to `10`.
|
|
126
|
+
|
|
127
|
+
**Note:** Templating is disabled by default. Enable it with `--templates`:
|
|
128
|
+
|
|
129
|
+
```shell
|
|
130
|
+
markserv --templates README.md
|
|
131
|
+
```
|
|
109
132
|
|
|
110
133
|
If you would like to look at an example, you can look in the [tests/templates](tests/templates/) directory of this repo.
|
|
111
134
|
|
|
@@ -114,7 +137,7 @@ To see the server output of this templating example:
|
|
|
114
137
|
```shell
|
|
115
138
|
$ git clone@github.com/f1lt3r/markserv.git
|
|
116
139
|
$ cd markserv
|
|
117
|
-
$ markserv tests/templates/index.html
|
|
140
|
+
$ markserv --templates tests/templates/index.html
|
|
118
141
|
```
|
|
119
142
|
|
|
120
143
|
### Include Markdown
|
package/lib/cli-defs.js
CHANGED
|
@@ -5,9 +5,10 @@ module.exports = {
|
|
|
5
5
|
default: '8642'
|
|
6
6
|
},
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
alias: '
|
|
10
|
-
|
|
8
|
+
hotreload: {
|
|
9
|
+
alias: 'l',
|
|
10
|
+
type: 'boolean',
|
|
11
|
+
default: true
|
|
11
12
|
},
|
|
12
13
|
|
|
13
14
|
address: {
|
|
@@ -23,6 +24,25 @@ module.exports = {
|
|
|
23
24
|
verbose: {
|
|
24
25
|
alias: 'v',
|
|
25
26
|
default: false
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
theme: {
|
|
30
|
+
default: 'dark'
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
light: {
|
|
34
|
+
type: 'boolean',
|
|
35
|
+
default: false
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
synthwave: {
|
|
39
|
+
type: 'boolean',
|
|
40
|
+
default: false
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
templates: {
|
|
44
|
+
type: 'boolean',
|
|
45
|
+
default: false
|
|
26
46
|
}
|
|
27
47
|
}
|
|
28
48
|
}
|
package/lib/cli-help.txt
CHANGED
|
@@ -3,9 +3,12 @@ $ markserv <file/dir>
|
|
|
3
3
|
$ readme (serve closest README.md file)
|
|
4
4
|
|
|
5
5
|
Options
|
|
6
|
-
--port, -p, HTTP port [port] (
|
|
7
|
-
--
|
|
6
|
+
--port, -p, HTTP port [port] (8642)
|
|
7
|
+
--hotreload, -l Hot-reload via WebSocket (true), disable with --no-hotreload
|
|
8
8
|
--browser, -b Launch browser (true)
|
|
9
9
|
--silent, -i Silent (false)
|
|
10
10
|
--address, -a Serve on ip/address [address] (localhost)
|
|
11
|
-
--verbose, -v Verbose output (false)
|
|
11
|
+
--verbose, -v Verbose output (false)
|
|
12
|
+
--light Use light theme (default: dark)
|
|
13
|
+
--synthwave Use synthwave/retro-neon theme (default: dark)
|
|
14
|
+
--templates Enable {file:}, {markdown:}, {html:}, {less:} templating (false)
|
package/lib/cli.js
CHANGED
|
@@ -14,21 +14,7 @@ const cliDefs = require('./cli-defs')
|
|
|
14
14
|
const cliOpts = meow(cliHelp, cliDefs)
|
|
15
15
|
|
|
16
16
|
const validateServerPath = (serverPath, cwd) => {
|
|
17
|
-
|
|
18
|
-
return cwd
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
let validatedPath
|
|
22
|
-
|
|
23
|
-
if (serverPath[0]) {
|
|
24
|
-
validatedPath = path.normalize(path.join(cwd, serverPath))
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (serverPath[0] === '/' || serverPath[0] === '.') {
|
|
28
|
-
validatedPath = path.normalize(path.join(cwd, serverPath))
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return validatedPath
|
|
17
|
+
return path.resolve(cwd, serverPath)
|
|
32
18
|
}
|
|
33
19
|
|
|
34
20
|
const run = opts => {
|
package/lib/server.js
CHANGED
|
@@ -10,8 +10,8 @@ const Promise = require('bluebird')
|
|
|
10
10
|
const connect = require('connect')
|
|
11
11
|
const less = require('less')
|
|
12
12
|
const send = require('send')
|
|
13
|
-
const
|
|
14
|
-
const
|
|
13
|
+
const WebSocket = require('ws')
|
|
14
|
+
const getPort = require('get-port')
|
|
15
15
|
const implant = require('implant')
|
|
16
16
|
const deepmerge = require('deepmerge')
|
|
17
17
|
const handlebars = require('handlebars')
|
|
@@ -166,12 +166,12 @@ const getFile = path => new Promise((resolve, reject) => {
|
|
|
166
166
|
|
|
167
167
|
// Get Custom Less CSS to use in all Markdown files
|
|
168
168
|
const buildLessStyleSheet = cssPath =>
|
|
169
|
-
new Promise(resolve =>
|
|
169
|
+
new Promise((resolve, reject) =>
|
|
170
170
|
getFile(cssPath).then(data =>
|
|
171
171
|
less.render(data).then(data =>
|
|
172
172
|
resolve(data.css)
|
|
173
173
|
)
|
|
174
|
-
)
|
|
174
|
+
).catch(reject)
|
|
175
175
|
)
|
|
176
176
|
|
|
177
177
|
const baseTemplate = (templateUrl, handlebarData) => new Promise((resolve, reject) => {
|
|
@@ -296,6 +296,22 @@ const createBreadcrumbs = path => {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
// Http_request_handler: handles all the browser requests
|
|
299
|
+
const resolveTheme = flags => {
|
|
300
|
+
if (flags.light) {
|
|
301
|
+
return 'light'
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (flags.synthwave) {
|
|
305
|
+
return 'synthwave'
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (flags.theme && flags.theme !== 'dark') {
|
|
309
|
+
return flags.theme
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return 'dark'
|
|
313
|
+
}
|
|
314
|
+
|
|
299
315
|
const createRequestHandler = flags => {
|
|
300
316
|
let {dir} = flags
|
|
301
317
|
const isDir = fs.statSync(dir).isDirectory()
|
|
@@ -304,6 +320,13 @@ const createRequestHandler = flags => {
|
|
|
304
320
|
}
|
|
305
321
|
|
|
306
322
|
flags.$openLocation = path.relative(dir, flags.dir)
|
|
323
|
+
const theme = resolveTheme(flags)
|
|
324
|
+
const themeFlags = {
|
|
325
|
+
themeDark: theme === 'dark',
|
|
326
|
+
themeLight: theme === 'light',
|
|
327
|
+
themeSynthwave: theme === 'synthwave',
|
|
328
|
+
themeSolarized: theme === 'solarized'
|
|
329
|
+
}
|
|
307
330
|
|
|
308
331
|
const implantOpts = {
|
|
309
332
|
maxDepth: 10
|
|
@@ -320,6 +343,10 @@ const createRequestHandler = flags => {
|
|
|
320
343
|
}),
|
|
321
344
|
|
|
322
345
|
file: (url, opts) => new Promise(resolve => {
|
|
346
|
+
if (typeof url !== 'string' || typeof (opts && opts.baseDir) !== 'string') {
|
|
347
|
+
return resolve(false)
|
|
348
|
+
}
|
|
349
|
+
|
|
323
350
|
const absUrl = path.join(opts.baseDir, url)
|
|
324
351
|
getFile(absUrl)
|
|
325
352
|
.then(data => {
|
|
@@ -333,6 +360,10 @@ const createRequestHandler = flags => {
|
|
|
333
360
|
}),
|
|
334
361
|
|
|
335
362
|
less: (url, opts) => new Promise(resolve => {
|
|
363
|
+
if (typeof url !== 'string' || typeof (opts && opts.baseDir) !== 'string') {
|
|
364
|
+
return resolve(false)
|
|
365
|
+
}
|
|
366
|
+
|
|
336
367
|
const absUrl = path.join(opts.baseDir, url)
|
|
337
368
|
buildLessStyleSheet(absUrl)
|
|
338
369
|
.then(data => {
|
|
@@ -346,6 +377,10 @@ const createRequestHandler = flags => {
|
|
|
346
377
|
}),
|
|
347
378
|
|
|
348
379
|
markdown: (url, opts) => new Promise(resolve => {
|
|
380
|
+
if (typeof url !== 'string' || typeof (opts && opts.baseDir) !== 'string') {
|
|
381
|
+
return resolve(false)
|
|
382
|
+
}
|
|
383
|
+
|
|
349
384
|
const absUrl = path.join(opts.baseDir, url)
|
|
350
385
|
getFile(absUrl).then(markdownToHTML)
|
|
351
386
|
.then(data => {
|
|
@@ -359,6 +394,10 @@ const createRequestHandler = flags => {
|
|
|
359
394
|
}),
|
|
360
395
|
|
|
361
396
|
html: (url, opts) => new Promise(resolve => {
|
|
397
|
+
if (typeof url !== 'string' || typeof (opts && opts.baseDir) !== 'string') {
|
|
398
|
+
return resolve(false)
|
|
399
|
+
}
|
|
400
|
+
|
|
362
401
|
const absUrl = path.join(opts.baseDir, url)
|
|
363
402
|
getFile(absUrl)
|
|
364
403
|
.then(data => {
|
|
@@ -396,7 +435,12 @@ const createRequestHandler = flags => {
|
|
|
396
435
|
filePath,
|
|
397
436
|
errorMsg,
|
|
398
437
|
errorStack,
|
|
399
|
-
referer
|
|
438
|
+
referer,
|
|
439
|
+
theme,
|
|
440
|
+
...themeFlags,
|
|
441
|
+
hotreload: flags.$hotreload,
|
|
442
|
+
wsPort: flags.$wsPort,
|
|
443
|
+
rootDir: flags.dir
|
|
400
444
|
}
|
|
401
445
|
|
|
402
446
|
return baseTemplate(templateUrl, handlebarData).then(final => {
|
|
@@ -455,26 +499,42 @@ const createRequestHandler = flags => {
|
|
|
455
499
|
if (isMarkdown) {
|
|
456
500
|
msg('markdown', style.link(prettyPath), flags)
|
|
457
501
|
getFile(filePath).then(markdownToHTML).then(filePath).then(html => {
|
|
458
|
-
|
|
502
|
+
const contentPromise = flags.templates ?
|
|
503
|
+
implant(html, implantHandlers, implantOpts) :
|
|
504
|
+
Promise.resolve(html)
|
|
505
|
+
|
|
506
|
+
return contentPromise.then(output => {
|
|
459
507
|
const templateUrl = path.join(__dirname, 'templates/markdown.html')
|
|
460
508
|
|
|
461
509
|
const handlebarData = {
|
|
462
510
|
title: path.parse(filePath).base,
|
|
463
511
|
content: output,
|
|
464
|
-
pid: process.pid | 'N/A'
|
|
512
|
+
pid: process.pid | 'N/A',
|
|
513
|
+
theme,
|
|
514
|
+
...themeFlags,
|
|
515
|
+
hotreload: flags.$hotreload,
|
|
516
|
+
wsPort: flags.$wsPort,
|
|
517
|
+
rootDir: flags.dir
|
|
465
518
|
}
|
|
466
519
|
|
|
467
520
|
return baseTemplate(templateUrl, handlebarData).then(final => {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
521
|
+
if (flags.templates) {
|
|
522
|
+
const lvl2Dir = path.parse(templateUrl).dir
|
|
523
|
+
const lvl2Opts = deepmerge(implantOpts, {baseDir: lvl2Dir})
|
|
524
|
+
|
|
525
|
+
return implant(final, implantHandlers, lvl2Opts)
|
|
526
|
+
.then(output => {
|
|
527
|
+
res.writeHead(200, {
|
|
528
|
+
'content-type': 'text/html'
|
|
529
|
+
})
|
|
530
|
+
res.end(output)
|
|
475
531
|
})
|
|
476
|
-
|
|
477
|
-
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
res.writeHead(200, {
|
|
535
|
+
'content-type': 'text/html'
|
|
536
|
+
})
|
|
537
|
+
res.end(final)
|
|
478
538
|
})
|
|
479
539
|
})
|
|
480
540
|
}).catch(error => {
|
|
@@ -483,7 +543,11 @@ const createRequestHandler = flags => {
|
|
|
483
543
|
} else if (isHtml) {
|
|
484
544
|
msg('html', style.link(prettyPath), flags)
|
|
485
545
|
getFile(filePath).then(html => {
|
|
486
|
-
|
|
546
|
+
const contentPromise = flags.templates ?
|
|
547
|
+
implant(html, implantHandlers, implantOpts) :
|
|
548
|
+
Promise.resolve(html)
|
|
549
|
+
|
|
550
|
+
return contentPromise.then(output => {
|
|
487
551
|
res.writeHead(200, {
|
|
488
552
|
'content-type': 'text/html'
|
|
489
553
|
})
|
|
@@ -504,20 +568,32 @@ const createRequestHandler = flags => {
|
|
|
504
568
|
content: dirToHtml(filePath),
|
|
505
569
|
title: path.parse(filePath).base,
|
|
506
570
|
pid: process.pid | 'N/A',
|
|
507
|
-
breadcrumbs: createBreadcrumbs(path.relative(dir, filePath))
|
|
571
|
+
breadcrumbs: createBreadcrumbs(path.relative(dir, filePath)),
|
|
572
|
+
theme,
|
|
573
|
+
...themeFlags,
|
|
574
|
+
hotreload: flags.$hotreload,
|
|
575
|
+
wsPort: flags.$wsPort,
|
|
576
|
+
rootDir: flags.dir
|
|
508
577
|
}
|
|
509
578
|
|
|
510
579
|
return baseTemplate(templateUrl, handlebarData).then(final => {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
580
|
+
if (flags.templates) {
|
|
581
|
+
const lvl2Dir = path.parse(templateUrl).dir
|
|
582
|
+
const lvl2Opts = deepmerge(implantOpts, {baseDir: lvl2Dir})
|
|
583
|
+
return implant(final, implantHandlers, lvl2Opts).then(output => {
|
|
584
|
+
res.writeHead(200, {
|
|
585
|
+
'content-type': 'text/html'
|
|
586
|
+
})
|
|
587
|
+
res.end(output)
|
|
588
|
+
}).catch(error => {
|
|
589
|
+
console.error(error)
|
|
516
590
|
})
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
res.writeHead(200, {
|
|
594
|
+
'content-type': 'text/html'
|
|
520
595
|
})
|
|
596
|
+
res.end(final)
|
|
521
597
|
})
|
|
522
598
|
} catch (error) {
|
|
523
599
|
errorPage(500, filePath, error)
|
|
@@ -530,11 +606,8 @@ const createRequestHandler = flags => {
|
|
|
530
606
|
}
|
|
531
607
|
}
|
|
532
608
|
|
|
533
|
-
const startConnectApp =
|
|
609
|
+
const startConnectApp = httpRequestHandler => {
|
|
534
610
|
return connect()
|
|
535
|
-
.use(connectLiveReload({
|
|
536
|
-
port: liveReloadPort
|
|
537
|
-
}))
|
|
538
611
|
.use('/', httpRequestHandler)
|
|
539
612
|
}
|
|
540
613
|
|
|
@@ -551,33 +624,178 @@ const startHTTPServer = (connectApp, port, flags) => {
|
|
|
551
624
|
return httpServer
|
|
552
625
|
}
|
|
553
626
|
|
|
554
|
-
const
|
|
627
|
+
const startHotReload = (wsPort, flags) => {
|
|
555
628
|
let {dir} = flags
|
|
556
629
|
const isDir = fs.statSync(dir).isDirectory()
|
|
557
630
|
if (!isDir) {
|
|
558
631
|
dir = path.parse(flags.dir).dir
|
|
559
632
|
}
|
|
560
633
|
|
|
561
|
-
const
|
|
562
|
-
const
|
|
563
|
-
|
|
634
|
+
const wss = new WebSocket.Server({port: wsPort})
|
|
635
|
+
const clients = new Map()
|
|
636
|
+
|
|
637
|
+
wss.on('connection', ws => {
|
|
638
|
+
ws.on('message', data => {
|
|
639
|
+
try {
|
|
640
|
+
const msg_ = JSON.parse(data)
|
|
641
|
+
if (msg_.path) {
|
|
642
|
+
clients.set(ws, msg_.path)
|
|
643
|
+
}
|
|
644
|
+
} catch (_) {}
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
ws.on('close', () => {
|
|
648
|
+
clients.delete(ws)
|
|
649
|
+
})
|
|
564
650
|
})
|
|
565
651
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
652
|
+
const sendToClients = (sockets, content) => {
|
|
653
|
+
for (const ws of sockets) {
|
|
654
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
655
|
+
ws.send(content)
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Debounced file watcher
|
|
661
|
+
let debounceTimer
|
|
662
|
+
const watchDir = path.resolve(dir)
|
|
663
|
+
|
|
664
|
+
const handleChange = () => {
|
|
665
|
+
// Group clients by path
|
|
666
|
+
const pathClients = new Map()
|
|
667
|
+
for (const [ws, clientPath] of clients) {
|
|
668
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
669
|
+
continue
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (!pathClients.has(clientPath)) {
|
|
673
|
+
pathClients.set(clientPath, [])
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
pathClients.get(clientPath).push(ws)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const implantOpts = {maxDepth: 10}
|
|
680
|
+
|
|
681
|
+
const implantHandlers = {
|
|
682
|
+
markserv: () => new Promise(resolve => {
|
|
683
|
+
const value = path.relative(dir, __dirname)
|
|
684
|
+
resolve(value)
|
|
685
|
+
}),
|
|
686
|
+
|
|
687
|
+
file: (url, opts) => new Promise(resolve => {
|
|
688
|
+
const absUrl = path.join(opts.baseDir, url)
|
|
689
|
+
getFile(absUrl).then(data => resolve(data)).catch(() => resolve(false))
|
|
690
|
+
}),
|
|
691
|
+
|
|
692
|
+
less: (url, opts) => new Promise(resolve => {
|
|
693
|
+
const absUrl = path.join(opts.baseDir, url)
|
|
694
|
+
buildLessStyleSheet(absUrl).then(data => resolve(data)).catch(() => resolve(false))
|
|
695
|
+
}),
|
|
696
|
+
|
|
697
|
+
markdown: (url, opts) => new Promise(resolve => {
|
|
698
|
+
const absUrl = path.join(opts.baseDir, url)
|
|
699
|
+
getFile(absUrl).then(markdownToHTML).then(data => resolve(data)).catch(() => resolve(false))
|
|
700
|
+
}),
|
|
701
|
+
|
|
702
|
+
html: (url, opts) => new Promise(resolve => {
|
|
703
|
+
const absUrl = path.join(opts.baseDir, url)
|
|
704
|
+
getFile(absUrl).then(data => resolve(data)).catch(() => resolve(false))
|
|
705
|
+
})
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
for (const [clientPath, sockets] of pathClients) {
|
|
709
|
+
const decodedUrl = getPathFromUrl(decodeURIComponent(clientPath))
|
|
710
|
+
const filePath = path.normalize(unescape(dir) + unescape(decodedUrl))
|
|
711
|
+
const baseDir = path.parse(filePath).dir
|
|
712
|
+
implantOpts.baseDir = baseDir
|
|
713
|
+
|
|
714
|
+
let stat
|
|
715
|
+
let isDir_
|
|
716
|
+
let isMarkdown
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
stat = fs.statSync(filePath)
|
|
720
|
+
isDir_ = stat.isDirectory()
|
|
721
|
+
if (!isDir_) {
|
|
722
|
+
isMarkdown = isType(fileTypes.markdown, filePath)
|
|
723
|
+
}
|
|
724
|
+
} catch (_) {
|
|
725
|
+
continue
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (isMarkdown) {
|
|
729
|
+
getFile(filePath)
|
|
730
|
+
.then(markdownToHTML)
|
|
731
|
+
.then(html => implant(html, implantHandlers, Object.assign({}, implantOpts, {baseDir})))
|
|
732
|
+
.then(output => {
|
|
733
|
+
sendToClients(sockets, output)
|
|
734
|
+
})
|
|
735
|
+
.catch(error => {
|
|
736
|
+
errormsg('hotreload', filePath, flags, error)
|
|
737
|
+
})
|
|
738
|
+
} else if (isDir_) {
|
|
739
|
+
try {
|
|
740
|
+
const content = dirToHtml(filePath)
|
|
741
|
+
const breadcrumbHtml = createBreadcrumbs(path.relative(dir, filePath))
|
|
742
|
+
let headerHtml = '<h1 class="icon folder isfolder">'
|
|
743
|
+
for (const crumb of breadcrumbHtml) {
|
|
744
|
+
headerHtml += `<a href="${crumb.href}">${crumb.text}</a>`
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
headerHtml += '</h1>\n'
|
|
748
|
+
const fullContent = headerHtml + content +
|
|
749
|
+
'<footer><sup><hr> Served by <a href="https://www.npmjs.com/package/markserv">MarkServ</a> | PID: ' + (process.pid || 'N/A') + '</sup></footer>'
|
|
750
|
+
|
|
751
|
+
sendToClients(sockets, fullContent)
|
|
752
|
+
} catch (error) {
|
|
753
|
+
errormsg('hotreload', filePath, flags, error)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
try {
|
|
760
|
+
fs.watch(watchDir, {recursive: true}, (eventType, filename) => {
|
|
761
|
+
if (!filename) {
|
|
762
|
+
return
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Check exclusions
|
|
766
|
+
for (const exclusion of fileTypes.exclusions) {
|
|
767
|
+
if (filename.includes(exclusion.replace(/\/$/, ''))) {
|
|
768
|
+
return
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Check if file extension is watched
|
|
773
|
+
const ext = path.extname(filename)
|
|
774
|
+
if (ext && !fileTypes.watch.includes(ext)) {
|
|
775
|
+
return
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
clearTimeout(debounceTimer)
|
|
779
|
+
debounceTimer = setTimeout(handleChange, 150)
|
|
780
|
+
})
|
|
781
|
+
} catch (error) {
|
|
782
|
+
errormsg('watch', watchDir, flags, error)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return wss
|
|
571
786
|
}
|
|
572
787
|
|
|
573
|
-
const logActiveServerInfo = async (serveURL, httpPort,
|
|
788
|
+
const logActiveServerInfo = async (serveURL, httpPort, wsPort, flags) => {
|
|
574
789
|
const dir = path.resolve(flags.dir)
|
|
575
790
|
|
|
576
791
|
const githubLink = 'github.com/markserv'
|
|
577
792
|
|
|
578
793
|
msg('address', style.address(serveURL), flags)
|
|
579
794
|
msg('path', chalk`{grey ${style.address(dir)}}`, flags)
|
|
580
|
-
|
|
795
|
+
|
|
796
|
+
if (wsPort) {
|
|
797
|
+
msg('hotreload', chalk`{grey ws://localhost:${style.port(wsPort)}}`, flags)
|
|
798
|
+
}
|
|
581
799
|
|
|
582
800
|
if (process.pid) {
|
|
583
801
|
msg('process', chalk`{grey your pid is: ${style.pid(process.pid)}}`, flags)
|
|
@@ -666,22 +884,31 @@ const optionalUpgrade = async flags => {
|
|
|
666
884
|
}
|
|
667
885
|
|
|
668
886
|
const init = async flags => {
|
|
669
|
-
const
|
|
670
|
-
const httpPort = flags.port
|
|
887
|
+
const preferredPort = Number(flags.port) || 8642
|
|
888
|
+
const httpPort = flags.port ? preferredPort : await getPort({port: preferredPort})
|
|
889
|
+
|
|
890
|
+
let wsPort = null
|
|
891
|
+
const hotreloadEnabled = flags.hotreload !== false && flags.hotreload !== 'false'
|
|
892
|
+
if (hotreloadEnabled) {
|
|
893
|
+
wsPort = await getPort({port: httpPort + 1})
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
flags.$wsPort = wsPort
|
|
897
|
+
flags.$hotreload = hotreloadEnabled
|
|
671
898
|
|
|
672
899
|
const httpRequestHandler = createRequestHandler(flags)
|
|
673
|
-
const connectApp = startConnectApp(
|
|
900
|
+
const connectApp = startConnectApp(httpRequestHandler)
|
|
674
901
|
const httpServer = await startHTTPServer(connectApp, httpPort, flags)
|
|
675
902
|
|
|
676
|
-
let
|
|
677
|
-
if (
|
|
678
|
-
|
|
903
|
+
let hotReloadServer
|
|
904
|
+
if (hotreloadEnabled) {
|
|
905
|
+
hotReloadServer = startHotReload(wsPort, flags)
|
|
679
906
|
}
|
|
680
907
|
|
|
681
908
|
const serveURL = 'http://' + flags.address + ':' + httpPort
|
|
682
909
|
|
|
683
910
|
// Log server info to CLI
|
|
684
|
-
logActiveServerInfo(serveURL, httpPort,
|
|
911
|
+
logActiveServerInfo(serveURL, httpPort, wsPort, flags)
|
|
685
912
|
|
|
686
913
|
let launchUrl = false
|
|
687
914
|
if (flags.$openLocation || flags.$pathProvided) {
|
|
@@ -691,7 +918,7 @@ const init = async flags => {
|
|
|
691
918
|
const service = {
|
|
692
919
|
pid: process.pid,
|
|
693
920
|
httpServer,
|
|
694
|
-
|
|
921
|
+
hotReloadServer,
|
|
695
922
|
connectApp,
|
|
696
923
|
launchUrl
|
|
697
924
|
}
|