@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/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
- distributeRaw.forEach((copyDir) => {
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
- return
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
- cpSync(`${cacheDir}/${copyDir}/`, `${distDir}/${copyDir}/`, { recursive: true, force: true })
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
@@ -4,6 +4,8 @@ import { templateDir, cssDir } from './dir.js'
4
4
  let staticFilesContainer = {}
5
5
  let loaded = false
6
6
 
7
+ // @vocab: テンプレート (docs/dictionary.md#テンプレート)
8
+ // @test: tests/editor/editor-sidebar.test.js
7
9
  const warmUp = async () => {
8
10
  if (loaded) {
9
11
  return
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' ? '/index.html' : `${metaDataMerged.url}.${metaDataMerged.ext}`
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.4",
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 test/**/*.test.js",
11
- "test:watch": "node --test --watch test/**/*.test.js"
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
- "@tenjuu99/blog": "file:./"
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: hidden;
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;
@@ -0,0 +1,6 @@
1
+ import { buildTree, renderTreeHtml } from '../js/tree.js'
2
+
3
+ export function renderSidebarTree(files) {
4
+ const tree = buildTree(files.filter(f => !f.__is_auto_category))
5
+ return renderTreeHtml(tree)
6
+ }