markserv 1.17.4 → 1.20.0
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 +60 -29
- package/lib/cli-defs.js +23 -3
- package/lib/cli-help.txt +6 -3
- package/lib/cli.js +1 -15
- package/lib/server.js +290 -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 +240 -1
- package/lib/templates/markserv.css +191 -519
- package/package.json +8 -4
- package/CHANGELOG.md +0 -155
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')
|
|
@@ -61,6 +61,20 @@ const md = new MarkdownIt({
|
|
|
61
61
|
slugify
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
+
// Mermaid fences are rendered client-side by the lazy loader in
|
|
65
|
+
// templates/markdown.html. Pass the source through escaped (the loader
|
|
66
|
+
// reads it back via innerHTML) and keep highlight.js away from it, which
|
|
67
|
+
// would otherwise log "Could not find the language 'mermaid'".
|
|
68
|
+
// markdown-it passes strings starting with <pre straight through.
|
|
69
|
+
const {highlight} = md.options
|
|
70
|
+
md.options.highlight = (code, lang) => {
|
|
71
|
+
if (lang === 'mermaid') {
|
|
72
|
+
return `<pre class="mermaid">${md.utils.escapeHtml(code)}</pre>`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return highlight(code, lang)
|
|
76
|
+
}
|
|
77
|
+
|
|
64
78
|
// Markdown Extension Types
|
|
65
79
|
const fileTypes = {
|
|
66
80
|
markdown: [
|
|
@@ -166,12 +180,12 @@ const getFile = path => new Promise((resolve, reject) => {
|
|
|
166
180
|
|
|
167
181
|
// Get Custom Less CSS to use in all Markdown files
|
|
168
182
|
const buildLessStyleSheet = cssPath =>
|
|
169
|
-
new Promise(resolve =>
|
|
183
|
+
new Promise((resolve, reject) =>
|
|
170
184
|
getFile(cssPath).then(data =>
|
|
171
185
|
less.render(data).then(data =>
|
|
172
186
|
resolve(data.css)
|
|
173
187
|
)
|
|
174
|
-
)
|
|
188
|
+
).catch(reject)
|
|
175
189
|
)
|
|
176
190
|
|
|
177
191
|
const baseTemplate = (templateUrl, handlebarData) => new Promise((resolve, reject) => {
|
|
@@ -296,6 +310,22 @@ const createBreadcrumbs = path => {
|
|
|
296
310
|
}
|
|
297
311
|
|
|
298
312
|
// Http_request_handler: handles all the browser requests
|
|
313
|
+
const resolveTheme = flags => {
|
|
314
|
+
if (flags.light) {
|
|
315
|
+
return 'light'
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (flags.synthwave) {
|
|
319
|
+
return 'synthwave'
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (flags.theme && flags.theme !== 'dark') {
|
|
323
|
+
return flags.theme
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return 'dark'
|
|
327
|
+
}
|
|
328
|
+
|
|
299
329
|
const createRequestHandler = flags => {
|
|
300
330
|
let {dir} = flags
|
|
301
331
|
const isDir = fs.statSync(dir).isDirectory()
|
|
@@ -304,6 +334,13 @@ const createRequestHandler = flags => {
|
|
|
304
334
|
}
|
|
305
335
|
|
|
306
336
|
flags.$openLocation = path.relative(dir, flags.dir)
|
|
337
|
+
const theme = resolveTheme(flags)
|
|
338
|
+
const themeFlags = {
|
|
339
|
+
themeDark: theme === 'dark',
|
|
340
|
+
themeLight: theme === 'light',
|
|
341
|
+
themeSynthwave: theme === 'synthwave',
|
|
342
|
+
themeSolarized: theme === 'solarized'
|
|
343
|
+
}
|
|
307
344
|
|
|
308
345
|
const implantOpts = {
|
|
309
346
|
maxDepth: 10
|
|
@@ -320,6 +357,10 @@ const createRequestHandler = flags => {
|
|
|
320
357
|
}),
|
|
321
358
|
|
|
322
359
|
file: (url, opts) => new Promise(resolve => {
|
|
360
|
+
if (typeof url !== 'string' || typeof (opts && opts.baseDir) !== 'string') {
|
|
361
|
+
return resolve(false)
|
|
362
|
+
}
|
|
363
|
+
|
|
323
364
|
const absUrl = path.join(opts.baseDir, url)
|
|
324
365
|
getFile(absUrl)
|
|
325
366
|
.then(data => {
|
|
@@ -333,6 +374,10 @@ const createRequestHandler = flags => {
|
|
|
333
374
|
}),
|
|
334
375
|
|
|
335
376
|
less: (url, opts) => new Promise(resolve => {
|
|
377
|
+
if (typeof url !== 'string' || typeof (opts && opts.baseDir) !== 'string') {
|
|
378
|
+
return resolve(false)
|
|
379
|
+
}
|
|
380
|
+
|
|
336
381
|
const absUrl = path.join(opts.baseDir, url)
|
|
337
382
|
buildLessStyleSheet(absUrl)
|
|
338
383
|
.then(data => {
|
|
@@ -346,6 +391,10 @@ const createRequestHandler = flags => {
|
|
|
346
391
|
}),
|
|
347
392
|
|
|
348
393
|
markdown: (url, opts) => new Promise(resolve => {
|
|
394
|
+
if (typeof url !== 'string' || typeof (opts && opts.baseDir) !== 'string') {
|
|
395
|
+
return resolve(false)
|
|
396
|
+
}
|
|
397
|
+
|
|
349
398
|
const absUrl = path.join(opts.baseDir, url)
|
|
350
399
|
getFile(absUrl).then(markdownToHTML)
|
|
351
400
|
.then(data => {
|
|
@@ -359,6 +408,10 @@ const createRequestHandler = flags => {
|
|
|
359
408
|
}),
|
|
360
409
|
|
|
361
410
|
html: (url, opts) => new Promise(resolve => {
|
|
411
|
+
if (typeof url !== 'string' || typeof (opts && opts.baseDir) !== 'string') {
|
|
412
|
+
return resolve(false)
|
|
413
|
+
}
|
|
414
|
+
|
|
362
415
|
const absUrl = path.join(opts.baseDir, url)
|
|
363
416
|
getFile(absUrl)
|
|
364
417
|
.then(data => {
|
|
@@ -396,7 +449,12 @@ const createRequestHandler = flags => {
|
|
|
396
449
|
filePath,
|
|
397
450
|
errorMsg,
|
|
398
451
|
errorStack,
|
|
399
|
-
referer
|
|
452
|
+
referer,
|
|
453
|
+
theme,
|
|
454
|
+
...themeFlags,
|
|
455
|
+
hotreload: flags.$hotreload,
|
|
456
|
+
wsPort: flags.$wsPort,
|
|
457
|
+
rootDir: flags.dir
|
|
400
458
|
}
|
|
401
459
|
|
|
402
460
|
return baseTemplate(templateUrl, handlebarData).then(final => {
|
|
@@ -455,26 +513,42 @@ const createRequestHandler = flags => {
|
|
|
455
513
|
if (isMarkdown) {
|
|
456
514
|
msg('markdown', style.link(prettyPath), flags)
|
|
457
515
|
getFile(filePath).then(markdownToHTML).then(filePath).then(html => {
|
|
458
|
-
|
|
516
|
+
const contentPromise = flags.templates ?
|
|
517
|
+
implant(html, implantHandlers, implantOpts) :
|
|
518
|
+
Promise.resolve(html)
|
|
519
|
+
|
|
520
|
+
return contentPromise.then(output => {
|
|
459
521
|
const templateUrl = path.join(__dirname, 'templates/markdown.html')
|
|
460
522
|
|
|
461
523
|
const handlebarData = {
|
|
462
524
|
title: path.parse(filePath).base,
|
|
463
525
|
content: output,
|
|
464
|
-
pid: process.pid | 'N/A'
|
|
526
|
+
pid: process.pid | 'N/A',
|
|
527
|
+
theme,
|
|
528
|
+
...themeFlags,
|
|
529
|
+
hotreload: flags.$hotreload,
|
|
530
|
+
wsPort: flags.$wsPort,
|
|
531
|
+
rootDir: flags.dir
|
|
465
532
|
}
|
|
466
533
|
|
|
467
534
|
return baseTemplate(templateUrl, handlebarData).then(final => {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
535
|
+
if (flags.templates) {
|
|
536
|
+
const lvl2Dir = path.parse(templateUrl).dir
|
|
537
|
+
const lvl2Opts = deepmerge(implantOpts, {baseDir: lvl2Dir})
|
|
538
|
+
|
|
539
|
+
return implant(final, implantHandlers, lvl2Opts)
|
|
540
|
+
.then(output => {
|
|
541
|
+
res.writeHead(200, {
|
|
542
|
+
'content-type': 'text/html'
|
|
543
|
+
})
|
|
544
|
+
res.end(output)
|
|
475
545
|
})
|
|
476
|
-
|
|
477
|
-
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
res.writeHead(200, {
|
|
549
|
+
'content-type': 'text/html'
|
|
550
|
+
})
|
|
551
|
+
res.end(final)
|
|
478
552
|
})
|
|
479
553
|
})
|
|
480
554
|
}).catch(error => {
|
|
@@ -483,7 +557,11 @@ const createRequestHandler = flags => {
|
|
|
483
557
|
} else if (isHtml) {
|
|
484
558
|
msg('html', style.link(prettyPath), flags)
|
|
485
559
|
getFile(filePath).then(html => {
|
|
486
|
-
|
|
560
|
+
const contentPromise = flags.templates ?
|
|
561
|
+
implant(html, implantHandlers, implantOpts) :
|
|
562
|
+
Promise.resolve(html)
|
|
563
|
+
|
|
564
|
+
return contentPromise.then(output => {
|
|
487
565
|
res.writeHead(200, {
|
|
488
566
|
'content-type': 'text/html'
|
|
489
567
|
})
|
|
@@ -504,20 +582,32 @@ const createRequestHandler = flags => {
|
|
|
504
582
|
content: dirToHtml(filePath),
|
|
505
583
|
title: path.parse(filePath).base,
|
|
506
584
|
pid: process.pid | 'N/A',
|
|
507
|
-
breadcrumbs: createBreadcrumbs(path.relative(dir, filePath))
|
|
585
|
+
breadcrumbs: createBreadcrumbs(path.relative(dir, filePath)),
|
|
586
|
+
theme,
|
|
587
|
+
...themeFlags,
|
|
588
|
+
hotreload: flags.$hotreload,
|
|
589
|
+
wsPort: flags.$wsPort,
|
|
590
|
+
rootDir: flags.dir
|
|
508
591
|
}
|
|
509
592
|
|
|
510
593
|
return baseTemplate(templateUrl, handlebarData).then(final => {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
594
|
+
if (flags.templates) {
|
|
595
|
+
const lvl2Dir = path.parse(templateUrl).dir
|
|
596
|
+
const lvl2Opts = deepmerge(implantOpts, {baseDir: lvl2Dir})
|
|
597
|
+
return implant(final, implantHandlers, lvl2Opts).then(output => {
|
|
598
|
+
res.writeHead(200, {
|
|
599
|
+
'content-type': 'text/html'
|
|
600
|
+
})
|
|
601
|
+
res.end(output)
|
|
602
|
+
}).catch(error => {
|
|
603
|
+
console.error(error)
|
|
516
604
|
})
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
res.writeHead(200, {
|
|
608
|
+
'content-type': 'text/html'
|
|
520
609
|
})
|
|
610
|
+
res.end(final)
|
|
521
611
|
})
|
|
522
612
|
} catch (error) {
|
|
523
613
|
errorPage(500, filePath, error)
|
|
@@ -530,11 +620,8 @@ const createRequestHandler = flags => {
|
|
|
530
620
|
}
|
|
531
621
|
}
|
|
532
622
|
|
|
533
|
-
const startConnectApp =
|
|
623
|
+
const startConnectApp = httpRequestHandler => {
|
|
534
624
|
return connect()
|
|
535
|
-
.use(connectLiveReload({
|
|
536
|
-
port: liveReloadPort
|
|
537
|
-
}))
|
|
538
625
|
.use('/', httpRequestHandler)
|
|
539
626
|
}
|
|
540
627
|
|
|
@@ -551,33 +638,178 @@ const startHTTPServer = (connectApp, port, flags) => {
|
|
|
551
638
|
return httpServer
|
|
552
639
|
}
|
|
553
640
|
|
|
554
|
-
const
|
|
641
|
+
const startHotReload = (wsPort, flags) => {
|
|
555
642
|
let {dir} = flags
|
|
556
643
|
const isDir = fs.statSync(dir).isDirectory()
|
|
557
644
|
if (!isDir) {
|
|
558
645
|
dir = path.parse(flags.dir).dir
|
|
559
646
|
}
|
|
560
647
|
|
|
561
|
-
const
|
|
562
|
-
const
|
|
563
|
-
|
|
648
|
+
const wss = new WebSocket.Server({port: wsPort})
|
|
649
|
+
const clients = new Map()
|
|
650
|
+
|
|
651
|
+
wss.on('connection', ws => {
|
|
652
|
+
ws.on('message', data => {
|
|
653
|
+
try {
|
|
654
|
+
const msg_ = JSON.parse(data)
|
|
655
|
+
if (msg_.path) {
|
|
656
|
+
clients.set(ws, msg_.path)
|
|
657
|
+
}
|
|
658
|
+
} catch (_) {}
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
ws.on('close', () => {
|
|
662
|
+
clients.delete(ws)
|
|
663
|
+
})
|
|
564
664
|
})
|
|
565
665
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
666
|
+
const sendToClients = (sockets, content) => {
|
|
667
|
+
for (const ws of sockets) {
|
|
668
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
669
|
+
ws.send(content)
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Debounced file watcher
|
|
675
|
+
let debounceTimer
|
|
676
|
+
const watchDir = path.resolve(dir)
|
|
677
|
+
|
|
678
|
+
const handleChange = () => {
|
|
679
|
+
// Group clients by path
|
|
680
|
+
const pathClients = new Map()
|
|
681
|
+
for (const [ws, clientPath] of clients) {
|
|
682
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
683
|
+
continue
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (!pathClients.has(clientPath)) {
|
|
687
|
+
pathClients.set(clientPath, [])
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
pathClients.get(clientPath).push(ws)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const implantOpts = {maxDepth: 10}
|
|
694
|
+
|
|
695
|
+
const implantHandlers = {
|
|
696
|
+
markserv: () => new Promise(resolve => {
|
|
697
|
+
const value = path.relative(dir, __dirname)
|
|
698
|
+
resolve(value)
|
|
699
|
+
}),
|
|
700
|
+
|
|
701
|
+
file: (url, opts) => new Promise(resolve => {
|
|
702
|
+
const absUrl = path.join(opts.baseDir, url)
|
|
703
|
+
getFile(absUrl).then(data => resolve(data)).catch(() => resolve(false))
|
|
704
|
+
}),
|
|
705
|
+
|
|
706
|
+
less: (url, opts) => new Promise(resolve => {
|
|
707
|
+
const absUrl = path.join(opts.baseDir, url)
|
|
708
|
+
buildLessStyleSheet(absUrl).then(data => resolve(data)).catch(() => resolve(false))
|
|
709
|
+
}),
|
|
710
|
+
|
|
711
|
+
markdown: (url, opts) => new Promise(resolve => {
|
|
712
|
+
const absUrl = path.join(opts.baseDir, url)
|
|
713
|
+
getFile(absUrl).then(markdownToHTML).then(data => resolve(data)).catch(() => resolve(false))
|
|
714
|
+
}),
|
|
715
|
+
|
|
716
|
+
html: (url, opts) => new Promise(resolve => {
|
|
717
|
+
const absUrl = path.join(opts.baseDir, url)
|
|
718
|
+
getFile(absUrl).then(data => resolve(data)).catch(() => resolve(false))
|
|
719
|
+
})
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
for (const [clientPath, sockets] of pathClients) {
|
|
723
|
+
const decodedUrl = getPathFromUrl(decodeURIComponent(clientPath))
|
|
724
|
+
const filePath = path.normalize(unescape(dir) + unescape(decodedUrl))
|
|
725
|
+
const baseDir = path.parse(filePath).dir
|
|
726
|
+
implantOpts.baseDir = baseDir
|
|
727
|
+
|
|
728
|
+
let stat
|
|
729
|
+
let isDir_
|
|
730
|
+
let isMarkdown
|
|
731
|
+
|
|
732
|
+
try {
|
|
733
|
+
stat = fs.statSync(filePath)
|
|
734
|
+
isDir_ = stat.isDirectory()
|
|
735
|
+
if (!isDir_) {
|
|
736
|
+
isMarkdown = isType(fileTypes.markdown, filePath)
|
|
737
|
+
}
|
|
738
|
+
} catch (_) {
|
|
739
|
+
continue
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (isMarkdown) {
|
|
743
|
+
getFile(filePath)
|
|
744
|
+
.then(markdownToHTML)
|
|
745
|
+
.then(html => implant(html, implantHandlers, Object.assign({}, implantOpts, {baseDir})))
|
|
746
|
+
.then(output => {
|
|
747
|
+
sendToClients(sockets, output)
|
|
748
|
+
})
|
|
749
|
+
.catch(error => {
|
|
750
|
+
errormsg('hotreload', filePath, flags, error)
|
|
751
|
+
})
|
|
752
|
+
} else if (isDir_) {
|
|
753
|
+
try {
|
|
754
|
+
const content = dirToHtml(filePath)
|
|
755
|
+
const breadcrumbHtml = createBreadcrumbs(path.relative(dir, filePath))
|
|
756
|
+
let headerHtml = '<h1 class="icon folder isfolder">'
|
|
757
|
+
for (const crumb of breadcrumbHtml) {
|
|
758
|
+
headerHtml += `<a href="${crumb.href}">${crumb.text}</a>`
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
headerHtml += '</h1>\n'
|
|
762
|
+
const fullContent = headerHtml + content +
|
|
763
|
+
'<footer><sup><hr> Served by <a href="https://www.npmjs.com/package/markserv">MarkServ</a> | PID: ' + (process.pid || 'N/A') + '</sup></footer>'
|
|
764
|
+
|
|
765
|
+
sendToClients(sockets, fullContent)
|
|
766
|
+
} catch (error) {
|
|
767
|
+
errormsg('hotreload', filePath, flags, error)
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
try {
|
|
774
|
+
fs.watch(watchDir, {recursive: true}, (eventType, filename) => {
|
|
775
|
+
if (!filename) {
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Check exclusions
|
|
780
|
+
for (const exclusion of fileTypes.exclusions) {
|
|
781
|
+
if (filename.includes(exclusion.replace(/\/$/, ''))) {
|
|
782
|
+
return
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Check if file extension is watched
|
|
787
|
+
const ext = path.extname(filename)
|
|
788
|
+
if (ext && !fileTypes.watch.includes(ext)) {
|
|
789
|
+
return
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
clearTimeout(debounceTimer)
|
|
793
|
+
debounceTimer = setTimeout(handleChange, 150)
|
|
794
|
+
})
|
|
795
|
+
} catch (error) {
|
|
796
|
+
errormsg('watch', watchDir, flags, error)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return wss
|
|
571
800
|
}
|
|
572
801
|
|
|
573
|
-
const logActiveServerInfo = async (serveURL, httpPort,
|
|
802
|
+
const logActiveServerInfo = async (serveURL, httpPort, wsPort, flags) => {
|
|
574
803
|
const dir = path.resolve(flags.dir)
|
|
575
804
|
|
|
576
805
|
const githubLink = 'github.com/markserv'
|
|
577
806
|
|
|
578
807
|
msg('address', style.address(serveURL), flags)
|
|
579
808
|
msg('path', chalk`{grey ${style.address(dir)}}`, flags)
|
|
580
|
-
|
|
809
|
+
|
|
810
|
+
if (wsPort) {
|
|
811
|
+
msg('hotreload', chalk`{grey ws://localhost:${style.port(wsPort)}}`, flags)
|
|
812
|
+
}
|
|
581
813
|
|
|
582
814
|
if (process.pid) {
|
|
583
815
|
msg('process', chalk`{grey your pid is: ${style.pid(process.pid)}}`, flags)
|
|
@@ -666,22 +898,31 @@ const optionalUpgrade = async flags => {
|
|
|
666
898
|
}
|
|
667
899
|
|
|
668
900
|
const init = async flags => {
|
|
669
|
-
const
|
|
670
|
-
const httpPort = flags.port
|
|
901
|
+
const preferredPort = Number(flags.port) || 8642
|
|
902
|
+
const httpPort = flags.port ? preferredPort : await getPort({port: preferredPort})
|
|
903
|
+
|
|
904
|
+
let wsPort = null
|
|
905
|
+
const hotreloadEnabled = flags.hotreload !== false && flags.hotreload !== 'false'
|
|
906
|
+
if (hotreloadEnabled) {
|
|
907
|
+
wsPort = await getPort({port: httpPort + 1})
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
flags.$wsPort = wsPort
|
|
911
|
+
flags.$hotreload = hotreloadEnabled
|
|
671
912
|
|
|
672
913
|
const httpRequestHandler = createRequestHandler(flags)
|
|
673
|
-
const connectApp = startConnectApp(
|
|
914
|
+
const connectApp = startConnectApp(httpRequestHandler)
|
|
674
915
|
const httpServer = await startHTTPServer(connectApp, httpPort, flags)
|
|
675
916
|
|
|
676
|
-
let
|
|
677
|
-
if (
|
|
678
|
-
|
|
917
|
+
let hotReloadServer
|
|
918
|
+
if (hotreloadEnabled) {
|
|
919
|
+
hotReloadServer = startHotReload(wsPort, flags)
|
|
679
920
|
}
|
|
680
921
|
|
|
681
922
|
const serveURL = 'http://' + flags.address + ':' + httpPort
|
|
682
923
|
|
|
683
924
|
// Log server info to CLI
|
|
684
|
-
logActiveServerInfo(serveURL, httpPort,
|
|
925
|
+
logActiveServerInfo(serveURL, httpPort, wsPort, flags)
|
|
685
926
|
|
|
686
927
|
let launchUrl = false
|
|
687
928
|
if (flags.$openLocation || flags.$pathProvided) {
|
|
@@ -691,7 +932,7 @@ const init = async flags => {
|
|
|
691
932
|
const service = {
|
|
692
933
|
pid: process.pid,
|
|
693
934
|
httpServer,
|
|
694
|
-
|
|
935
|
+
hotReloadServer,
|
|
695
936
|
connectApp,
|
|
696
937
|
launchUrl
|
|
697
938
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
2
|
+
<html data-theme="{{theme}}">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
7
7
|
<title>{{title}}</title>
|
|
8
8
|
<meta charset="utf-8">
|
|
9
|
+
<link rel="stylesheet" id="theme-dark" href="{markserv}templates/github-markdown-dark.css"{{#unless themeDark}} disabled{{/unless}}>
|
|
10
|
+
<link rel="stylesheet" id="theme-light" href="{markserv}templates/github-markdown-light.css"{{#unless themeLight}} disabled{{/unless}}>
|
|
11
|
+
<link rel="stylesheet" id="theme-synthwave" href="{markserv}templates/github-markdown-synthwave.css"{{#unless themeSynthwave}} disabled{{/unless}}>
|
|
12
|
+
<link rel="stylesheet" id="theme-solarized" href="{markserv}templates/github-markdown-solarized.css"{{#unless themeSolarized}} disabled{{/unless}}>
|
|
9
13
|
<link rel="stylesheet" href="{markserv}templates/markserv.css">
|
|
10
14
|
<link rel="stylesheet" href="{markserv}templates/highlight-js-github-gist.css">
|
|
11
15
|
<link rel="stylesheet" href="{markserv}icons/icons.css">
|
|
@@ -16,5 +20,157 @@
|
|
|
16
20
|
{{{content}}}
|
|
17
21
|
<footer><sup><hr> Served by <a href="https://www.npmjs.com/package/markserv">MarkServ</a> | PID: {{pid}}</sup></footer>
|
|
18
22
|
</article>
|
|
23
|
+
<div class="width-control" id="width-control">
|
|
24
|
+
<span id="width-tooltip"></span>
|
|
25
|
+
<input type="range" id="width-slider" min="0" max="1920" value="978" step="1">
|
|
26
|
+
</div>
|
|
27
|
+
<button class="theme-toggle" id="theme-toggle" title="Toggle theme"></button>
|
|
28
|
+
<script>
|
|
29
|
+
(function() {
|
|
30
|
+
var themes = ['dark', 'light', 'synthwave', 'solarized'];
|
|
31
|
+
var icons = {dark: '\u{1F319}', light: '\u{2600}\u{FE0F}', synthwave: '\u{1F9D1}\u{200D}\u{1F3A4}', solarized: '\u{262F}\u{FE0F}'};
|
|
32
|
+
var serverTheme = '{{theme}}' || 'dark';
|
|
33
|
+
var current = localStorage.getItem('markserv-theme') || serverTheme;
|
|
34
|
+
|
|
35
|
+
function applyTheme(t) {
|
|
36
|
+
document.documentElement.setAttribute('data-theme', t);
|
|
37
|
+
themes.forEach(function(name) {
|
|
38
|
+
var el = document.getElementById('theme-' + name);
|
|
39
|
+
if (el) el.disabled = (name !== t);
|
|
40
|
+
});
|
|
41
|
+
var btn = document.getElementById('theme-toggle');
|
|
42
|
+
if (btn) btn.textContent = icons[t] || icons.dark;
|
|
43
|
+
current = t;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
applyTheme(current);
|
|
47
|
+
|
|
48
|
+
document.getElementById('theme-toggle').addEventListener('click', function() {
|
|
49
|
+
var idx = themes.indexOf(current);
|
|
50
|
+
var next = themes[(idx + 1) % themes.length];
|
|
51
|
+
localStorage.setItem('markserv-theme', next);
|
|
52
|
+
applyTheme(next);
|
|
53
|
+
});
|
|
54
|
+
})();
|
|
55
|
+
</script>
|
|
56
|
+
<script>
|
|
57
|
+
(function() {
|
|
58
|
+
var slider = document.getElementById('width-slider');
|
|
59
|
+
var tooltip = document.getElementById('width-tooltip');
|
|
60
|
+
if (!slider) return;
|
|
61
|
+
|
|
62
|
+
var CSS_DEFAULT = 978; // must match body { width } in markserv.css
|
|
63
|
+
var storageKey = 'markserv-width:' + '{{rootDir}}';
|
|
64
|
+
var stored = localStorage.getItem(storageKey);
|
|
65
|
+
|
|
66
|
+
function updateSliderMax() {
|
|
67
|
+
// Slider max = current viewport width so user can slide up to full-bleed
|
|
68
|
+
slider.max = window.innerWidth;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function applyWidth(px) {
|
|
72
|
+
if (px === CSS_DEFAULT) {
|
|
73
|
+
// Matches stylesheet default — remove inline style so CSS rules apply normally
|
|
74
|
+
document.body.style.width = '';
|
|
75
|
+
} else {
|
|
76
|
+
document.body.style.width = px + 'px';
|
|
77
|
+
}
|
|
78
|
+
slider.value = Math.min(px, parseInt(slider.max, 10));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Initialize
|
|
82
|
+
updateSliderMax();
|
|
83
|
+
if (stored) {
|
|
84
|
+
applyWidth(parseInt(stored, 10));
|
|
85
|
+
} else {
|
|
86
|
+
// No stored value — keep the 978px stylesheet default; slider reflects it
|
|
87
|
+
slider.value = Math.min(CSS_DEFAULT, parseInt(slider.max, 10));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function positionTooltip() {
|
|
91
|
+
if (!tooltip) return;
|
|
92
|
+
var val = parseInt(slider.value, 10);
|
|
93
|
+
var min = parseInt(slider.min, 10);
|
|
94
|
+
var max = parseInt(slider.max, 10);
|
|
95
|
+
var pct = (val - min) / (max - min);
|
|
96
|
+
var sliderW = slider.offsetWidth;
|
|
97
|
+
var thumbW = 14;
|
|
98
|
+
var offset = thumbW / 2 + pct * (sliderW - thumbW);
|
|
99
|
+
tooltip.textContent = val + 'px';
|
|
100
|
+
tooltip.style.left = offset + 'px';
|
|
101
|
+
tooltip.style.transform = 'translateX(-50%)';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Slider input → live update
|
|
105
|
+
slider.addEventListener('input', function() {
|
|
106
|
+
var val = parseInt(slider.value, 10);
|
|
107
|
+
applyWidth(val);
|
|
108
|
+
localStorage.setItem(storageKey, String(val));
|
|
109
|
+
if (tooltip) tooltip.classList.add('visible');
|
|
110
|
+
positionTooltip();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
slider.addEventListener('mousedown', function() {
|
|
114
|
+
if (tooltip) tooltip.classList.add('visible');
|
|
115
|
+
positionTooltip();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
window.addEventListener('mouseup', function() {
|
|
119
|
+
if (tooltip) tooltip.classList.remove('visible');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
slider.addEventListener('focus', function() {
|
|
123
|
+
if (tooltip) tooltip.classList.add('visible');
|
|
124
|
+
positionTooltip();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
slider.addEventListener('blur', function() {
|
|
128
|
+
if (tooltip) tooltip.classList.remove('visible');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Double-click to reset to default width
|
|
132
|
+
slider.addEventListener('dblclick', function() {
|
|
133
|
+
applyWidth(CSS_DEFAULT);
|
|
134
|
+
localStorage.removeItem(storageKey);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// On window resize, update slider max to reflect new viewport
|
|
138
|
+
window.addEventListener('resize', function() {
|
|
139
|
+
updateSliderMax();
|
|
140
|
+
});
|
|
141
|
+
})();
|
|
142
|
+
</script>
|
|
143
|
+
{{#if hotreload}}
|
|
144
|
+
<script>
|
|
145
|
+
(function() {
|
|
146
|
+
var ws;
|
|
147
|
+
var delay = 1000;
|
|
148
|
+
var maxDelay = 30000;
|
|
149
|
+
var maxRetries = 20;
|
|
150
|
+
var retries = 0;
|
|
151
|
+
|
|
152
|
+
function connect() {
|
|
153
|
+
ws = new WebSocket('ws://localhost:{{wsPort}}');
|
|
154
|
+
ws.onopen = function() {
|
|
155
|
+
delay = 1000;
|
|
156
|
+
retries = 0;
|
|
157
|
+
ws.send(JSON.stringify({path: location.pathname}));
|
|
158
|
+
};
|
|
159
|
+
ws.onmessage = function(e) {
|
|
160
|
+
var el = document.querySelector('.markdown-body');
|
|
161
|
+
if (el) el.innerHTML = e.data;
|
|
162
|
+
};
|
|
163
|
+
ws.onclose = function() {
|
|
164
|
+
if (retries < maxRetries) {
|
|
165
|
+
retries++;
|
|
166
|
+
setTimeout(connect, delay);
|
|
167
|
+
delay = Math.min(delay * 2, maxDelay);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
connect();
|
|
172
|
+
})();
|
|
173
|
+
</script>
|
|
174
|
+
{{/if}}
|
|
19
175
|
</body>
|
|
20
|
-
</html>
|
|
176
|
+
</html>
|