@tenjuu99/blog 0.3.4 → 0.3.6
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/docs/develop.md +6 -6
- package/docs/dictionary.md +553 -0
- package/docs/spec.md +34 -2
- package/lib/dir.js +3 -0
- package/lib/distribute.js +19 -5
- package/lib/files.js +2 -0
- package/lib/filter.js +7 -0
- package/lib/generate.js +5 -0
- package/lib/helper.js +3 -0
- package/lib/imageDistributor.js +59 -0
- package/lib/indexer.js +3 -0
- package/lib/pageData.js +12 -1
- package/lib/render.js +5 -0
- package/lib/replaceVariablesFilter.js +5 -0
- package/lib/tryServer.js +3 -1
- package/package.json +8 -5
- package/packages/breadcrumbs/helper/breadcrumbs.js +3 -0
- package/packages/category/helper/category.js +13 -0
- package/packages/category/helper/categoryIndexer.js +15 -3
- package/packages/category/helper/pagination.js +5 -0
- package/packages/editor/css/editor.css +20 -1
- package/packages/editor/helper/sidebarTree.js +6 -0
- package/packages/editor/js/editor.js +163 -8
- package/packages/editor/js/frontmatter_template.js +67 -0
- package/packages/editor/js/image_upload.js +12 -0
- package/packages/editor/js/tree.js +66 -0
- package/packages/editor/server/converters/sharp.js +14 -0
- package/packages/editor/server/createConverter.js +26 -0
- package/packages/editor/server/get_frontmatter_templates.js +22 -0
- package/packages/editor/server/image_upload.js +130 -0
- package/packages/editor/template/editor.html +3 -7
- package/src-sample/converters/webp.js +20 -0
- package/src-sample/helper/add.js +2 -0
- package/src-sample/helper/index.js +9 -0
- package/src-sample/image/test.jpg +0 -0
package/lib/dir.js
CHANGED
|
@@ -27,6 +27,9 @@ const packageDirCore = path.dirname(__filename) + '/../packages'
|
|
|
27
27
|
|
|
28
28
|
let alreadyCached = false
|
|
29
29
|
|
|
30
|
+
// @vocab: パッケージ (docs/dictionary.md#パッケージ)
|
|
31
|
+
// @vocab: ビルド (docs/dictionary.md#ビルド)
|
|
32
|
+
// @test: tests/ssg-core/hooks.test.js
|
|
30
33
|
const cache = () => {
|
|
31
34
|
if (alreadyCached) {
|
|
32
35
|
return
|
package/lib/distribute.js
CHANGED
|
@@ -8,14 +8,22 @@ import { styleText } from 'node:util'
|
|
|
8
8
|
import config from './config.js'
|
|
9
9
|
import { cacheDir } from './dir.js'
|
|
10
10
|
import { applyTemplate } from './applyTemplate.js'
|
|
11
|
+
import { distributeImages } from './imageDistributor.js'
|
|
12
|
+
import { createConverter } from '../packages/editor/server/createConverter.js'
|
|
11
13
|
|
|
12
14
|
const indexFile = `${cacheDir}/index.json`
|
|
13
15
|
|
|
16
|
+
// @vocab: ページ (docs/dictionary.md#ページ)
|
|
17
|
+
// @vocab: テンプレート (docs/dictionary.md#テンプレート)
|
|
14
18
|
const renderPage = async (page) => {
|
|
15
19
|
const template = page.template
|
|
16
20
|
return [page.name, await render(template, page)]
|
|
17
21
|
}
|
|
18
22
|
|
|
23
|
+
// @vocab: ビルド (docs/dictionary.md#ビルド)
|
|
24
|
+
// @vocab: ページ (docs/dictionary.md#ページ)
|
|
25
|
+
// @vocab: ページインデックス (docs/dictionary.md#ページインデックス)
|
|
26
|
+
// @test: tests/ssg-core/hooks.test.js
|
|
19
27
|
const distribute = async (data, srcDir, distDir) => {
|
|
20
28
|
const promises = []
|
|
21
29
|
const newIndex = []
|
|
@@ -36,13 +44,19 @@ const distribute = async (data, srcDir, distDir) => {
|
|
|
36
44
|
})
|
|
37
45
|
}
|
|
38
46
|
const distributeRaw = config.distribute_raw.split(',')
|
|
39
|
-
|
|
47
|
+
const imageConverter = config.image_converter ? await createConverter(config.image_converter) : null
|
|
48
|
+
for (const copyDir of distributeRaw) {
|
|
40
49
|
if (!existsSync(`${cacheDir}/${copyDir}`)) {
|
|
41
|
-
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
if (imageConverter) {
|
|
53
|
+
await distributeImages(`${cacheDir}/${copyDir}/`, `${distDir}/${copyDir}/`, imageConverter)
|
|
54
|
+
console.log(styleText('green', '[convert]'), `${cacheDir}/${copyDir}/ => ${distDir}/${copyDir}/`)
|
|
55
|
+
} else {
|
|
56
|
+
cpSync(`${cacheDir}/${copyDir}/`, `${distDir}/${copyDir}/`, { recursive: true, force: true })
|
|
57
|
+
console.log(styleText('green', '[copy]'), `${cacheDir}/${copyDir}/ => ${distDir}/${copyDir}/`)
|
|
42
58
|
}
|
|
43
|
-
|
|
44
|
-
console.log(styleText('green', '[copy]'), `${cacheDir}/${copyDir}/ => ${distDir}/${copyDir}/`)
|
|
45
|
-
})
|
|
59
|
+
}
|
|
46
60
|
|
|
47
61
|
if (!existsSync(cacheDir)) {
|
|
48
62
|
mkdirSync(cacheDir)
|
package/lib/files.js
CHANGED
package/lib/filter.js
CHANGED
|
@@ -70,6 +70,9 @@ const ifConditionEvaluator = (condition, variables) => {
|
|
|
70
70
|
* @param {object} variables
|
|
71
71
|
* @returns {string}
|
|
72
72
|
*/
|
|
73
|
+
// @vocab: テンプレート (docs/dictionary.md#テンプレート)
|
|
74
|
+
// @vocab: ヘルパー関数 (docs/dictionary.md#ヘルパー関数)
|
|
75
|
+
// @test: tests/ssg-core/filter.test.js
|
|
73
76
|
const replaceIfFilter = (text, variables) => {
|
|
74
77
|
const matched = [...text.matchAll(IF_REGEXP)]
|
|
75
78
|
for (const item of matched) {
|
|
@@ -87,6 +90,10 @@ const replaceIfFilter = (text, variables) => {
|
|
|
87
90
|
return text
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
// @vocab: SSGスクリプト (docs/dictionary.md#ssgスクリプト)
|
|
94
|
+
// @vocab: テンプレート (docs/dictionary.md#テンプレート)
|
|
95
|
+
// @vocab: ヘルパー関数 (docs/dictionary.md#ヘルパー関数)
|
|
96
|
+
// @test: tests/ssg-core/filter.test.js
|
|
90
97
|
const replaceScriptFilter = async (text, variables) => {
|
|
91
98
|
let replaced = text
|
|
92
99
|
const scripts = [...text.matchAll(SCRIPT_REGEXP)].map((matched) => {
|
package/lib/generate.js
CHANGED
|
@@ -12,6 +12,8 @@ const beforeGenerate = async () => {
|
|
|
12
12
|
await warmUpTemplate()
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// @vocab: ビルド (docs/dictionary.md#ビルド)
|
|
16
|
+
// @test: tests/ssg-core/hooks.test.js
|
|
15
17
|
export const runHooks = async (hookName, customHelperDir = null, customAllData = null, customConfig = null) => {
|
|
16
18
|
const targetConfig = customConfig || config
|
|
17
19
|
const targetAllData = customAllData || allData
|
|
@@ -43,6 +45,9 @@ export const runHooks = async (hookName, customHelperDir = null, customAllData =
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
// @vocab: ビルド (docs/dictionary.md#ビルド)
|
|
49
|
+
// @vocab: ページインデックス (docs/dictionary.md#ページインデックス)
|
|
50
|
+
// @test: tests/ssg-core/hooks.test.js
|
|
46
51
|
const generate = async () => {
|
|
47
52
|
let start = performance.now()
|
|
48
53
|
await beforeGenerate()
|
package/lib/helper.js
CHANGED
|
@@ -7,6 +7,9 @@ const helper = {}
|
|
|
7
7
|
// top-level await を使うと循環依存がある場合に ESM のデッドロックが発生するため IIFE を使用する。
|
|
8
8
|
// helperReady をエクスポートすることで、ヘルパーのロード完了を外部から await できる。
|
|
9
9
|
// (Promise は一度 resolve すれば以降の await は即座に返るため、複数箇所で await しても問題ない)
|
|
10
|
+
// @vocab: ヘルパー関数 (docs/dictionary.md#ヘルパー関数)
|
|
11
|
+
// @vocab: パッケージ (docs/dictionary.md#パッケージ)
|
|
12
|
+
// @test: tests/ssg-core/helper.test.js
|
|
10
13
|
export const helperReady = (async () => {
|
|
11
14
|
if (config.helper) {
|
|
12
15
|
const files = config.helper.split(',')
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
const IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'tiff', 'heic', 'bmp'])
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @vocab ビルド画像配布器 (docs/dictionary.md)
|
|
8
|
+
* @test tests/ssg-core/imageDistributor.test.js
|
|
9
|
+
*/
|
|
10
|
+
export async function distributeImages(srcDir, distDir, { fn, ext }) {
|
|
11
|
+
const files = await collectFiles(srcDir)
|
|
12
|
+
for (const relPath of files) {
|
|
13
|
+
const srcPath = path.join(srcDir, relPath)
|
|
14
|
+
const fileExt = path.extname(relPath).slice(1).toLowerCase()
|
|
15
|
+
const isImage = IMAGE_EXTENSIONS.has(fileExt)
|
|
16
|
+
const baseName = path.basename(relPath, path.extname(relPath))
|
|
17
|
+
const subDir = path.dirname(relPath)
|
|
18
|
+
if (fn && isImage) {
|
|
19
|
+
const outputName = ext ? `${baseName}.${ext}` : path.basename(relPath)
|
|
20
|
+
const distPath = path.join(distDir, subDir, outputName)
|
|
21
|
+
await writeConvertedFile(srcPath, distPath, fn)
|
|
22
|
+
} else {
|
|
23
|
+
const distPath = path.join(distDir, relPath)
|
|
24
|
+
fs.mkdirSync(path.dirname(distPath), { recursive: true })
|
|
25
|
+
fs.copyFileSync(srcPath, distPath)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @vocab ビルド画像配布器 (docs/dictionary.md)
|
|
32
|
+
* @test tests/ssg-core/imageDistributor.test.js
|
|
33
|
+
*/
|
|
34
|
+
export async function collectFiles(dir) {
|
|
35
|
+
const result = []
|
|
36
|
+
const walk = (current, rel) => {
|
|
37
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
38
|
+
const relPath = rel ? path.join(rel, entry.name) : entry.name
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
walk(path.join(current, entry.name), relPath)
|
|
41
|
+
} else {
|
|
42
|
+
result.push(relPath)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
walk(dir, '')
|
|
47
|
+
return result
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @vocab ビルド画像配布器 (docs/dictionary.md)
|
|
52
|
+
* @test tests/ssg-core/imageDistributor.test.js
|
|
53
|
+
*/
|
|
54
|
+
export async function writeConvertedFile(srcPath, distPath, fn) {
|
|
55
|
+
const buffer = fs.readFileSync(srcPath)
|
|
56
|
+
const converted = await fn(buffer)
|
|
57
|
+
fs.mkdirSync(path.dirname(distPath), { recursive: true })
|
|
58
|
+
fs.writeFileSync(distPath, converted)
|
|
59
|
+
}
|
package/lib/indexer.js
CHANGED
|
@@ -31,6 +31,9 @@ const collect = (dir, namePrefix = '', promises = []) => {
|
|
|
31
31
|
return promises
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// @vocab: ページインデックス (docs/dictionary.md#ページインデックス)
|
|
35
|
+
// @vocab: ページ (docs/dictionary.md#ページ)
|
|
36
|
+
// @test: tests/ssg-core/indexer.test.js
|
|
34
37
|
const indexing = async () => {
|
|
35
38
|
allData = {}
|
|
36
39
|
|
package/lib/pageData.js
CHANGED
|
@@ -5,11 +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: ページ (docs/dictionary.md#ページ)
|
|
9
|
+
// @vocab: フロントマター (docs/dictionary.md#フロントマター)
|
|
10
|
+
// @test: tests/ssg-core/pageData.test.js
|
|
8
11
|
const makePageData = (filename, content) => {
|
|
9
12
|
const [name, ext] = filename.split('.')
|
|
10
13
|
return parse(content, name, ext)
|
|
11
14
|
}
|
|
12
15
|
|
|
16
|
+
// @vocab: フロントマター (docs/dictionary.md#フロントマター)
|
|
17
|
+
// @vocab: ページ (docs/dictionary.md#ページ)
|
|
13
18
|
const parse = (content, name, ext) => {
|
|
14
19
|
const matched = content.match(FRONTMATTER_REGEXP)
|
|
15
20
|
const markdownReplaced = content.replace(FRONTMATTER_REGEXP, '')
|
|
@@ -56,11 +61,17 @@ const parse = (content, name, ext) => {
|
|
|
56
61
|
metaDataMerged.url = '/preview' + metaDataMerged.url
|
|
57
62
|
}
|
|
58
63
|
metaDataMerged.full_url = fullUrl(metaDataMerged)
|
|
59
|
-
metaDataMerged['__output'] = name === 'index'
|
|
64
|
+
metaDataMerged['__output'] = name === 'index'
|
|
65
|
+
? '/index.html'
|
|
66
|
+
: metaDataMerged.url.endsWith('/')
|
|
67
|
+
? `${metaDataMerged.url}index.html`
|
|
68
|
+
: `${metaDataMerged.url}.${metaDataMerged.ext}`
|
|
60
69
|
|
|
61
70
|
return metaDataMerged
|
|
62
71
|
}
|
|
63
72
|
|
|
73
|
+
// @vocab: フロントマター (docs/dictionary.md#フロントマター)
|
|
74
|
+
// @test: tests/ssg-core/pageData.test.js
|
|
64
75
|
const parseMetaData = (data) => {
|
|
65
76
|
const metaData = {}
|
|
66
77
|
let isStringContinue = false
|
package/lib/render.js
CHANGED
|
@@ -13,6 +13,11 @@ marked.use({
|
|
|
13
13
|
breaks: true
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
+
// @vocab: テンプレート (docs/dictionary.md#テンプレート)
|
|
17
|
+
// @vocab: ページ (docs/dictionary.md#ページ)
|
|
18
|
+
// @vocab: SSGスクリプト (docs/dictionary.md#ssgスクリプト)
|
|
19
|
+
// @vocab: ヘルパー関数 (docs/dictionary.md#ヘルパー関数)
|
|
20
|
+
// @test: tests/ssg-core/helper.test.js
|
|
16
21
|
const render = async (templateName, data) => {
|
|
17
22
|
await helperReady
|
|
18
23
|
let template
|
|
@@ -11,6 +11,9 @@ const VARIABLE_REGEXP = /(\\)?{{[\s]*([^{}]+)[\s]*}}/g
|
|
|
11
11
|
* @params {object} variables
|
|
12
12
|
* @return {text}
|
|
13
13
|
*/
|
|
14
|
+
// @vocab: テンプレート (docs/dictionary.md#テンプレート)
|
|
15
|
+
// @vocab: ヘルパー関数 (docs/dictionary.md#ヘルパー関数)
|
|
16
|
+
// @test: tests/ssg-core/replaceVariablesFilter.test.js
|
|
14
17
|
const replaceVariablesFilter = (text, variables) => {
|
|
15
18
|
const matched = [...text.matchAll(VARIABLE_REGEXP)]
|
|
16
19
|
const replace = Object.fromEntries(matched.map(match => [match[0], {variableName: match[2].trim().toLowerCase(), backslash: !!match[1]}]))
|
|
@@ -29,6 +32,8 @@ const replaceVariablesFilter = (text, variables) => {
|
|
|
29
32
|
* @param {object} variables
|
|
30
33
|
* @return {Array.<string>}
|
|
31
34
|
*/
|
|
35
|
+
// @vocab: ヘルパー関数 (docs/dictionary.md#ヘルパー関数)
|
|
36
|
+
// @vocab: テンプレート (docs/dictionary.md#テンプレート)
|
|
32
37
|
const replaceVariable = (target, replaceTargetRawText, backslash, variables) => {
|
|
33
38
|
const toBeReplace = replaceTargetRawText
|
|
34
39
|
const toBeReplaceScript = toBeReplace.match(/([\w\d_]+)\((.*)\)/)
|
package/lib/tryServer.js
CHANGED
|
@@ -9,7 +9,9 @@ const handlers = async (path) => {
|
|
|
9
9
|
if (handlersAlreadyRegistered) {
|
|
10
10
|
return registeredHandlers[path]
|
|
11
11
|
}
|
|
12
|
-
const serverFiles = fs.readdirSync(serverDir)
|
|
12
|
+
const serverFiles = fs.readdirSync(serverDir, { withFileTypes: true })
|
|
13
|
+
.filter(e => e.isFile())
|
|
14
|
+
.map(e => e.name)
|
|
13
15
|
const loaded = await Promise.all(serverFiles.map(file => import(`${serverDir}/${file}`)))
|
|
14
16
|
loaded.forEach(s => registeredHandlers[s.path] = s)
|
|
15
17
|
handlersAlreadyRegistered = true
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenjuu99/blog",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "blog template",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "node bin/dev-server",
|
|
8
8
|
"server": "node bin/server",
|
|
9
9
|
"generate": "node bin/generate",
|
|
10
|
-
"test": "node --test --test-concurrency=1
|
|
11
|
-
"test:watch": "node --test --watch
|
|
10
|
+
"test": "node --test --test-concurrency=1 tests/**/*.test.js",
|
|
11
|
+
"test:watch": "node --test --watch tests/**/*.test.js"
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
14
|
"generate": "bin/generate",
|
|
@@ -23,10 +23,13 @@
|
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"chokidar": "^4.0.x",
|
|
26
|
-
"marked": "^13.x"
|
|
26
|
+
"marked": "^13.x",
|
|
27
|
+
"sharp": "^0.35.2"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
|
-
"@
|
|
30
|
+
"@playwright/test": "^1.61.0",
|
|
31
|
+
"@tenjuu99/blog": "file:./",
|
|
32
|
+
"@types/node": "^25.9.3"
|
|
30
33
|
},
|
|
31
34
|
"type": "module",
|
|
32
35
|
"engines": {
|
|
@@ -2,6 +2,9 @@ import { allData, config } from '@tenjuu99/blog'
|
|
|
2
2
|
|
|
3
3
|
export function breadcrumbList(pageName, topPageName = 'top') {
|
|
4
4
|
const pageData = allData[pageName]
|
|
5
|
+
if (!pageData) {
|
|
6
|
+
return ''
|
|
7
|
+
}
|
|
5
8
|
const entries = Object.entries(allData)
|
|
6
9
|
const breadCrumbs = ['/']
|
|
7
10
|
if (pageData.breadcrumbs) {
|
|
@@ -8,6 +8,11 @@ let categoryTreeCache = null
|
|
|
8
8
|
* @param {Object} config - 設定オブジェクト
|
|
9
9
|
* @returns {Object} カテゴリーツリー
|
|
10
10
|
*/
|
|
11
|
+
// @vocab: カテゴリーシステム (docs/dictionary.md#カテゴリーシステム)
|
|
12
|
+
// @vocab: カテゴリー (docs/dictionary.md#カテゴリー)
|
|
13
|
+
// @vocab: カテゴリーパス (docs/dictionary.md#カテゴリーパス)
|
|
14
|
+
// @vocab: サブカテゴリー (docs/dictionary.md#サブカテゴリー)
|
|
15
|
+
// @test: tests/category/category-tree.test.js
|
|
11
16
|
export function buildCategoryTree(data = allData, conf = config) {
|
|
12
17
|
const tree = {}
|
|
13
18
|
const urlCase = conf.category?.url_case || 'lower'
|
|
@@ -74,6 +79,10 @@ export function getCategoryTree() {
|
|
|
74
79
|
* @param {Array} categoryPath - カテゴリーパス(例: ["Art", "Painting"])
|
|
75
80
|
* @returns {Array} ページデータの配列
|
|
76
81
|
*/
|
|
82
|
+
// @vocab: カテゴリーパス (docs/dictionary.md#カテゴリーパス)
|
|
83
|
+
// @vocab: カテゴリー (docs/dictionary.md#カテゴリー)
|
|
84
|
+
// @vocab: ページ (docs/dictionary.md#ページ)
|
|
85
|
+
// @test: tests/category/category-helper.test.js
|
|
77
86
|
export function getCategoryPages(categoryPath) {
|
|
78
87
|
const pages = []
|
|
79
88
|
|
|
@@ -95,6 +104,10 @@ export function getCategoryPages(categoryPath) {
|
|
|
95
104
|
* @param {Array} categoryPath - カテゴリーパス(例: ["Art"])
|
|
96
105
|
* @returns {Array} ページデータの配列
|
|
97
106
|
*/
|
|
107
|
+
// @vocab: サブカテゴリー (docs/dictionary.md#サブカテゴリー)
|
|
108
|
+
// @vocab: カテゴリーパス (docs/dictionary.md#カテゴリーパス)
|
|
109
|
+
// @vocab: ページ (docs/dictionary.md#ページ)
|
|
110
|
+
// @test: tests/category/category-helper.test.js
|
|
98
111
|
export function getCategoryPagesRecursive(categoryPath) {
|
|
99
112
|
const pages = []
|
|
100
113
|
|
|
@@ -4,6 +4,8 @@ import { buildCategoryTree } from './category.js'
|
|
|
4
4
|
* 記事名配列を perPage 件ずつのページ配列に分割する
|
|
5
5
|
* 空の場合は [[]] を返す(ページ数ゼロを避けるため)
|
|
6
6
|
*/
|
|
7
|
+
// @vocab: ページネーション (docs/dictionary.md#ページネーション)
|
|
8
|
+
// @test: tests/category/category-pagination.test.js
|
|
7
9
|
function sliceIntoPages(items, perPage) {
|
|
8
10
|
if (items.length === 0) return [[]]
|
|
9
11
|
const pages = []
|
|
@@ -37,6 +39,9 @@ function sliceIntoPages(items, perPage) {
|
|
|
37
39
|
* @param {Object} categoryConfig - 単一カテゴリー設定オブジェクト
|
|
38
40
|
* @param {Object} config - グローバル設定オブジェクト
|
|
39
41
|
*/
|
|
42
|
+
// @vocab: カテゴリーページ (docs/dictionary.md#カテゴリーページ)
|
|
43
|
+
// @vocab: ページネーション (docs/dictionary.md#ページネーション)
|
|
44
|
+
// @test: tests/category/category-pagination.test.js
|
|
40
45
|
function buildVirtualPage(url, categoryData, children, currentPage, totalPages, categoryConfig, config) {
|
|
41
46
|
const pageName = url.replace(/^\//, '') + '/index'
|
|
42
47
|
const paginationBase = categoryData.baseUrl + '/'
|
|
@@ -44,7 +49,7 @@ function buildVirtualPage(url, categoryData, children, currentPage, totalPages,
|
|
|
44
49
|
|
|
45
50
|
const page = {
|
|
46
51
|
name: pageName,
|
|
47
|
-
url: url,
|
|
52
|
+
url: url + '/',
|
|
48
53
|
__output: `${url}/index.html`,
|
|
49
54
|
title: categoryData.title,
|
|
50
55
|
template: categoryConfig.template || 'category.html',
|
|
@@ -65,7 +70,7 @@ function buildVirtualPage(url, categoryData, children, currentPage, totalPages,
|
|
|
65
70
|
og_description: `${categoryData.title} カテゴリーのページ一覧`,
|
|
66
71
|
__filetype: 'md',
|
|
67
72
|
markdown_not_parsed: '',
|
|
68
|
-
full_url: `${config.url_base || 'http://localhost:8000'}${url}
|
|
73
|
+
full_url: `${config.url_base || 'http://localhost:8000'}${url}/`,
|
|
69
74
|
category_pages: [],
|
|
70
75
|
category_current_page: currentPage,
|
|
71
76
|
category_total_pages: totalPages,
|
|
@@ -82,6 +87,10 @@ function buildVirtualPage(url, categoryData, children, currentPage, totalPages,
|
|
|
82
87
|
* @param {Object} categoryConfig - 単一カテゴリー設定オブジェクト
|
|
83
88
|
* @param {Object} config - グローバル設定オブジェクト
|
|
84
89
|
*/
|
|
90
|
+
// @vocab: カテゴリーページ (docs/dictionary.md#カテゴリーページ)
|
|
91
|
+
// @vocab: カテゴリーシステム (docs/dictionary.md#カテゴリーシステム)
|
|
92
|
+
// @vocab: ページネーション (docs/dictionary.md#ページネーション)
|
|
93
|
+
// @test: tests/category/category-pagination.test.js
|
|
85
94
|
function generateCategoryPages(allData, categoryConfig, config) {
|
|
86
95
|
const pathFilter = categoryConfig.path_filter || ''
|
|
87
96
|
let filteredData = allData
|
|
@@ -100,7 +109,7 @@ function generateCategoryPages(allData, categoryConfig, config) {
|
|
|
100
109
|
|
|
101
110
|
for (const [url, categoryData] of Object.entries(tree)) {
|
|
102
111
|
const children = Object.entries(categoryData.children).map(([childUrl, childNode]) => ({
|
|
103
|
-
url: childUrl,
|
|
112
|
+
url: childUrl + '/',
|
|
104
113
|
title: childNode.title,
|
|
105
114
|
}))
|
|
106
115
|
|
|
@@ -140,6 +149,9 @@ function generateCategoryPages(allData, categoryConfig, config) {
|
|
|
140
149
|
* @param {Object} allData - 全ページデータ
|
|
141
150
|
* @param {Object} config - 設定オブジェクト
|
|
142
151
|
*/
|
|
152
|
+
// @vocab: カテゴリーシステム (docs/dictionary.md#カテゴリーシステム)
|
|
153
|
+
// @vocab: ビルド (docs/dictionary.md#ビルド)
|
|
154
|
+
// @test: tests/category/category-indexer.test.js
|
|
143
155
|
export async function afterIndexing(allData, config) {
|
|
144
156
|
const categorySystems = config.categories
|
|
145
157
|
? config.categories
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* getPaginationUrl: ページ番号から URL を生成する
|
|
3
3
|
* page === 1 なら basePath、それ以外なら basePath + page + '/'
|
|
4
4
|
*/
|
|
5
|
+
// @vocab: ページネーション (docs/dictionary.md#ページネーション)
|
|
6
|
+
// @vocab: カテゴリーページ (docs/dictionary.md#カテゴリーページ)
|
|
7
|
+
// @test: tests/category/category-pagination.test.js
|
|
5
8
|
export function getPaginationUrl(basePath, page) {
|
|
6
9
|
const base = basePath.endsWith('/') ? basePath : `${basePath}/`
|
|
7
10
|
if (page === 1) return base
|
|
@@ -12,6 +15,8 @@ export function getPaginationUrl(basePath, page) {
|
|
|
12
15
|
* buildWindowedPages: ウィンドウ付きページ番号リストを生成する
|
|
13
16
|
* 各要素は {num, isCurrent} または {num: null, isEllipsis: true}
|
|
14
17
|
*/
|
|
18
|
+
// @vocab: ページネーション (docs/dictionary.md#ページネーション)
|
|
19
|
+
// @test: tests/category/category-pagination.test.js
|
|
15
20
|
export function buildWindowedPages(totalPages, currentPage, windowSize = 2) {
|
|
16
21
|
if (totalPages <= 0) return []
|
|
17
22
|
if (totalPages === 1) return [{ num: 1, isCurrent: currentPage === 1 }]
|
|
@@ -90,7 +90,7 @@ form select {
|
|
|
90
90
|
background: rgb(192, 198, 217);
|
|
91
91
|
height: 100%;
|
|
92
92
|
padding: 0;
|
|
93
|
-
overflow-y:
|
|
93
|
+
overflow-y: auto;
|
|
94
94
|
flex-basis: 300px;
|
|
95
95
|
position: relative;
|
|
96
96
|
z-index: 3;
|
|
@@ -117,9 +117,28 @@ form select {
|
|
|
117
117
|
color: #666;
|
|
118
118
|
text-decoration: none;
|
|
119
119
|
}
|
|
120
|
+
.sidebar a.active {
|
|
121
|
+
font-weight: bold;
|
|
122
|
+
color: #333;
|
|
123
|
+
background: rgba(104, 117, 154, .2);
|
|
124
|
+
display: block;
|
|
125
|
+
}
|
|
120
126
|
.sidebar li:hover {
|
|
121
127
|
background: rgba(104, 117, 154, .3);
|
|
122
128
|
}
|
|
129
|
+
.sidebar details > summary {
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
padding: 2px 0;
|
|
132
|
+
color: #444;
|
|
133
|
+
font-size: 0.9em;
|
|
134
|
+
list-style: disclosure-closed;
|
|
135
|
+
}
|
|
136
|
+
.sidebar details[open] > summary {
|
|
137
|
+
list-style: disclosure-open;
|
|
138
|
+
}
|
|
139
|
+
.sidebar details > ul {
|
|
140
|
+
padding-left: 8px;
|
|
141
|
+
}
|
|
123
142
|
.sidebar-toggle {
|
|
124
143
|
cursor: pointer;
|
|
125
144
|
width: 20px;
|