@tenjuu99/blog 0.1.9 → 0.2.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/lib/applyTemplate.js +13 -5
- package/lib/cssGenerator.js +6 -5
- package/lib/dir.js +2 -0
- package/lib/distribute.js +40 -11
- package/lib/files.js +43 -0
- package/lib/filter.js +1 -3
- package/lib/generate.js +8 -5
- package/lib/includeFilter.js +15 -15
- package/lib/indexer.js +19 -24
- package/lib/pageData.js +3 -17
- package/lib/render.js +8 -4
- package/lib/server.js +8 -2
- package/lib/tryServer.js +58 -0
- package/package.json +1 -1
- package/src-sample/css/layout.css +1 -0
- package/src-sample/pages/editor.html +3 -0
- package/src-sample/pages/post/1.md +1 -0
- package/src-sample/pages/post/2.md +1 -0
- package/src-sample/pages/post/3.md +1 -0
- package/src-sample/pages/post/index.md +7 -0
- package/src-sample/server/editor.js +38 -0
- package/src-sample/server/get_editor_target.js +29 -0
- package/src-sample/server/preview.js +28 -0
- package/src-sample/template/default.html +5 -0
- package/src-sample/template/editor.html +197 -0
package/lib/applyTemplate.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict"
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
2
|
import applyCss from './cssGenerator.js'
|
|
4
|
-
import
|
|
3
|
+
import includeFilter from './includeFilter.js'
|
|
5
4
|
import { templateDir, cssDir } from './dir.js'
|
|
5
|
+
import { staticFile, staticFiles, warmUp } from './files.js'
|
|
6
6
|
import { watchers } from './watcher.js'
|
|
7
7
|
|
|
8
8
|
let templates = {}
|
|
@@ -11,16 +11,24 @@ const applyTemplate = async (name = 'default.html') => {
|
|
|
11
11
|
if (templates[name]) {
|
|
12
12
|
return templates[name]
|
|
13
13
|
}
|
|
14
|
-
let templateContent = await
|
|
15
|
-
templateContent =
|
|
14
|
+
let templateContent = await staticFile(`template/${name}`)
|
|
15
|
+
templateContent = includeFilter(templateContent)
|
|
16
16
|
templateContent = await applyCss(templateContent)
|
|
17
17
|
templates[name] = templateContent
|
|
18
18
|
return templateContent
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const warmUpTemplate = async () => {
|
|
22
|
+
await warmUp()
|
|
23
|
+
const templates = staticFiles()
|
|
24
|
+
.filter(file => file[0].indexOf('template/') === 0)
|
|
25
|
+
.map(f => applyTemplate(f[0].split('/')[1]))
|
|
26
|
+
await Promise.all(templates)
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
watchers.push({
|
|
22
30
|
paths: [cssDir, templateDir],
|
|
23
31
|
callback: () => { templates = {} }
|
|
24
32
|
})
|
|
25
33
|
|
|
26
|
-
export
|
|
34
|
+
export { applyTemplate, warmUpTemplate }
|
package/lib/cssGenerator.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import { staticFile } from './files.js'
|
|
3
4
|
import { minifyCss } from './minify.js'
|
|
4
5
|
import { createHash } from 'crypto'
|
|
5
6
|
import path from 'path'
|
|
6
|
-
import { distDir
|
|
7
|
+
import { distDir, cssDir } from './dir.js'
|
|
7
8
|
import { watchers } from './watcher.js'
|
|
8
9
|
import { styleText } from 'node:util'
|
|
9
10
|
import config from './config.js'
|
|
@@ -26,14 +27,14 @@ const cssGenerator = async (src, dist) => {
|
|
|
26
27
|
}
|
|
27
28
|
let css = ''
|
|
28
29
|
for (const cssFile of src.split(',')) {
|
|
29
|
-
css +=
|
|
30
|
+
css += staticFile(`css/${cssFile}`)
|
|
30
31
|
}
|
|
31
32
|
css = minifyCss(css)
|
|
32
33
|
cacheBuster[key] = createHash('md5').update(css).digest('hex')
|
|
33
34
|
|
|
34
|
-
return await fs.mkdir(`${
|
|
35
|
-
fs.writeFile(`${
|
|
36
|
-
console.log(styleText('green', '[generate]'), `${src} => ${
|
|
35
|
+
return await fs.mkdir(`${distDir}${path.dirname(dist)}`, { recursive: true }).then(() => {
|
|
36
|
+
fs.writeFile(`${distDir}${dist}`, css)
|
|
37
|
+
console.log(styleText('green', '[generate]'), `${src} => ${distDir}${dist}`)
|
|
37
38
|
return cacheBuster[key]
|
|
38
39
|
})
|
|
39
40
|
}
|
package/lib/dir.js
CHANGED
|
@@ -7,6 +7,7 @@ const pageDir = `${srcDir}/pages`
|
|
|
7
7
|
const templateDir = `${srcDir}/template`
|
|
8
8
|
const cssDir = `${srcDir}/css`
|
|
9
9
|
const cacheDir = `${rootDir}/.cache`
|
|
10
|
+
const serverDir = `${srcDir}/server`
|
|
10
11
|
|
|
11
12
|
export {
|
|
12
13
|
rootDir,
|
|
@@ -16,4 +17,5 @@ export {
|
|
|
16
17
|
templateDir,
|
|
17
18
|
cssDir,
|
|
18
19
|
cacheDir,
|
|
20
|
+
serverDir
|
|
19
21
|
}
|
package/lib/distribute.js
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
3
4
|
import path from 'path'
|
|
4
5
|
import { minifyHtml } from './minify.js'
|
|
5
6
|
import render from './render.js'
|
|
6
7
|
import { styleText } from 'node:util'
|
|
7
8
|
import config from './config.js'
|
|
9
|
+
import { cacheDir } from './dir.js'
|
|
10
|
+
import { applyTemplate, warmUpTemplate } from './applyTemplate.js'
|
|
8
11
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
const indexFile = `${cacheDir}/index.json`
|
|
13
|
+
|
|
14
|
+
const renderPage = async (page) => {
|
|
15
|
+
const template = page.template
|
|
16
|
+
return [page.name, await render(template, page)]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const distribute = async (data, srcDir, distDir) => {
|
|
20
|
+
await warmUpTemplate()
|
|
21
|
+
const promises = []
|
|
22
|
+
const newIndex = []
|
|
17
23
|
for (const name in data) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
promises.push(renderPage(data[name]))
|
|
25
|
+
newIndex.push({ name: data.name, url: data.url, __output: data.__output })
|
|
26
|
+
}
|
|
27
|
+
const renderedString = await Promise.all(promises)
|
|
28
|
+
for (const page of renderedString) {
|
|
29
|
+
const [ pageName, rendered ] = page
|
|
30
|
+
let writeTo = `${distDir}${data[pageName].__output}`
|
|
21
31
|
fs.mkdir(path.dirname(writeTo), { recursive: true}).then(() => {
|
|
22
32
|
fs.writeFile(writeTo, minifyHtml(rendered))
|
|
23
33
|
console.log(styleText('green', '[generate]'), writeTo)
|
|
@@ -33,6 +43,25 @@ const distribute = async (data, deleted, srcDir, distDir) => {
|
|
|
33
43
|
})
|
|
34
44
|
})
|
|
35
45
|
})
|
|
46
|
+
|
|
47
|
+
if (!existsSync(cacheDir)) {
|
|
48
|
+
mkdirSync(cacheDir)
|
|
49
|
+
}
|
|
50
|
+
fs.readFile(indexFile, 'utf8')
|
|
51
|
+
.then(text => {
|
|
52
|
+
const oldIndex = JSON.parse(text)
|
|
53
|
+
deleted = oldIndex.filter(oi => !newIndex.map(ni => ni.__output).includes(oi.__output))
|
|
54
|
+
fs.writeFile(indexFile, JSON.stringify(newIndex))
|
|
55
|
+
if (deleted) {
|
|
56
|
+
for (const obj of deleted) {
|
|
57
|
+
console.log(styleText('red', '[unlink]'), `${distDir}${obj.__output}`)
|
|
58
|
+
fs.unlink(`${distDir}${obj.__output}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
.catch(error => {
|
|
63
|
+
fs.writeFile(indexFile, JSON.stringify(newIndex))
|
|
64
|
+
})
|
|
36
65
|
}
|
|
37
66
|
|
|
38
67
|
export default distribute
|
package/lib/files.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { templateDir, cssDir } from './dir.js'
|
|
3
|
+
import { watchers } from './watcher.js'
|
|
4
|
+
|
|
5
|
+
let staticFilesContainer = {}
|
|
6
|
+
let loaded = false
|
|
7
|
+
|
|
8
|
+
const warmUp = async () => {
|
|
9
|
+
if (loaded) {
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
const templateFiles = await fs.readdir(templateDir).then(files => files.map(f => [`template/${f}`, `${templateDir}/${f}`]))
|
|
13
|
+
const cssFiles = await fs.readdir(cssDir).then(files => files.map(f => [`css/${f}`, `${cssDir}/${f}`]))
|
|
14
|
+
const files = [...templateFiles, ...cssFiles]
|
|
15
|
+
const loadFiles = files.map(file => fs.readFile(file[1], 'utf8').then(content => [file[0], content]))
|
|
16
|
+
staticFilesContainer = Object.fromEntries(await Promise.all(loadFiles))
|
|
17
|
+
loaded = true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const staticFile = (name) => {
|
|
21
|
+
if (!loaded) {
|
|
22
|
+
throw new Error('not initialized')
|
|
23
|
+
}
|
|
24
|
+
if (staticFilesContainer[name]) {
|
|
25
|
+
return staticFilesContainer[name]
|
|
26
|
+
}
|
|
27
|
+
if (name.indexOf('template/') === 0) {
|
|
28
|
+
return fs.readFile(templateDir + '/' + name.replace('template/', ''))
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const staticFiles = () => {
|
|
33
|
+
return Object.entries(staticFilesContainer)
|
|
34
|
+
}
|
|
35
|
+
watchers.push({
|
|
36
|
+
paths: [cssDir, templateDir],
|
|
37
|
+
callback: async () => {
|
|
38
|
+
loaded = false
|
|
39
|
+
await warmUp()
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export { staticFile, staticFiles, warmUp }
|
package/lib/filter.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import helper from '../lib/helper.js'
|
|
2
|
-
import includeFilter from './includeFilter.js'
|
|
3
2
|
import { srcDir } from './dir.js'
|
|
4
3
|
import config from './config.js'
|
|
5
4
|
import replaceVariablesFilter from './replaceVariablesFilter.js'
|
|
@@ -122,6 +121,5 @@ const replaceScriptFilter = async (text, variables) => {
|
|
|
122
121
|
export {
|
|
123
122
|
replaceIfFilter,
|
|
124
123
|
replaceScriptFilter,
|
|
125
|
-
replaceVariablesFilter
|
|
126
|
-
includeFilter
|
|
124
|
+
replaceVariablesFilter
|
|
127
125
|
}
|
package/lib/generate.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
import distribute from './distribute.js'
|
|
3
|
-
import { indexing, allData
|
|
3
|
+
import { indexing, allData } from './indexer.js'
|
|
4
4
|
import { srcDir, distDir } from './dir.js'
|
|
5
5
|
import { styleText } from 'node:util'
|
|
6
6
|
|
|
7
7
|
const generate = async () => {
|
|
8
|
-
|
|
8
|
+
let start = performance.now()
|
|
9
9
|
await indexing()
|
|
10
|
+
let end = performance.now()
|
|
11
|
+
console.log(styleText('blue', '[indexing: ' + (end - start) + "ms]"))
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
start = performance.now()
|
|
14
|
+
await distribute(allData, srcDir, distDir)
|
|
15
|
+
end = performance.now()
|
|
16
|
+
console.log(styleText('blue', '[distribute: ' + (end - start) + "ms]"))
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export default generate
|
package/lib/includeFilter.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
1
|
import { minifyCss } from './minify.js'
|
|
3
2
|
import { templateDir, cssDir } from './dir.js'
|
|
3
|
+
import { staticFile } from './files.js'
|
|
4
4
|
|
|
5
5
|
const alreadyLoaded = {}
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const includeRegexp = new RegExp(/\{\s*include\('(template|css)\/([\w\./]+)'\)\s*\}/g)
|
|
8
|
+
|
|
9
|
+
const includeFilter = (text) => {
|
|
8
10
|
let replaced = text
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
+
const include = [...text.matchAll(includeRegexp)].map(matched => {
|
|
12
|
+
return { toBeReplace: matched[0], type: matched[1], filename: matched[2] }
|
|
13
|
+
})
|
|
14
|
+
if (include.length === 0) {
|
|
15
|
+
return replaced
|
|
16
|
+
}
|
|
11
17
|
for (const index in include) {
|
|
12
|
-
const
|
|
18
|
+
const {toBeReplace, type, filename} = include[index]
|
|
13
19
|
let content
|
|
14
20
|
const cacheKey = `${type}/${filename}`
|
|
15
21
|
if (!alreadyLoaded[cacheKey]) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
break
|
|
20
|
-
case 'css':
|
|
21
|
-
content = await fs.readFile(`${cssDir}/${filename}`, 'utf8')
|
|
22
|
-
break
|
|
23
|
-
default:
|
|
24
|
-
throw new Error('type does not match neither `template` nor `css`.');
|
|
22
|
+
content = staticFile(cacheKey)
|
|
23
|
+
if (typeof content === 'undefined') {
|
|
24
|
+
throw new Error(cacheKey + ' is not found')
|
|
25
25
|
}
|
|
26
26
|
// include を再帰的に解決する
|
|
27
27
|
if (content.match(includeRegexp)) {
|
|
28
|
-
content =
|
|
28
|
+
content = includeFilter(content)
|
|
29
29
|
}
|
|
30
30
|
alreadyLoaded[cacheKey] = content
|
|
31
31
|
} else {
|
package/lib/indexer.js
CHANGED
|
@@ -1,45 +1,40 @@
|
|
|
1
1
|
"use strict"
|
|
2
|
-
import {
|
|
3
|
-
import { readdirSync
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { readdirSync } from "node:fs";
|
|
4
4
|
import { pageDir, cacheDir } from './dir.js'
|
|
5
5
|
import makePageData from './pageData.js'
|
|
6
6
|
|
|
7
|
-
const indexFile = `${cacheDir}/index.json`
|
|
8
|
-
|
|
9
|
-
let newIndex = []
|
|
10
7
|
let allData = {}
|
|
11
|
-
let deleted = []
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} dir
|
|
11
|
+
* @param {string} namePrefix
|
|
12
|
+
* @param {Array<Promise>} promises
|
|
13
|
+
* @return {Promise<string[]>[]}
|
|
14
|
+
*/
|
|
15
|
+
const collect = (dir, namePrefix = '', promises = []) => {
|
|
14
16
|
const dirents = readdirSync(dir, { withFileTypes: true })
|
|
15
17
|
dirents.forEach((dirent) => {
|
|
16
18
|
if (dirent.isDirectory()) {
|
|
17
|
-
collect(`${dirent.path}/${dirent.name}`,
|
|
19
|
+
collect(`${dirent.path}/${dirent.name}`, namePrefix + dirent.name + '/', promises)
|
|
18
20
|
} else {
|
|
19
21
|
if (dirent.name.match(/\.(md|html)$/)) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const { name, url, __output } = pageData
|
|
23
|
-
newIndex.push({ name, url, __output })
|
|
22
|
+
const name = `${namePrefix}${dirent.name}`
|
|
23
|
+
promises.push(readFile(`${dir}/${dirent.name}`, 'utf8').then(f => [name, f]))
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
})
|
|
27
|
+
return promises
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const indexing = async () => {
|
|
30
|
-
newIndex = []
|
|
31
31
|
allData = {}
|
|
32
|
-
deleted = []
|
|
33
|
-
if (!existsSync(cacheDir)) {
|
|
34
|
-
mkdirSync(cacheDir)
|
|
35
|
-
}
|
|
36
|
-
const oldIndex = await readFile(indexFile, 'utf8').then(text => JSON.parse(text)).catch(error => [])
|
|
37
|
-
|
|
38
|
-
collect(pageDir)
|
|
39
|
-
writeFile(indexFile, JSON.stringify(newIndex))
|
|
40
32
|
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
const files = await Promise.all(collect(pageDir))
|
|
34
|
+
files.forEach((file) => {
|
|
35
|
+
const pageData = makePageData(file[0], file[1])
|
|
36
|
+
allData[pageData.name] = pageData
|
|
37
|
+
})
|
|
43
38
|
}
|
|
44
39
|
|
|
45
|
-
export { indexing, allData
|
|
40
|
+
export { indexing, allData }
|
package/lib/pageData.js
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
"use strict"
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import { pageDir } from './dir.js'
|
|
4
2
|
import config from './config.js'
|
|
5
3
|
|
|
6
|
-
const
|
|
7
|
-
return fs.readFileSync(path, 'utf8')
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const makePageData = (filename) => {
|
|
11
|
-
const content = load(`${pageDir}/${filename}`)
|
|
4
|
+
const makePageData = (filename, content) => {
|
|
12
5
|
const [name, ext] = filename.split('.')
|
|
13
6
|
return parse(content, name, ext)
|
|
14
7
|
}
|
|
@@ -17,7 +10,7 @@ const parse = (content, name, ext) => {
|
|
|
17
10
|
const regexp = new RegExp(/^(<!|-)--(?<variables>[\s\S]*?)--(-|>)/)
|
|
18
11
|
const matched = content.match(regexp)
|
|
19
12
|
const markdownReplaced = content.replace(regexp, '')
|
|
20
|
-
const metaDataDefault = {
|
|
13
|
+
const metaDataDefault = Object.assign({
|
|
21
14
|
name,
|
|
22
15
|
title: name,
|
|
23
16
|
url: `/${name}`,
|
|
@@ -29,14 +22,13 @@ const parse = (content, name, ext) => {
|
|
|
29
22
|
lang: 'ja',
|
|
30
23
|
site_name: config.site_name,
|
|
31
24
|
url_base: config.url_base,
|
|
32
|
-
gtag_id: config.gtag_id,
|
|
33
25
|
markdown: markdownReplaced,
|
|
34
26
|
relative_path: config.relative_path || '',
|
|
35
27
|
template: 'default.html',
|
|
36
28
|
ext: 'html',
|
|
37
29
|
__output: `/${name}.html`,
|
|
38
30
|
__filetype: ext,
|
|
39
|
-
}
|
|
31
|
+
}, config)
|
|
40
32
|
if (!matched) {
|
|
41
33
|
return metaDataDefault
|
|
42
34
|
}
|
|
@@ -53,12 +45,6 @@ const parse = (content, name, ext) => {
|
|
|
53
45
|
})
|
|
54
46
|
)
|
|
55
47
|
const metaDataMerged = Object.assign(metaDataDefault, metaData)
|
|
56
|
-
if (!metaDataMerged.description) {
|
|
57
|
-
metaDataMerged.description = markdownReplaced.replace(/(<([^>]+)>)/gi, '').slice(0, 200).replaceAll("\n", '') + '...'
|
|
58
|
-
}
|
|
59
|
-
if (!metaDataMerged.og_description) {
|
|
60
|
-
metaDataMerged.og_description = metaDataMerged.og_description
|
|
61
|
-
}
|
|
62
48
|
metaDataMerged['__output'] = name === 'index' ? '/index.html' : `${metaDataMerged.url}.${metaDataMerged.ext}`
|
|
63
49
|
|
|
64
50
|
return metaDataMerged
|
package/lib/render.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
replaceIfFilter,
|
|
3
3
|
replaceScriptFilter,
|
|
4
|
-
replaceVariablesFilter
|
|
5
|
-
includeFilter
|
|
4
|
+
replaceVariablesFilter
|
|
6
5
|
} from './filter.js'
|
|
6
|
+
import includeFilter from './includeFilter.js'
|
|
7
7
|
import { marked } from "marked";
|
|
8
|
-
import applyTemplate from './applyTemplate.js'
|
|
8
|
+
import { applyTemplate } from './applyTemplate.js'
|
|
9
9
|
|
|
10
10
|
const render = async (templateName, data) => {
|
|
11
11
|
let template = await applyTemplate(templateName)
|
|
@@ -13,11 +13,15 @@ const render = async (templateName, data) => {
|
|
|
13
13
|
template = await replaceScriptFilter(template, data)
|
|
14
14
|
|
|
15
15
|
let markdown = data.markdown
|
|
16
|
-
markdown =
|
|
16
|
+
markdown = includeFilter(markdown)
|
|
17
17
|
markdown = await replaceIfFilter(markdown, data)
|
|
18
18
|
markdown = await replaceScriptFilter(markdown, data)
|
|
19
19
|
markdown = replaceVariablesFilter(markdown, data)
|
|
20
20
|
data.markdown = data.__filetype === 'md' ? marked.parse(markdown) : markdown
|
|
21
|
+
if (!data.description) {
|
|
22
|
+
data.description = data.markdown.replaceAll("\n", '').replace(/(<([^>]+)>)/gi, '').slice(0, 150) + '...'
|
|
23
|
+
data.og_description = data.description
|
|
24
|
+
}
|
|
21
25
|
|
|
22
26
|
return replaceVariablesFilter(template, data)
|
|
23
27
|
}
|
package/lib/server.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import http from 'http'
|
|
2
2
|
import url from 'url'
|
|
3
3
|
import fs from 'node:fs'
|
|
4
|
-
import { distDir } from './dir.js'
|
|
4
|
+
import { distDir, serverDir } from './dir.js'
|
|
5
5
|
import { styleText } from 'node:util'
|
|
6
|
+
import handle from './tryServer.js'
|
|
6
7
|
|
|
7
8
|
const contentType = (ext) => {
|
|
8
9
|
switch (ext) {
|
|
@@ -31,11 +32,16 @@ const contentType = (ext) => {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
const server = () => {
|
|
34
|
-
return http.createServer((request, response) => {
|
|
35
|
+
return http.createServer(async (request, response) => {
|
|
36
|
+
request.setEncoding('utf8')
|
|
35
37
|
const url = new URL(`http://${request.headers.host}${request.url}`)
|
|
36
38
|
const isIndex = url.pathname.match(/(.+)?\/$/)
|
|
37
39
|
let path = isIndex ? `${url.pathname}index.html` : decodeURIComponent(url.pathname)
|
|
38
40
|
if (!path.includes('.')) {
|
|
41
|
+
const result = await handle(path, request, response)
|
|
42
|
+
if (result) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
39
45
|
path += '.html'
|
|
40
46
|
}
|
|
41
47
|
if (!fs.existsSync(`${distDir}${path}`)) {
|
package/lib/tryServer.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import http from 'http'
|
|
2
|
+
import { distDir, serverDir } from './dir.js'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import { styleText } from 'node:util'
|
|
5
|
+
|
|
6
|
+
let handlersAlreadyRegistered = false
|
|
7
|
+
const registeredHandlers = {}
|
|
8
|
+
const handlers = async (path) => {
|
|
9
|
+
if (handlersAlreadyRegistered) {
|
|
10
|
+
return registeredHandlers[path]
|
|
11
|
+
}
|
|
12
|
+
const serverFiles = fs.readdirSync(serverDir)
|
|
13
|
+
const loaded = await Promise.all(serverFiles.map(file => import(`${serverDir}/${file}`)))
|
|
14
|
+
loaded.forEach(s => registeredHandlers[s.path] = s)
|
|
15
|
+
handlersAlreadyRegistered = true
|
|
16
|
+
return registeredHandlers[path]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} path
|
|
21
|
+
* @param {http.IncomingMessage} req
|
|
22
|
+
* @param {http.ServerResponse} res
|
|
23
|
+
*/
|
|
24
|
+
const tryServer = async (path, req, res) => {
|
|
25
|
+
const handler = await handlers(path)
|
|
26
|
+
const method = req.method.toLowerCase()
|
|
27
|
+
if (handler && handler[method]) {
|
|
28
|
+
console.log(styleText('blue', `[server ${method.toUpperCase()} ${path}]`))
|
|
29
|
+
try {
|
|
30
|
+
const response = await handler[method](req, res)
|
|
31
|
+
if (response) {
|
|
32
|
+
return response
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.log(e)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} path
|
|
42
|
+
* @param {http.IncomingMessage} request
|
|
43
|
+
* @param {http.ServerResponse} response
|
|
44
|
+
*/
|
|
45
|
+
const getResponse = async (path , request, response) => {
|
|
46
|
+
const url = new URL(`http://${request.headers.host}${request.url}`)
|
|
47
|
+
const res = await tryServer(url.pathname, request, response)
|
|
48
|
+
if (res) {
|
|
49
|
+
if (res === true) {
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
const { status, contentType, body } = res
|
|
53
|
+
response.writeHead(status, {'content-type': contentType })
|
|
54
|
+
return response.end(body)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default getResponse
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
top > [post](/post/) > /post/1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
top > [post](/post/) > /post/2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
top > [post](/post/) > /post/3
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'http'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import { styleText } from 'node:util'
|
|
4
|
+
|
|
5
|
+
const config = (await import('../../lib/config.js')).default
|
|
6
|
+
const dir = (await import('../../lib/dir.js'))
|
|
7
|
+
|
|
8
|
+
export const path = '/editor'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {IncomingMessage} req
|
|
12
|
+
* @param {ServerResponse} res
|
|
13
|
+
*/
|
|
14
|
+
export const post = async (req, res) => {
|
|
15
|
+
const chunks = []
|
|
16
|
+
req
|
|
17
|
+
.on('data', (chunk) => chunks.push(chunk))
|
|
18
|
+
.on('end', async () => {
|
|
19
|
+
const json = JSON.parse(chunks.join())
|
|
20
|
+
const file = json.inputFileName ? json.inputFileName : json.selectDataFile
|
|
21
|
+
if (!file) {
|
|
22
|
+
res.writeHead(401, { 'content-type': 'application/json' })
|
|
23
|
+
res.end(JSON.stringify({
|
|
24
|
+
'message': 'ファイル名がありません'
|
|
25
|
+
}))
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
await fs.writeFile(`${dir.pageDir}/${file}`, json.content)
|
|
29
|
+
console.log(styleText('blue', '[editor/post] finished'))
|
|
30
|
+
|
|
31
|
+
const href = file.split('.')[0]
|
|
32
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
33
|
+
res.end(JSON.stringify({
|
|
34
|
+
'href': `/${href}`
|
|
35
|
+
}))
|
|
36
|
+
})
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'http'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
|
|
4
|
+
const config = (await import('../../lib/config.js')).default
|
|
5
|
+
const pageDir = (await import('../../lib/dir.js')).pageDir
|
|
6
|
+
|
|
7
|
+
export const path = '/get_editor_target'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {IncomingMessage} req
|
|
11
|
+
* @param {ServerResponse} res
|
|
12
|
+
*/
|
|
13
|
+
export const get = async (req, res) => {
|
|
14
|
+
const url = new URL(`${config.url_base}${req.url}`)
|
|
15
|
+
const target = url.searchParams.get('md')
|
|
16
|
+
if (!target) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
const file = `${pageDir}/${target}`
|
|
20
|
+
if (!fs.existsSync(`${file}`)) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
const f = fs.readFileSync(`${file}`, 'utf8')
|
|
24
|
+
return {
|
|
25
|
+
status: 200,
|
|
26
|
+
contentType: 'application/json',
|
|
27
|
+
body: JSON.stringify({ content: f, filename: target }),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'http'
|
|
2
|
+
import { styleText } from 'node:util'
|
|
3
|
+
|
|
4
|
+
const render = (await import('../../lib/render.js')).default
|
|
5
|
+
const makePageData = (await import('../../lib/pageData.js')).default
|
|
6
|
+
|
|
7
|
+
export const path = '/preview'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {IncomingMessage} req
|
|
11
|
+
* @param {ServerResponse} res
|
|
12
|
+
*/
|
|
13
|
+
export const post = async (req, res) => {
|
|
14
|
+
const chunks = []
|
|
15
|
+
req
|
|
16
|
+
.on('data', (chunk) => chunks.push(chunk))
|
|
17
|
+
.on('end', async () => {
|
|
18
|
+
const json = JSON.parse(chunks.join())
|
|
19
|
+
const filename = json.inputFileName ? json.inputFileName : json.selectDataFile
|
|
20
|
+
const pageData = makePageData(filename, json.content)
|
|
21
|
+
const rendered = await render('default.html', pageData)
|
|
22
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
23
|
+
res.end(JSON.stringify({
|
|
24
|
+
'preview': rendered
|
|
25
|
+
}))
|
|
26
|
+
})
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
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>{{SITE_NAME}}</title>
|
|
7
|
+
{include('template/css.html')}
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
background: #fafafa;
|
|
11
|
+
}
|
|
12
|
+
main {
|
|
13
|
+
width: 90%;
|
|
14
|
+
padding: 15px;
|
|
15
|
+
height: 100%;
|
|
16
|
+
margin: 0 auto;
|
|
17
|
+
}
|
|
18
|
+
.textareaAndPreview {
|
|
19
|
+
display: flex;
|
|
20
|
+
margin: 10px 0;
|
|
21
|
+
border: 1px solid #666;
|
|
22
|
+
border-radius: 5px;
|
|
23
|
+
}
|
|
24
|
+
.textareaAndPreview>* {
|
|
25
|
+
flex-basis: 50%;
|
|
26
|
+
height: 80vh;
|
|
27
|
+
}
|
|
28
|
+
.editor {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
padding: 15px 0;
|
|
32
|
+
justify-content:center;
|
|
33
|
+
}
|
|
34
|
+
#editorTextArea {
|
|
35
|
+
resize: none;
|
|
36
|
+
background: #cccccc;
|
|
37
|
+
color: #333;
|
|
38
|
+
padding: 5px;
|
|
39
|
+
border-radius: 5px 0 0 5px;
|
|
40
|
+
border: 0;
|
|
41
|
+
}
|
|
42
|
+
form select {
|
|
43
|
+
padding: 5px;
|
|
44
|
+
margin-top: 5px;
|
|
45
|
+
border-radius: 5px;
|
|
46
|
+
}
|
|
47
|
+
#previewContent {
|
|
48
|
+
flex-basis: 100%;
|
|
49
|
+
height: 100%;
|
|
50
|
+
min-height: 90%;
|
|
51
|
+
display: block;
|
|
52
|
+
width: 100%;
|
|
53
|
+
}
|
|
54
|
+
#previewContent iframe {
|
|
55
|
+
width: 100%;
|
|
56
|
+
height: 100%;
|
|
57
|
+
border: none;
|
|
58
|
+
border-radius: 0 5px 5px 0;
|
|
59
|
+
}
|
|
60
|
+
@media screen and (max-width: 600px) {
|
|
61
|
+
main {
|
|
62
|
+
width: 100%;
|
|
63
|
+
display: block;
|
|
64
|
+
}
|
|
65
|
+
.editor {
|
|
66
|
+
padding: 15px 0;
|
|
67
|
+
}
|
|
68
|
+
.preview {
|
|
69
|
+
display: none;
|
|
70
|
+
}
|
|
71
|
+
#editorTextArea {
|
|
72
|
+
flex-basis: 100%;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
76
|
+
|
|
77
|
+
<link href="https://cdn.jsdelivr.net/npm/prismjs@v1.x/themes/prism.css" rel="stylesheet" />
|
|
78
|
+
|
|
79
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@v1.x/components/prism-core.min.js"></script>
|
|
80
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@v1.x/plugins/autoloader/prism-autoloader.min.js"></script>
|
|
81
|
+
</head>
|
|
82
|
+
<body>
|
|
83
|
+
<script>
|
|
84
|
+
const sleep = waitTime => new Promise( resolve => setTimeout(resolve, waitTime) );
|
|
85
|
+
|
|
86
|
+
const fetchData = (target) => {
|
|
87
|
+
return fetch(`/get_editor_target?md=${target}`)
|
|
88
|
+
.then(async res => {
|
|
89
|
+
const json = await res.json()
|
|
90
|
+
return json
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
const onloadFunction = async (e) => {
|
|
94
|
+
const form = document.querySelector('#editor')
|
|
95
|
+
const textarea = form.querySelector('#editorTextArea')
|
|
96
|
+
const select = form.querySelector('#selectDataFile')
|
|
97
|
+
const inputFileName = form.querySelector('#inputFileName')
|
|
98
|
+
const preview = document.querySelector('#previewContent')
|
|
99
|
+
const url = new URL(location)
|
|
100
|
+
const target = url.searchParams.get('md')
|
|
101
|
+
if (target) {
|
|
102
|
+
fetchData(target).then(json => {
|
|
103
|
+
textarea.value = json.content
|
|
104
|
+
select.value = json.filename
|
|
105
|
+
inputFileName.value = json.filename
|
|
106
|
+
inputFileName.setAttribute('disabled', true)
|
|
107
|
+
submit('/preview', form)
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
select.addEventListener('change', async (event) => {
|
|
111
|
+
if (select.value) {
|
|
112
|
+
const json = await fetchData(select.value)
|
|
113
|
+
textarea.value = json.content
|
|
114
|
+
inputFileName.value = json.filename
|
|
115
|
+
inputFileName.setAttribute('disabled', true)
|
|
116
|
+
url.searchParams.set('md', select.value)
|
|
117
|
+
} else {
|
|
118
|
+
inputFileName.value = ""
|
|
119
|
+
inputFileName.removeAttribute('disabled')
|
|
120
|
+
}
|
|
121
|
+
history.pushState({}, "", url)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const submit = (fetchUrl, form) => {
|
|
125
|
+
const formData = new FormData(form)
|
|
126
|
+
const obj = {}
|
|
127
|
+
formData.forEach((v, k) => {
|
|
128
|
+
obj[k] = v
|
|
129
|
+
})
|
|
130
|
+
fetch(fetchUrl, {
|
|
131
|
+
method: 'post',
|
|
132
|
+
body: JSON.stringify(obj)
|
|
133
|
+
}).then(async response => {
|
|
134
|
+
const json = await response.json()
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
alert(json.message)
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
if (json.href) {
|
|
140
|
+
await sleep(300)
|
|
141
|
+
location.href = json.href
|
|
142
|
+
}
|
|
143
|
+
if (json.preview) {
|
|
144
|
+
const iframe = document.createElement('iframe')
|
|
145
|
+
iframe.setAttribute('srcdoc', json.preview)
|
|
146
|
+
const old = preview.querySelector('iframe')
|
|
147
|
+
if (!old) {
|
|
148
|
+
preview.appendChild(iframe)
|
|
149
|
+
}
|
|
150
|
+
old.setAttribute('srcdoc', json.preview)
|
|
151
|
+
}
|
|
152
|
+
}).catch(e => {
|
|
153
|
+
console.log(e.message)
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
form.addEventListener('submit', (event) => {
|
|
157
|
+
event.preventDefault()
|
|
158
|
+
const fetchUrl = event.submitter.dataset.url
|
|
159
|
+
submit(fetchUrl, event.target)
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
document.addEventListener('DOMContentLoaded', (event) => {
|
|
164
|
+
onloadFunction(event)
|
|
165
|
+
})
|
|
166
|
+
</script>
|
|
167
|
+
<main>
|
|
168
|
+
<form action="/editor" class="editor" method="post" id="editor">
|
|
169
|
+
<input id="inputFileName" name="inputFileName" type="text" value="" placeholder="sample.md">
|
|
170
|
+
<select id="selectDataFile" name="selectDataFile">
|
|
171
|
+
<option value="">新規作成</option>
|
|
172
|
+
<script type="ssg">
|
|
173
|
+
return helper.readIndex().map(p => {
|
|
174
|
+
return `<option value="${p.name}.${p.__filetype}">${p.url}.${p.__filetype}</option>`
|
|
175
|
+
}).join("\n")
|
|
176
|
+
</script>
|
|
177
|
+
</select>
|
|
178
|
+
<div class="textareaAndPreview">
|
|
179
|
+
<textarea id="editorTextArea" name="content" cols="30" rows="10">
|
|
180
|
+
# H1
|
|
181
|
+
|
|
182
|
+
ここにマークダウンを入力してください。
|
|
183
|
+
</textarea>
|
|
184
|
+
<div class="preview">
|
|
185
|
+
<div id="previewContent"></div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
<input type="hidden" name="token" value="{{TOKEN}}">
|
|
189
|
+
<div>
|
|
190
|
+
<input type="submit" value="preview" data-url="/preview">
|
|
191
|
+
<input type="submit" value="編集" data-url="/editor">
|
|
192
|
+
<a href="/">戻る</a>
|
|
193
|
+
</div>
|
|
194
|
+
</form>
|
|
195
|
+
</main>
|
|
196
|
+
</body>
|
|
197
|
+
</html>
|