@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.
@@ -1,8 +1,8 @@
1
1
  "use strict"
2
- import fs from "node:fs/promises";
3
2
  import applyCss from './cssGenerator.js'
4
- import { includeFilter } from './filter.js'
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 fs.readFile(`${templateDir}/${name}`, 'utf8')
15
- templateContent = await includeFilter(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 default applyTemplate
34
+ export { applyTemplate, warmUpTemplate }
@@ -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 as distRoot, cssDir } from './dir.js'
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 += await fs.readFile(`${cssDir}/${cssFile}`)
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(`${distRoot}${path.dirname(dist)}`, { recursive: true }).then(() => {
35
- fs.writeFile(`${distRoot}${dist}`, css)
36
- console.log(styleText('green', '[generate]'), `${src} => ${distRoot}${dist}`)
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 distribute = async (data, deleted, srcDir, distDir) => {
10
- if (deleted) {
11
- for (const obj of deleted) {
12
- console.log(styleText('red', '[unlink]'), `${distDir}${obj.__output}`)
13
- fs.unlink(`${distDir}${obj.__output}`)
14
- }
15
- delete data['__deleted']
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
- const template = data[name].template
19
- const rendered = await render(template, data[name])
20
- let writeTo = `${distDir}${data[name].__output}`
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, deleted } from './indexer.js'
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
- const start = performance.now()
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
- await distribute(allData, deleted, srcDir, distDir)
12
- const end = performance.now()
13
- console.log(styleText('blue', '[build: ' + (end - start) + "ms]"))
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
@@ -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 includeFilter = async (text) => {
7
+ const includeRegexp = new RegExp(/\{\s*include\('(template|css)\/([\w\./]+)'\)\s*\}/g)
8
+
9
+ const includeFilter = (text) => {
8
10
  let replaced = text
9
- const includeRegexp = new RegExp(/\{\s*include\('(template|css)\/([\w\./]+)'\)\s*\}/g)
10
- const include = [...text.matchAll(includeRegexp)].map(matched => [matched[0], matched[1], matched[2]])
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 [toBeReplace, type, filename] = [...include[index]]
18
+ const {toBeReplace, type, filename} = include[index]
13
19
  let content
14
20
  const cacheKey = `${type}/${filename}`
15
21
  if (!alreadyLoaded[cacheKey]) {
16
- switch (type) {
17
- case 'template':
18
- content = await fs.readFile(`${templateDir}/${filename}`, 'utf8')
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 = await includeFilter(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 { writeFile, readFile } from "node:fs/promises";
3
- import { readdirSync, existsSync, mkdirSync } from "node:fs";
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
- const collect = (dir, files = {}, namePrefix = '') => {
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}`, files, namePrefix + 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 pageData = makePageData(`${namePrefix}${dirent.name}`)
21
- allData[pageData.name] = pageData
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
- deleted = oldIndex.filter(oi => !newIndex.map(ni => ni.__output).includes(oi.__output))
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, deleted }
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 load = (path) => {
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 = await includeFilter(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}`)) {
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenjuu99/blog",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "blog template",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -7,6 +7,7 @@
7
7
  }
8
8
 
9
9
  body {
10
+ background: #fff;
10
11
  display: flex;
11
12
  flex-direction: column;
12
13
  min-height: 100dvh !important;
@@ -0,0 +1,3 @@
1
+ ---
2
+ template: editor.html
3
+ ---
@@ -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,7 @@
1
+ ---
2
+ title: 投稿一覧
3
+ ---
4
+ <script type="ssg">
5
+ variables.postPages = helper.readIndex().filter(v => v.url.indexOf('/post/') === 0)
6
+ </script>
7
+ {{ renderIndex(postPages) }}
@@ -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
+ }
@@ -43,6 +43,11 @@
43
43
  {/if}
44
44
  {{MARKDOWN}}
45
45
  </article>
46
+ {if editor}
47
+ <div class="container">
48
+ <a href="/editor?md={{name}}.{{__filetype}}">編集する</a>
49
+ </div>
50
+ {/if}
46
51
 
47
52
  {include('template/prevNext.html')}
48
53
  </main>
@@ -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>