markupeditor 0.9.48 → 0.9.50

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/bin/muedit.js CHANGED
@@ -5,13 +5,11 @@
5
5
  */
6
6
  import {argv} from 'node:process'
7
7
  import {parseArgs} from 'node:util'
8
- import express from 'express'
9
8
  import path from 'node:path'
9
+ import {createMueditServer} from './static-server.js'
10
10
 
11
11
  const __dirname = import.meta.dirname
12
12
 
13
- const app = express()
14
-
15
13
  const options = {
16
14
  'port': {
17
15
  type: 'string',
@@ -31,55 +29,18 @@ if ((values.length > 1) || (!values.port) || (positionals.length > 1)) {
31
29
  } else {
32
30
  let port = parseInt(values.port)
33
31
  let filename = (positionals.length > 0) ? positionals[0] : null
34
- let filenameAttribute = (filename) ? `filename="${filename}"` : '' // Empty for a new document
35
- // Note that `base` is set to the filename path automatically
36
- let placeholder = 'Edit document...'
37
32
 
38
33
  // The web component is always located relative to node's `__dirname`,
39
- // the location where muedit.js resides, which `app.use(express.static(__dirname))`
34
+ // the location where muedit.js resides, which serving binDir statically
40
35
  // ensures is available no matter what `process.cwd()` is.
41
- let webcomponent = `/markup-editor.js`
42
-
43
- // Allow references to dist from bin to work in index.html, to load the web component
44
- app.use(express.static(path.join(__dirname, "../dist")))
45
-
46
- // Allow the userscript and userstyles to load
47
- app.use(express.static(__dirname))
48
-
49
- // Allow the relative references inside of the edited document to work
50
- app.use(express.static(`${process.cwd()}/`, {index: false}))
51
-
52
- // For parsing application/json
53
- app.use(express.json())
54
-
55
- // Load when loading http://localhost:${port}
56
- app.get('/', (req, res) => {
57
- res.send(
58
- `
59
- <!DOCTYPE html>
60
- <html lang="en">
61
- <head>
62
- <title>${filename ?? "MarkupEditor"}</title>
63
- <meta name="viewport" charset="utf-8" content="width=device-width, initial-scale=1.0">
64
- </head>
65
- <body>
66
- <markup-editor
67
- placeholder="${placeholder}"
68
- ${filenameAttribute}
69
- userscript="/filetoolbar.js"
70
- userstyle="/filetoolbar.css"
71
- prepend="FileToolbar"
72
- >
73
- </markup-editor>
74
- <input id="docpicker" type="file" accept=".html" style="display: none">
75
- <script src="${webcomponent}" type="module"></script>
76
- </body>
77
- </html>
78
- `
79
- )
36
+ const server = createMueditServer({
37
+ distDir: path.join(__dirname, "../dist"), // Allow references to dist from bin to work, to load the web component
38
+ binDir: __dirname, // Allow the userscript and userstyles to load
39
+ cwd: process.cwd(), // Allow the relative references inside of the edited document to work
40
+ filename,
80
41
  })
81
42
 
82
- app.listen(port, () => {
43
+ server.listen(port, () => {
83
44
  console.log(`Server listening at http://localhost:${port}`)
84
45
  })
85
- }
46
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Dependency-free static file serving on node:http, shared by muedit.js
3
+ * (createMueditServer) and docs/index.js (createStaticServer). Previously
4
+ * each had its own express-based server.
5
+ */
6
+ import { createServer } from 'node:http'
7
+ import { readFile, stat } from 'node:fs/promises'
8
+ import path from 'node:path'
9
+
10
+ export const CONTENT_TYPES = {
11
+ '.html': 'text/html; charset=utf-8',
12
+ '.js': 'text/javascript; charset=utf-8',
13
+ '.css': 'text/css; charset=utf-8',
14
+ '.json': 'application/json; charset=utf-8',
15
+ '.svg': 'image/svg+xml',
16
+ '.png': 'image/png',
17
+ '.jpg': 'image/jpeg',
18
+ '.jpeg': 'image/jpeg',
19
+ '.gif': 'image/gif',
20
+ '.webp': 'image/webp',
21
+ '.ico': 'image/vnd.microsoft.icon',
22
+ }
23
+
24
+ export function contentTypeFor(filePath) {
25
+ return CONTENT_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream'
26
+ }
27
+
28
+ /**
29
+ * Resolve `urlPath` against `root`, refusing to resolve outside of it.
30
+ * Returns the absolute file path if a regular file is found, or null.
31
+ */
32
+ export async function resolveStaticFile(root, urlPath, { index = true } = {}) {
33
+ const resolvedRoot = path.resolve(root)
34
+ const decoded = decodeURIComponent(urlPath.split('?')[0])
35
+ let candidate = path.resolve(resolvedRoot, '.' + decoded)
36
+ if (candidate !== resolvedRoot && !candidate.startsWith(resolvedRoot + path.sep)) {
37
+ return null
38
+ }
39
+ try {
40
+ let stats = await stat(candidate)
41
+ if (stats.isDirectory()) {
42
+ if (!index) return null
43
+ candidate = path.join(candidate, 'index.html')
44
+ stats = await stat(candidate)
45
+ }
46
+ return stats.isFile() ? candidate : null
47
+ } catch {
48
+ return null
49
+ }
50
+ }
51
+
52
+ export function renderIndexHtml({ filename, webcomponent }) {
53
+ const filenameAttribute = filename ? `filename="${filename}"` : '' // Empty for a new document
54
+ // Note that `base` is set to the filename path automatically
55
+ const placeholder = 'Edit document...'
56
+ return `
57
+ <!DOCTYPE html>
58
+ <html lang="en">
59
+ <head>
60
+ <title>${filename ?? "MarkupEditor"}</title>
61
+ <meta name="viewport" charset="utf-8" content="width=device-width, initial-scale=1.0">
62
+ </head>
63
+ <body>
64
+ <markup-editor
65
+ placeholder="${placeholder}"
66
+ ${filenameAttribute}
67
+ userscript="/filetoolbar.js"
68
+ userstyle="/filetoolbar.css"
69
+ prepend="FileToolbar"
70
+ >
71
+ </markup-editor>
72
+ <input id="docpicker" type="file" accept=".html" style="display: none">
73
+ <script src="${webcomponent}" type="module"></script>
74
+ </body>
75
+ </html>
76
+ `
77
+ }
78
+
79
+ /**
80
+ * Serve `req`/`res` from the first matching root in `staticRoots`
81
+ * (`[{root, index}]`, checked in order), 404 if none match. Shared by
82
+ * createMueditServer and createStaticServer.
83
+ */
84
+ async function serveFromRoots(staticRoots, req, res) {
85
+ const urlPath = req.url.split('?')[0]
86
+
87
+ for (const { root, index } of staticRoots) {
88
+ const filePath = await resolveStaticFile(root, urlPath, { index })
89
+ if (filePath) {
90
+ try {
91
+ const data = await readFile(filePath)
92
+ res.writeHead(200, { 'Content-Type': contentTypeFor(filePath) })
93
+ res.end(req.method === 'HEAD' ? undefined : data)
94
+ } catch {
95
+ res.writeHead(500, { 'Content-Type': 'text/plain' })
96
+ res.end('Internal Server Error')
97
+ }
98
+ return
99
+ }
100
+ }
101
+
102
+ res.writeHead(404, { 'Content-Type': 'text/plain' })
103
+ res.end('Not Found')
104
+ }
105
+
106
+ /**
107
+ * Build (but do not start) the muedit HTTP server. Static roots are checked
108
+ * in the same priority order the previous express.static() chain used:
109
+ * distDir (the built web component), binDir (userscript/userstyle pair),
110
+ * then cwd (the edited document's own relative assets, no directory index).
111
+ * `/` is special-cased ahead of the static chain to serve the dynamic,
112
+ * templated editor shell instead of a file.
113
+ */
114
+ export function createMueditServer({ distDir, binDir, cwd, filename, webcomponent = '/markup-editor.js' }) {
115
+ const staticRoots = [
116
+ { root: distDir, index: true },
117
+ { root: binDir, index: true },
118
+ { root: cwd, index: false },
119
+ ]
120
+
121
+ return createServer(async (req, res) => {
122
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
123
+ res.writeHead(405, { 'Content-Type': 'text/plain' })
124
+ res.end('Method Not Allowed')
125
+ return
126
+ }
127
+
128
+ const urlPath = req.url.split('?')[0]
129
+
130
+ if (urlPath === '/') {
131
+ const body = renderIndexHtml({ filename, webcomponent })
132
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
133
+ res.end(req.method === 'HEAD' ? undefined : body)
134
+ return
135
+ }
136
+
137
+ await serveFromRoots(staticRoots, req, res)
138
+ })
139
+ }
140
+
141
+ /**
142
+ * Build (but do not start) a plain single-root static file server, with the
143
+ * default directory index (root/index.html served for `/`) -- what
144
+ * docs/index.js needs, as distinct from createMueditServer's multi-root
145
+ * chain and dynamic templated `/` shell.
146
+ */
147
+ export function createStaticServer({ root }) {
148
+ const staticRoots = [{ root, index: true }]
149
+
150
+ return createServer(async (req, res) => {
151
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
152
+ res.writeHead(405, { 'Content-Type': 'text/plain' })
153
+ res.end('Method Not Allowed')
154
+ return
155
+ }
156
+
157
+ await serveFromRoots(staticRoots, req, res)
158
+ })
159
+ }