@tenjuu99/blog 0.3.6 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/dev-server +3 -1
- package/bin/server +8 -1
- package/docs/develop.md +30 -0
- package/docs/dictionary.json +1806 -0
- package/lib/dir.js +2 -2
- package/lib/distribute.js +5 -5
- package/lib/files.js +1 -1
- package/lib/filter.js +5 -5
- package/lib/generate.js +3 -3
- package/lib/helper.js +2 -2
- package/lib/imageDistributor.js +3 -3
- package/lib/indexer.js +2 -2
- package/lib/pageData.js +5 -5
- package/lib/render.js +4 -4
- package/lib/replaceVariablesFilter.js +4 -4
- package/lib/server/helper/parseRequestBody.js +46 -0
- package/lib/server.js +7 -3
- package/package.json +3 -2
- package/packages/category/helper/category.js +10 -10
- package/packages/category/helper/categoryIndexer.js +8 -8
- package/packages/category/helper/pagination.js +3 -3
- package/packages/editor/css/editor.css +87 -18
- package/packages/editor/helper/sidebarTree.js +7 -2
- package/packages/editor/js/autoPreviewInitializer.js +11 -0
- package/packages/editor/js/autoSaveInitializer.js +15 -0
- package/packages/editor/js/debouncer.js +21 -0
- package/packages/editor/js/editor.js +261 -91
- package/packages/editor/js/frontmatter_template.js +3 -3
- package/packages/editor/js/imageReferenceExtractor.js +19 -0
- package/packages/editor/js/image_upload.js +1 -1
- package/packages/editor/js/tree.js +27 -12
- package/packages/editor/server/changeReflector.js +47 -0
- package/packages/editor/server/createConverter.js +2 -2
- package/packages/editor/server/get_editor_target.js +2 -2
- package/packages/editor/server/get_frontmatter_templates.js +1 -1
- package/packages/editor/server/get_publication_status.js +29 -0
- package/packages/editor/server/get_sidebar.js +51 -0
- package/packages/editor/server/image_upload.js +32 -40
- package/packages/editor/server/preview.js +49 -19
- package/packages/editor/server/publicationStatus.js +27 -0
- package/packages/editor/server/publish.js +135 -0
- package/packages/editor/server/publishTargetCollector.js +19 -0
- package/packages/editor/server/save.js +97 -0
- package/packages/editor/server/sidebarStatusCollector.js +17 -0
- package/packages/editor/template/editor.html +28 -18
- package/src-sample/helper/add.js +1 -1
- package/src-sample/helper/index.js +6 -6
- package/docs/dictionary.md +0 -553
- package/packages/editor/server/editor.js +0 -43
package/lib/dir.js
CHANGED
|
@@ -27,8 +27,8 @@ const packageDirCore = path.dirname(__filename) + '/../packages'
|
|
|
27
27
|
|
|
28
28
|
let alreadyCached = false
|
|
29
29
|
|
|
30
|
-
// @vocab: パッケージ
|
|
31
|
-
// @vocab: ビルド
|
|
30
|
+
// @vocab: パッケージ
|
|
31
|
+
// @vocab: ビルド
|
|
32
32
|
// @test: tests/ssg-core/hooks.test.js
|
|
33
33
|
const cache = () => {
|
|
34
34
|
if (alreadyCached) {
|
package/lib/distribute.js
CHANGED
|
@@ -13,16 +13,16 @@ import { createConverter } from '../packages/editor/server/createConverter.js'
|
|
|
13
13
|
|
|
14
14
|
const indexFile = `${cacheDir}/index.json`
|
|
15
15
|
|
|
16
|
-
// @vocab: ページ
|
|
17
|
-
// @vocab: テンプレート
|
|
16
|
+
// @vocab: ページ
|
|
17
|
+
// @vocab: テンプレート
|
|
18
18
|
const renderPage = async (page) => {
|
|
19
19
|
const template = page.template
|
|
20
20
|
return [page.name, await render(template, page)]
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// @vocab: ビルド
|
|
24
|
-
// @vocab: ページ
|
|
25
|
-
// @vocab: ページインデックス
|
|
23
|
+
// @vocab: ビルド
|
|
24
|
+
// @vocab: ページ
|
|
25
|
+
// @vocab: ページインデックス
|
|
26
26
|
// @test: tests/ssg-core/hooks.test.js
|
|
27
27
|
const distribute = async (data, srcDir, distDir) => {
|
|
28
28
|
const promises = []
|
package/lib/files.js
CHANGED
package/lib/filter.js
CHANGED
|
@@ -70,8 +70,8 @@ const ifConditionEvaluator = (condition, variables) => {
|
|
|
70
70
|
* @param {object} variables
|
|
71
71
|
* @returns {string}
|
|
72
72
|
*/
|
|
73
|
-
// @vocab: テンプレート
|
|
74
|
-
// @vocab: ヘルパー関数
|
|
73
|
+
// @vocab: テンプレート
|
|
74
|
+
// @vocab: ヘルパー関数
|
|
75
75
|
// @test: tests/ssg-core/filter.test.js
|
|
76
76
|
const replaceIfFilter = (text, variables) => {
|
|
77
77
|
const matched = [...text.matchAll(IF_REGEXP)]
|
|
@@ -90,9 +90,9 @@ const replaceIfFilter = (text, variables) => {
|
|
|
90
90
|
return text
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
// @vocab: SSGスクリプト
|
|
94
|
-
// @vocab: テンプレート
|
|
95
|
-
// @vocab: ヘルパー関数
|
|
93
|
+
// @vocab: SSGスクリプト
|
|
94
|
+
// @vocab: テンプレート
|
|
95
|
+
// @vocab: ヘルパー関数
|
|
96
96
|
// @test: tests/ssg-core/filter.test.js
|
|
97
97
|
const replaceScriptFilter = async (text, variables) => {
|
|
98
98
|
let replaced = text
|
package/lib/generate.js
CHANGED
|
@@ -12,7 +12,7 @@ const beforeGenerate = async () => {
|
|
|
12
12
|
await warmUpTemplate()
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
// @vocab: ビルド
|
|
15
|
+
// @vocab: ビルド
|
|
16
16
|
// @test: tests/ssg-core/hooks.test.js
|
|
17
17
|
export const runHooks = async (hookName, customHelperDir = null, customAllData = null, customConfig = null) => {
|
|
18
18
|
const targetConfig = customConfig || config
|
|
@@ -45,8 +45,8 @@ export const runHooks = async (hookName, customHelperDir = null, customAllData =
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// @vocab: ビルド
|
|
49
|
-
// @vocab: ページインデックス
|
|
48
|
+
// @vocab: ビルド
|
|
49
|
+
// @vocab: ページインデックス
|
|
50
50
|
// @test: tests/ssg-core/hooks.test.js
|
|
51
51
|
const generate = async () => {
|
|
52
52
|
let start = performance.now()
|
package/lib/helper.js
CHANGED
|
@@ -7,8 +7,8 @@ const helper = {}
|
|
|
7
7
|
// top-level await を使うと循環依存がある場合に ESM のデッドロックが発生するため IIFE を使用する。
|
|
8
8
|
// helperReady をエクスポートすることで、ヘルパーのロード完了を外部から await できる。
|
|
9
9
|
// (Promise は一度 resolve すれば以降の await は即座に返るため、複数箇所で await しても問題ない)
|
|
10
|
-
// @vocab: ヘルパー関数
|
|
11
|
-
// @vocab: パッケージ
|
|
10
|
+
// @vocab: ヘルパー関数
|
|
11
|
+
// @vocab: パッケージ
|
|
12
12
|
// @test: tests/ssg-core/helper.test.js
|
|
13
13
|
export const helperReady = (async () => {
|
|
14
14
|
if (config.helper) {
|
package/lib/imageDistributor.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'node:path'
|
|
|
4
4
|
const IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'tiff', 'heic', 'bmp'])
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* @vocab ビルド画像配布器
|
|
7
|
+
* @vocab ビルド画像配布器
|
|
8
8
|
* @test tests/ssg-core/imageDistributor.test.js
|
|
9
9
|
*/
|
|
10
10
|
export async function distributeImages(srcDir, distDir, { fn, ext }) {
|
|
@@ -28,7 +28,7 @@ export async function distributeImages(srcDir, distDir, { fn, ext }) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* @vocab ビルド画像配布器
|
|
31
|
+
* @vocab ビルド画像配布器
|
|
32
32
|
* @test tests/ssg-core/imageDistributor.test.js
|
|
33
33
|
*/
|
|
34
34
|
export async function collectFiles(dir) {
|
|
@@ -48,7 +48,7 @@ export async function collectFiles(dir) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* @vocab ビルド画像配布器
|
|
51
|
+
* @vocab ビルド画像配布器
|
|
52
52
|
* @test tests/ssg-core/imageDistributor.test.js
|
|
53
53
|
*/
|
|
54
54
|
export async function writeConvertedFile(srcPath, distPath, fn) {
|
package/lib/indexer.js
CHANGED
|
@@ -31,8 +31,8 @@ const collect = (dir, namePrefix = '', promises = []) => {
|
|
|
31
31
|
return promises
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// @vocab: ページインデックス
|
|
35
|
-
// @vocab: ページ
|
|
34
|
+
// @vocab: ページインデックス
|
|
35
|
+
// @vocab: ページ
|
|
36
36
|
// @test: tests/ssg-core/indexer.test.js
|
|
37
37
|
const indexing = async () => {
|
|
38
38
|
allData = {}
|
package/lib/pageData.js
CHANGED
|
@@ -5,16 +5,16 @@ import config from './config.js'
|
|
|
5
5
|
const FRONTMATTER_REGEXP = /^(<!|-)--(?<variables>[\s\S]*?)--(-|>)/
|
|
6
6
|
const METADATA_KEY_REGEXP = /^([a-zA-Z\d_-]+):/
|
|
7
7
|
|
|
8
|
-
// @vocab: ページ
|
|
9
|
-
// @vocab: フロントマター
|
|
8
|
+
// @vocab: ページ
|
|
9
|
+
// @vocab: フロントマター
|
|
10
10
|
// @test: tests/ssg-core/pageData.test.js
|
|
11
11
|
const makePageData = (filename, content) => {
|
|
12
12
|
const [name, ext] = filename.split('.')
|
|
13
13
|
return parse(content, name, ext)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
// @vocab: フロントマター
|
|
17
|
-
// @vocab: ページ
|
|
16
|
+
// @vocab: フロントマター
|
|
17
|
+
// @vocab: ページ
|
|
18
18
|
const parse = (content, name, ext) => {
|
|
19
19
|
const matched = content.match(FRONTMATTER_REGEXP)
|
|
20
20
|
const markdownReplaced = content.replace(FRONTMATTER_REGEXP, '')
|
|
@@ -70,7 +70,7 @@ const parse = (content, name, ext) => {
|
|
|
70
70
|
return metaDataMerged
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
// @vocab: フロントマター
|
|
73
|
+
// @vocab: フロントマター
|
|
74
74
|
// @test: tests/ssg-core/pageData.test.js
|
|
75
75
|
const parseMetaData = (data) => {
|
|
76
76
|
const metaData = {}
|
package/lib/render.js
CHANGED
|
@@ -13,10 +13,10 @@ marked.use({
|
|
|
13
13
|
breaks: true
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
// @vocab: テンプレート
|
|
17
|
-
// @vocab: ページ
|
|
18
|
-
// @vocab: SSGスクリプト
|
|
19
|
-
// @vocab: ヘルパー関数
|
|
16
|
+
// @vocab: テンプレート
|
|
17
|
+
// @vocab: ページ
|
|
18
|
+
// @vocab: SSGスクリプト
|
|
19
|
+
// @vocab: ヘルパー関数
|
|
20
20
|
// @test: tests/ssg-core/helper.test.js
|
|
21
21
|
const render = async (templateName, data) => {
|
|
22
22
|
await helperReady
|
|
@@ -11,8 +11,8 @@ const VARIABLE_REGEXP = /(\\)?{{[\s]*([^{}]+)[\s]*}}/g
|
|
|
11
11
|
* @params {object} variables
|
|
12
12
|
* @return {text}
|
|
13
13
|
*/
|
|
14
|
-
// @vocab: テンプレート
|
|
15
|
-
// @vocab: ヘルパー関数
|
|
14
|
+
// @vocab: テンプレート
|
|
15
|
+
// @vocab: ヘルパー関数
|
|
16
16
|
// @test: tests/ssg-core/replaceVariablesFilter.test.js
|
|
17
17
|
const replaceVariablesFilter = (text, variables) => {
|
|
18
18
|
const matched = [...text.matchAll(VARIABLE_REGEXP)]
|
|
@@ -32,8 +32,8 @@ const replaceVariablesFilter = (text, variables) => {
|
|
|
32
32
|
* @param {object} variables
|
|
33
33
|
* @return {Array.<string>}
|
|
34
34
|
*/
|
|
35
|
-
// @vocab: ヘルパー関数
|
|
36
|
-
// @vocab: テンプレート
|
|
35
|
+
// @vocab: ヘルパー関数
|
|
36
|
+
// @vocab: テンプレート
|
|
37
37
|
const replaceVariable = (target, replaceTargetRawText, backslash, variables) => {
|
|
38
38
|
const toBeReplace = replaceTargetRawText
|
|
39
39
|
const toBeReplaceScript = toBeReplace.match(/([\w\d_]+)\((.*)\)/)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP リクエストボディを JSON としてパースする共通ヘルパー。
|
|
3
|
+
* Buffer.concat による安全な連結、サイズ制限、JSON.parse エラーハンドリングを提供する。
|
|
4
|
+
*
|
|
5
|
+
* @param {import('http').IncomingMessage} req
|
|
6
|
+
* @param {{ maxSize?: number }} [options] maxSize: バイト上限(デフォルト: 無制限)
|
|
7
|
+
* @returns {Promise<unknown>} パース済み JSON
|
|
8
|
+
* @throws {{ message: string, code: 'PAYLOAD_TOO_LARGE' | 'INVALID_JSON' }} パース失敗時
|
|
9
|
+
*/
|
|
10
|
+
export function parseJsonBody(req, options = {}) {
|
|
11
|
+
const { maxSize = Infinity } = options
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const chunks = []
|
|
14
|
+
let totalSize = 0
|
|
15
|
+
let aborted = false
|
|
16
|
+
req
|
|
17
|
+
.on('data', chunk => {
|
|
18
|
+
if (aborted) return
|
|
19
|
+
totalSize += chunk.length
|
|
20
|
+
if (totalSize > maxSize) {
|
|
21
|
+
aborted = true
|
|
22
|
+
req.destroy()
|
|
23
|
+
const err = new Error('リクエストサイズが上限を超えています')
|
|
24
|
+
err.code = 'PAYLOAD_TOO_LARGE'
|
|
25
|
+
reject(err)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
chunks.push(chunk)
|
|
29
|
+
})
|
|
30
|
+
.on('end', () => {
|
|
31
|
+
if (aborted) return
|
|
32
|
+
try {
|
|
33
|
+
// setEncoding('utf8') 時は chunks が string[] になるため両ケースに対応する
|
|
34
|
+
const body = Buffer.isBuffer(chunks[0])
|
|
35
|
+
? Buffer.concat(chunks).toString('utf8')
|
|
36
|
+
: chunks.join('')
|
|
37
|
+
resolve(JSON.parse(body))
|
|
38
|
+
} catch {
|
|
39
|
+
const err = new Error('リクエストボディのJSON解析に失敗しました')
|
|
40
|
+
err.code = 'INVALID_JSON'
|
|
41
|
+
reject(err)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
.on('error', reject)
|
|
45
|
+
})
|
|
46
|
+
}
|
package/lib/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http from 'http'
|
|
2
2
|
import url from 'url'
|
|
3
3
|
import fs from 'node:fs'
|
|
4
|
-
import { distDir, serverDir } from './dir.js'
|
|
4
|
+
import { distDir, srcDir, serverDir } from './dir.js'
|
|
5
5
|
import { styleText } from 'node:util'
|
|
6
6
|
import handle from './tryServer.js'
|
|
7
7
|
import contentType from './contentType.js'
|
|
@@ -20,7 +20,11 @@ const server = () => {
|
|
|
20
20
|
}
|
|
21
21
|
path += '.html'
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
let filePath = `${distDir}${path}`
|
|
24
|
+
if (!fs.existsSync(filePath)) {
|
|
25
|
+
filePath = `${srcDir}${path}`
|
|
26
|
+
}
|
|
27
|
+
if (!fs.existsSync(filePath)) {
|
|
24
28
|
console.log(styleText('red', `[${request.method}] 404`), request.url)
|
|
25
29
|
const errorContent = fs.readFileSync(`${distDir}/404.html`)
|
|
26
30
|
response.writeHead(404)
|
|
@@ -28,7 +32,7 @@ const server = () => {
|
|
|
28
32
|
return
|
|
29
33
|
}
|
|
30
34
|
try {
|
|
31
|
-
const content = fs.readFileSync(
|
|
35
|
+
const content = fs.readFileSync(filePath, 'binary')
|
|
32
36
|
|
|
33
37
|
const ext = path.split('.')[1]
|
|
34
38
|
console.log(styleText('green', `[${request.method}] 200`), request.url)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenjuu99/blog",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "blog template",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
14
|
"generate": "bin/generate",
|
|
15
|
-
"server": "bin/
|
|
15
|
+
"server": "bin/server",
|
|
16
|
+
"dev-server": "bin/dev-server",
|
|
16
17
|
"create-blog": "bin/new"
|
|
17
18
|
},
|
|
18
19
|
"author": "AmashigeSeiji",
|
|
@@ -8,10 +8,10 @@ let categoryTreeCache = null
|
|
|
8
8
|
* @param {Object} config - 設定オブジェクト
|
|
9
9
|
* @returns {Object} カテゴリーツリー
|
|
10
10
|
*/
|
|
11
|
-
// @vocab: カテゴリーシステム
|
|
12
|
-
// @vocab: カテゴリー
|
|
13
|
-
// @vocab: カテゴリーパス
|
|
14
|
-
// @vocab: サブカテゴリー
|
|
11
|
+
// @vocab: カテゴリーシステム
|
|
12
|
+
// @vocab: カテゴリー
|
|
13
|
+
// @vocab: カテゴリーパス
|
|
14
|
+
// @vocab: サブカテゴリー
|
|
15
15
|
// @test: tests/category/category-tree.test.js
|
|
16
16
|
export function buildCategoryTree(data = allData, conf = config) {
|
|
17
17
|
const tree = {}
|
|
@@ -79,9 +79,9 @@ export function getCategoryTree() {
|
|
|
79
79
|
* @param {Array} categoryPath - カテゴリーパス(例: ["Art", "Painting"])
|
|
80
80
|
* @returns {Array} ページデータの配列
|
|
81
81
|
*/
|
|
82
|
-
// @vocab: カテゴリーパス
|
|
83
|
-
// @vocab: カテゴリー
|
|
84
|
-
// @vocab: ページ
|
|
82
|
+
// @vocab: カテゴリーパス
|
|
83
|
+
// @vocab: カテゴリー
|
|
84
|
+
// @vocab: ページ
|
|
85
85
|
// @test: tests/category/category-helper.test.js
|
|
86
86
|
export function getCategoryPages(categoryPath) {
|
|
87
87
|
const pages = []
|
|
@@ -104,9 +104,9 @@ export function getCategoryPages(categoryPath) {
|
|
|
104
104
|
* @param {Array} categoryPath - カテゴリーパス(例: ["Art"])
|
|
105
105
|
* @returns {Array} ページデータの配列
|
|
106
106
|
*/
|
|
107
|
-
// @vocab: サブカテゴリー
|
|
108
|
-
// @vocab: カテゴリーパス
|
|
109
|
-
// @vocab: ページ
|
|
107
|
+
// @vocab: サブカテゴリー
|
|
108
|
+
// @vocab: カテゴリーパス
|
|
109
|
+
// @vocab: ページ
|
|
110
110
|
// @test: tests/category/category-helper.test.js
|
|
111
111
|
export function getCategoryPagesRecursive(categoryPath) {
|
|
112
112
|
const pages = []
|
|
@@ -4,7 +4,7 @@ import { buildCategoryTree } from './category.js'
|
|
|
4
4
|
* 記事名配列を perPage 件ずつのページ配列に分割する
|
|
5
5
|
* 空の場合は [[]] を返す(ページ数ゼロを避けるため)
|
|
6
6
|
*/
|
|
7
|
-
// @vocab: ページネーション
|
|
7
|
+
// @vocab: ページネーション
|
|
8
8
|
// @test: tests/category/category-pagination.test.js
|
|
9
9
|
function sliceIntoPages(items, perPage) {
|
|
10
10
|
if (items.length === 0) return [[]]
|
|
@@ -39,8 +39,8 @@ function sliceIntoPages(items, perPage) {
|
|
|
39
39
|
* @param {Object} categoryConfig - 単一カテゴリー設定オブジェクト
|
|
40
40
|
* @param {Object} config - グローバル設定オブジェクト
|
|
41
41
|
*/
|
|
42
|
-
// @vocab: カテゴリーページ
|
|
43
|
-
// @vocab: ページネーション
|
|
42
|
+
// @vocab: カテゴリーページ
|
|
43
|
+
// @vocab: ページネーション
|
|
44
44
|
// @test: tests/category/category-pagination.test.js
|
|
45
45
|
function buildVirtualPage(url, categoryData, children, currentPage, totalPages, categoryConfig, config) {
|
|
46
46
|
const pageName = url.replace(/^\//, '') + '/index'
|
|
@@ -87,9 +87,9 @@ function buildVirtualPage(url, categoryData, children, currentPage, totalPages,
|
|
|
87
87
|
* @param {Object} categoryConfig - 単一カテゴリー設定オブジェクト
|
|
88
88
|
* @param {Object} config - グローバル設定オブジェクト
|
|
89
89
|
*/
|
|
90
|
-
// @vocab: カテゴリーページ
|
|
91
|
-
// @vocab: カテゴリーシステム
|
|
92
|
-
// @vocab: ページネーション
|
|
90
|
+
// @vocab: カテゴリーページ
|
|
91
|
+
// @vocab: カテゴリーシステム
|
|
92
|
+
// @vocab: ページネーション
|
|
93
93
|
// @test: tests/category/category-pagination.test.js
|
|
94
94
|
function generateCategoryPages(allData, categoryConfig, config) {
|
|
95
95
|
const pathFilter = categoryConfig.path_filter || ''
|
|
@@ -149,8 +149,8 @@ function generateCategoryPages(allData, categoryConfig, config) {
|
|
|
149
149
|
* @param {Object} allData - 全ページデータ
|
|
150
150
|
* @param {Object} config - 設定オブジェクト
|
|
151
151
|
*/
|
|
152
|
-
// @vocab: カテゴリーシステム
|
|
153
|
-
// @vocab: ビルド
|
|
152
|
+
// @vocab: カテゴリーシステム
|
|
153
|
+
// @vocab: ビルド
|
|
154
154
|
// @test: tests/category/category-indexer.test.js
|
|
155
155
|
export async function afterIndexing(allData, config) {
|
|
156
156
|
const categorySystems = config.categories
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* getPaginationUrl: ページ番号から URL を生成する
|
|
3
3
|
* page === 1 なら basePath、それ以外なら basePath + page + '/'
|
|
4
4
|
*/
|
|
5
|
-
// @vocab: ページネーション
|
|
6
|
-
// @vocab: カテゴリーページ
|
|
5
|
+
// @vocab: ページネーション
|
|
6
|
+
// @vocab: カテゴリーページ
|
|
7
7
|
// @test: tests/category/category-pagination.test.js
|
|
8
8
|
export function getPaginationUrl(basePath, page) {
|
|
9
9
|
const base = basePath.endsWith('/') ? basePath : `${basePath}/`
|
|
@@ -15,7 +15,7 @@ export function getPaginationUrl(basePath, page) {
|
|
|
15
15
|
* buildWindowedPages: ウィンドウ付きページ番号リストを生成する
|
|
16
16
|
* 各要素は {num, isCurrent} または {num: null, isEllipsis: true}
|
|
17
17
|
*/
|
|
18
|
-
// @vocab: ページネーション
|
|
18
|
+
// @vocab: ページネーション
|
|
19
19
|
// @test: tests/category/category-pagination.test.js
|
|
20
20
|
export function buildWindowedPages(totalPages, currentPage, windowSize = 2) {
|
|
21
21
|
if (totalPages <= 0) return []
|
|
@@ -3,6 +3,8 @@ body {
|
|
|
3
3
|
height: 100vh;
|
|
4
4
|
display: flex;
|
|
5
5
|
flex-direction: column;
|
|
6
|
+
font-family: Helvetica, "Noto Sans JP", "Hiragino Sans", system-ui, sans-serif;
|
|
7
|
+
font-weight: 300;
|
|
6
8
|
}
|
|
7
9
|
header {
|
|
8
10
|
text-align: center;
|
|
@@ -49,6 +51,42 @@ main.container {
|
|
|
49
51
|
.editor-options {
|
|
50
52
|
display: flex;
|
|
51
53
|
justify-content: space-between;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 8px;
|
|
56
|
+
margin-bottom: 5px;
|
|
57
|
+
}
|
|
58
|
+
.editor-options-left {
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: 8px;
|
|
62
|
+
}
|
|
63
|
+
.editor-options-right {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 8px;
|
|
67
|
+
}
|
|
68
|
+
#newFileDialog {
|
|
69
|
+
border: 1px solid #ccc;
|
|
70
|
+
border-radius: 8px;
|
|
71
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
72
|
+
padding: 24px;
|
|
73
|
+
min-width: 320px;
|
|
74
|
+
}
|
|
75
|
+
#newFileDialog form {
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-direction: column;
|
|
78
|
+
gap: 12px;
|
|
79
|
+
}
|
|
80
|
+
#newFileDialog label {
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-direction: column;
|
|
83
|
+
gap: 4px;
|
|
84
|
+
}
|
|
85
|
+
#newFileDialog #newFileError {
|
|
86
|
+
color: #c00;
|
|
87
|
+
font-size: 0.9em;
|
|
88
|
+
margin: 0;
|
|
89
|
+
min-height: 1.2em;
|
|
52
90
|
}
|
|
53
91
|
#editorTextArea {
|
|
54
92
|
resize: none;
|
|
@@ -81,20 +119,27 @@ form select {
|
|
|
81
119
|
border: none;
|
|
82
120
|
border-radius: 0 5px 5px 0;
|
|
83
121
|
}
|
|
122
|
+
|
|
84
123
|
.sidebar {
|
|
124
|
+
--sidebar-bg: #313d64;
|
|
125
|
+
--sidebar-dir: #79afb4;
|
|
126
|
+
--sidebar-file: #cbcbcb;
|
|
127
|
+
--sidebar-active-file-fg: #333;
|
|
128
|
+
--sidebar-active-file-bg: rgba(165,218,246,0.8);
|
|
129
|
+
--sidebar-modified-fg: #ff7777;
|
|
130
|
+
--sidebar-hover-bg: rgba(104, 117, 154, .3);
|
|
85
131
|
display: flex;
|
|
86
132
|
justify-content: space-between;
|
|
87
133
|
width: 300px;
|
|
88
|
-
overflow:
|
|
134
|
+
overflow: scroll;
|
|
89
135
|
left: 0;
|
|
90
|
-
background:
|
|
136
|
+
background: var(--sidebar-bg);
|
|
91
137
|
height: 100%;
|
|
92
138
|
padding: 0;
|
|
93
|
-
overflow-y: auto;
|
|
94
139
|
flex-basis: 300px;
|
|
95
140
|
position: relative;
|
|
96
141
|
z-index: 3;
|
|
97
|
-
|
|
142
|
+
font-size: 0.85em;
|
|
98
143
|
}
|
|
99
144
|
.sidebar-close .sidebar {
|
|
100
145
|
left: -290px;
|
|
@@ -104,6 +149,9 @@ form select {
|
|
|
104
149
|
padding: 0;
|
|
105
150
|
margin: 0;
|
|
106
151
|
}
|
|
152
|
+
.sidebar > ul {
|
|
153
|
+
padding: 0 0 0 15px;
|
|
154
|
+
}
|
|
107
155
|
.sidebar-close .sidebar ul {
|
|
108
156
|
position: absolute;
|
|
109
157
|
left: -290px;
|
|
@@ -111,45 +159,66 @@ form select {
|
|
|
111
159
|
}
|
|
112
160
|
.sidebar li {
|
|
113
161
|
list-style: none;
|
|
114
|
-
padding-left:
|
|
162
|
+
padding-left: 10px;
|
|
163
|
+
white-space: nowrap;
|
|
164
|
+
text-overflow: ellipsis;
|
|
165
|
+
}
|
|
166
|
+
.sidebar li:has(a.active):not(:has(li)) {
|
|
167
|
+
background: var(--sidebar-active-file-bg);
|
|
115
168
|
}
|
|
116
169
|
.sidebar a {
|
|
117
|
-
color:
|
|
170
|
+
color: var(--sidebar-file);
|
|
118
171
|
text-decoration: none;
|
|
119
172
|
}
|
|
120
173
|
.sidebar a.active {
|
|
121
|
-
|
|
122
|
-
color: #333;
|
|
123
|
-
background: rgba(104, 117, 154, .2);
|
|
174
|
+
color: var(--sidebar-active-file-fg);
|
|
124
175
|
display: block;
|
|
125
176
|
}
|
|
126
|
-
.sidebar
|
|
127
|
-
|
|
177
|
+
.sidebar a[data-status="new"]::after {
|
|
178
|
+
content: " ●";
|
|
179
|
+
color: #e06c00;
|
|
180
|
+
}
|
|
181
|
+
.sidebar a[data-status="modified"]::after {
|
|
182
|
+
content: " ◆";
|
|
183
|
+
color: #0070c0;
|
|
184
|
+
}
|
|
185
|
+
.sidebar a[data-status="published"]::after {
|
|
186
|
+
content: " ✓";
|
|
187
|
+
color: #888;
|
|
188
|
+
}
|
|
189
|
+
.sidebar a[data-status="new"],
|
|
190
|
+
.sidebar a[data-status="modified"] {
|
|
191
|
+
color: var(--sidebar-modified-fg);
|
|
128
192
|
}
|
|
193
|
+
.sidebar li:not(:has(li)):hover,
|
|
194
|
+
.sidebar li.dir-node summary:hover {
|
|
195
|
+
background: var(--sidebar-hover-bg);
|
|
196
|
+
}
|
|
197
|
+
|
|
129
198
|
.sidebar details > summary {
|
|
130
199
|
cursor: pointer;
|
|
131
200
|
padding: 2px 0;
|
|
132
|
-
color:
|
|
133
|
-
font-size: 0.9em;
|
|
201
|
+
color: var(--sidebar-dir);
|
|
134
202
|
list-style: disclosure-closed;
|
|
135
203
|
}
|
|
204
|
+
.sidebar li.dir-node details:not([open]):has(a[data-status="new"]) summary,
|
|
205
|
+
.sidebar li.dir-node details:not([open]):has(a[data-status="modified"]) summary {
|
|
206
|
+
color: var(--sidebar-modified-fg);
|
|
207
|
+
}
|
|
136
208
|
.sidebar details[open] > summary {
|
|
137
209
|
list-style: disclosure-open;
|
|
138
210
|
}
|
|
139
|
-
.sidebar details > ul {
|
|
140
|
-
padding-left: 8px;
|
|
141
|
-
}
|
|
142
211
|
.sidebar-toggle {
|
|
143
212
|
cursor: pointer;
|
|
144
213
|
width: 20px;
|
|
145
214
|
height: 100%;
|
|
146
215
|
position: absolute;
|
|
147
|
-
|
|
216
|
+
left: 0;
|
|
148
217
|
}
|
|
149
218
|
.sidebar-close .sidebar-toggle {
|
|
150
219
|
position: fixed;
|
|
151
220
|
left: 0;
|
|
152
|
-
background:
|
|
221
|
+
background: var(--sidebar-bg);
|
|
153
222
|
}
|
|
154
223
|
.sidebar-toggle:hover:after {
|
|
155
224
|
position: absolute;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { buildTree, renderTreeHtml } from '../js/tree.js'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* @param {Array<{name: string, __filetype: string, __is_auto_category: boolean}>} files
|
|
5
|
+
* @param {Object.<string, 'new'|'modified'|'published'|'unknown'>} [statusMap]
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
export function renderSidebarTree(files, statusMap = {}) {
|
|
4
9
|
const tree = buildTree(files.filter(f => !f.__is_auto_category))
|
|
5
|
-
return renderTreeHtml(tree)
|
|
10
|
+
return renderTreeHtml(tree, '', statusMap)
|
|
6
11
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vocab プレビュー自動更新器
|
|
3
|
+
* @test tests/editor/auto-preview.test.js
|
|
4
|
+
*/
|
|
5
|
+
import { createDebounce } from './debouncer.js'
|
|
6
|
+
|
|
7
|
+
export function initAutoPreview(textarea, onUpdate, delay = 500) {
|
|
8
|
+
const debouncedUpdate = createDebounce(onUpdate, delay)
|
|
9
|
+
textarea.addEventListener('input', debouncedUpdate)
|
|
10
|
+
return debouncedUpdate
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createDebounce } from './debouncer.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @vocab 自動保存初期化器
|
|
5
|
+
* @test tests/editor/editor-ui-cleanup.test.js
|
|
6
|
+
* @param {EventTarget} textarea
|
|
7
|
+
* @param {() => void} onSave
|
|
8
|
+
* @param {number} delay
|
|
9
|
+
* @returns {import('./debouncer.js').Debounce}
|
|
10
|
+
*/
|
|
11
|
+
export function initAutoSave(textarea, onSave, delay = 500) {
|
|
12
|
+
const debouncedSave = createDebounce(onSave, delay)
|
|
13
|
+
textarea.addEventListener('input', debouncedSave)
|
|
14
|
+
return debouncedSave
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vocab デバウンサー
|
|
3
|
+
* @test tests/editor/auto-preview.test.js
|
|
4
|
+
*/
|
|
5
|
+
export function createDebounce(fn, delay) {
|
|
6
|
+
let timer = null
|
|
7
|
+
function debounced(...args) {
|
|
8
|
+
if (timer) clearTimeout(timer)
|
|
9
|
+
timer = setTimeout(() => {
|
|
10
|
+
timer = null
|
|
11
|
+
fn(...args)
|
|
12
|
+
}, delay)
|
|
13
|
+
}
|
|
14
|
+
debounced.cancel = () => {
|
|
15
|
+
if (timer) {
|
|
16
|
+
clearTimeout(timer)
|
|
17
|
+
timer = null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return debounced
|
|
21
|
+
}
|