methanol 0.0.13 → 0.0.15
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/index.js +1 -0
- package/package.json +1 -1
- package/src/build-system.js +1 -0
- package/src/config.js +33 -3
- package/src/dev-server.js +21 -13
- package/src/pages-index.js +42 -0
- package/src/pages.js +1 -0
- package/src/reframe.js +1 -1
- package/src/state.js +8 -0
- package/src/text-utils.js +60 -0
- package/src/vite-plugins.js +9 -0
- package/src/workers/build-pool.js +1 -0
- package/themes/blog/README.md +26 -0
- package/themes/blog/components/CategoryView.client.jsx +164 -0
- package/themes/blog/components/CategoryView.static.jsx +35 -0
- package/themes/blog/components/CollectionView.client.jsx +151 -0
- package/themes/blog/components/CollectionView.static.jsx +37 -0
- package/themes/blog/components/PostList.client.jsx +92 -0
- package/themes/blog/components/PostList.static.jsx +36 -0
- package/themes/blog/components/ThemeSearchBox.client.jsx +427 -0
- package/themes/blog/components/ThemeSearchBox.static.jsx +40 -0
- package/themes/blog/index.js +40 -0
- package/themes/blog/pages/404.mdx +12 -0
- package/themes/blog/pages/about.mdx +14 -0
- package/themes/blog/pages/categories.mdx +6 -0
- package/themes/blog/pages/collections.mdx +6 -0
- package/themes/blog/pages/index.mdx +16 -0
- package/themes/blog/pages/offline.mdx +11 -0
- package/themes/blog/sources/style.css +579 -0
- package/themes/blog/src/date-utils.js +28 -0
- package/themes/blog/src/heading.jsx +37 -0
- package/themes/blog/src/layout-categories.jsx +66 -0
- package/themes/blog/src/layout-collections.jsx +65 -0
- package/themes/blog/src/layout-home.jsx +66 -0
- package/themes/blog/src/layout-post.jsx +42 -0
- package/themes/blog/src/page.jsx +152 -0
- package/themes/blog/src/post-utils.js +83 -0
- package/themes/default/sources/theme-prepare.js +0 -1
- package/themes/default/src/page.jsx +1 -1
package/index.js
CHANGED
package/package.json
CHANGED
package/src/build-system.js
CHANGED
package/src/config.js
CHANGED
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
|
|
21
21
|
import { readFile } from 'fs/promises'
|
|
22
22
|
import { existsSync } from 'fs'
|
|
23
|
-
import { resolve, isAbsolute, extname, basename } from 'path'
|
|
24
|
-
import { pathToFileURL } from 'url'
|
|
23
|
+
import { resolve, isAbsolute, extname, basename, dirname } from 'path'
|
|
24
|
+
import { pathToFileURL, fileURLToPath } from 'url'
|
|
25
25
|
import { mergeConfig } from 'vite'
|
|
26
|
+
import { projectRequire } from './node-loader.js'
|
|
26
27
|
import { cli, state } from './state.js'
|
|
27
28
|
import { logger } from './logger.js'
|
|
28
29
|
import { HTMLRenderer } from './renderer.js'
|
|
@@ -239,6 +240,34 @@ const buildConfigContext = (mode) => ({
|
|
|
239
240
|
HTMLRenderer
|
|
240
241
|
})
|
|
241
242
|
|
|
243
|
+
const resolveTheme = async (themeValue, root) => {
|
|
244
|
+
if (typeof themeValue !== 'string') return themeValue
|
|
245
|
+
|
|
246
|
+
const load = async (p) => {
|
|
247
|
+
const mod = await import(pathToFileURL(p).href)
|
|
248
|
+
const fn = mod.default ?? mod
|
|
249
|
+
return typeof fn === 'function' ? fn() : fn
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 1. Check Methanol themes dir
|
|
253
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
254
|
+
const builtInPath = resolve(__dirname, '../themes', themeValue, 'index.js')
|
|
255
|
+
if (existsSync(builtInPath)) {
|
|
256
|
+
return load(builtInPath)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 2. Resolve methanol-theme-<name> from user dir
|
|
260
|
+
try {
|
|
261
|
+
const pkgName = `methanol-theme-${themeValue}`
|
|
262
|
+
const pkgPath = projectRequire.resolve(pkgName)
|
|
263
|
+
return load(pkgPath)
|
|
264
|
+
} catch (e) {
|
|
265
|
+
// Ignore
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
throw new Error(`Theme not found: ${themeValue}`)
|
|
269
|
+
}
|
|
270
|
+
|
|
242
271
|
export const loadUserConfig = async (mode, configPath = null) => {
|
|
243
272
|
if (configPath) {
|
|
244
273
|
const filePath = resolveConfigPath(configPath)
|
|
@@ -323,7 +352,8 @@ export const applyConfig = async (config, mode) => {
|
|
|
323
352
|
|
|
324
353
|
state.VIRTUAL_HTML_OUTPUT_ROOT = state.PAGES_DIR
|
|
325
354
|
|
|
326
|
-
|
|
355
|
+
const themeValue = cli.CLI_THEME || config.theme
|
|
356
|
+
state.USER_THEME = themeValue ? await resolveTheme(themeValue, root) : await defaultTheme()
|
|
327
357
|
if (!state.USER_THEME?.root && !config.theme?.root) {
|
|
328
358
|
throw new Error('Theme root is required.')
|
|
329
359
|
}
|
package/src/dev-server.js
CHANGED
|
@@ -143,6 +143,7 @@ export const runViteDev = async () => {
|
|
|
143
143
|
let pagesContextToken = 0
|
|
144
144
|
const setPagesContext = (next) => {
|
|
145
145
|
pagesContext = next
|
|
146
|
+
state.PAGES_CONTEXT = next
|
|
146
147
|
pagesContextToken += 1
|
|
147
148
|
}
|
|
148
149
|
setPagesContext(await buildPagesContext({ compileAll: false }))
|
|
@@ -160,6 +161,23 @@ export const runViteDev = async () => {
|
|
|
160
161
|
console.error(error?.stack || error)
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
const _invalidate = (id) => {
|
|
165
|
+
const _module = server.moduleGraph.getModuleById(id)
|
|
166
|
+
if (_module) {
|
|
167
|
+
server.moduleGraph.invalidateModule(_module)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const invalidateRewindInject = () => {
|
|
171
|
+
_invalidate('\0/.methanol_virtual_module/registry.js')
|
|
172
|
+
_invalidate('\0methanol:registry')
|
|
173
|
+
_invalidate('\0/.methanol_virtual_module/inject.js')
|
|
174
|
+
_invalidate('\0methanol:inject')
|
|
175
|
+
}
|
|
176
|
+
const invalidatePagesIndex = () => {
|
|
177
|
+
_invalidate('\0/.methanol_virtual_module/pages.js')
|
|
178
|
+
_invalidate('\0methanol:pages')
|
|
179
|
+
}
|
|
180
|
+
|
|
163
181
|
const refreshPagesContext = async () => {
|
|
164
182
|
setPagesContext(await buildPagesContext({ compileAll: false }))
|
|
165
183
|
}
|
|
@@ -214,6 +232,7 @@ export const runViteDev = async () => {
|
|
|
214
232
|
}
|
|
215
233
|
}
|
|
216
234
|
pagesContext.refreshPagesTree?.()
|
|
235
|
+
invalidatePagesIndex()
|
|
217
236
|
invalidateHtmlCache()
|
|
218
237
|
const renderEpoch = htmlCacheEpoch
|
|
219
238
|
|
|
@@ -518,19 +537,6 @@ export const runViteDev = async () => {
|
|
|
518
537
|
await server.listen()
|
|
519
538
|
server.printUrls()
|
|
520
539
|
|
|
521
|
-
const _invalidate = (id) => {
|
|
522
|
-
const _module = server.moduleGraph.getModuleById(id)
|
|
523
|
-
if (_module) {
|
|
524
|
-
server.moduleGraph.invalidateModule(_module)
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
const invalidateRewindInject = () => {
|
|
528
|
-
_invalidate('\0/.methanol_virtual_module/registry.js')
|
|
529
|
-
_invalidate('\0methanol:registry')
|
|
530
|
-
_invalidate('\0/.methanol_virtual_module/inject.js')
|
|
531
|
-
_invalidate('\0methanol:inject')
|
|
532
|
-
}
|
|
533
|
-
|
|
534
540
|
let queue = Promise.resolve()
|
|
535
541
|
const enqueue = (task) => {
|
|
536
542
|
queue = queue.then(task).catch((err) => {
|
|
@@ -547,6 +553,7 @@ export const runViteDev = async () => {
|
|
|
547
553
|
|
|
548
554
|
const refreshPages = async () => {
|
|
549
555
|
await refreshPagesContext()
|
|
556
|
+
invalidatePagesIndex()
|
|
550
557
|
invalidateHtmlCache()
|
|
551
558
|
reload()
|
|
552
559
|
}
|
|
@@ -700,6 +707,7 @@ export const runViteDev = async () => {
|
|
|
700
707
|
}
|
|
701
708
|
const updated = await updatePageEntry(path, resolved)
|
|
702
709
|
if (updated) {
|
|
710
|
+
invalidatePagesIndex()
|
|
703
711
|
invalidateHtmlCache()
|
|
704
712
|
reload()
|
|
705
713
|
return
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Copyright Yukino Song, SudoMaker Ltd.
|
|
2
|
+
*
|
|
3
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
* or more contributor license agreements. See the NOTICE file
|
|
5
|
+
* distributed with this work for additional information
|
|
6
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
* to you under the Apache License, Version 2.0 (the
|
|
8
|
+
* "License"); you may not use this file except in compliance
|
|
9
|
+
* with the License. You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing,
|
|
14
|
+
* software distributed under the License is distributed on an
|
|
15
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
* KIND, either express or implied. See the License for the
|
|
17
|
+
* specific language governing permissions and limitations
|
|
18
|
+
* under the License.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import JSON5 from 'json5'
|
|
22
|
+
import { extractExcerpt } from './text-utils.js'
|
|
23
|
+
|
|
24
|
+
const OMIT_KEYS = new Set(['content', 'mdxComponent', 'mdxCtx', 'getSiblings'])
|
|
25
|
+
|
|
26
|
+
const sanitizePage = (page) => {
|
|
27
|
+
const result = {}
|
|
28
|
+
for (const [key, value] of Object.entries(page || {})) {
|
|
29
|
+
if (OMIT_KEYS.has(key)) continue
|
|
30
|
+
if (typeof value === 'function') continue
|
|
31
|
+
result[key] = value
|
|
32
|
+
}
|
|
33
|
+
result.excerpt = extractExcerpt(page)
|
|
34
|
+
return result
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const serializePagesIndex = (pages) => {
|
|
38
|
+
const list = Array.isArray(pages)
|
|
39
|
+
? pages.filter((page) => !page?.hidden).map(sanitizePage)
|
|
40
|
+
: []
|
|
41
|
+
return JSON5.stringify(list)
|
|
42
|
+
}
|
package/src/pages.js
CHANGED
package/src/reframe.js
CHANGED
|
@@ -65,7 +65,7 @@ export function env(parentEnv) {
|
|
|
65
65
|
|
|
66
66
|
const id = renderCount++
|
|
67
67
|
const idStr = id.toString(16)
|
|
68
|
-
const script = `$$rfrm(${JSON.stringify(key)},${id},${Object.keys(props).length ? JSON5.stringify(props) : '{}'})`
|
|
68
|
+
const script = `$$rfrm(${JSON.stringify(key)},${id},${Object.keys(props).length ? JSON5.stringify(props).replace(/<\/script/ig, '<\\/script') : '{}'})`
|
|
69
69
|
|
|
70
70
|
return (R) => {
|
|
71
71
|
return [
|
package/src/state.js
CHANGED
|
@@ -130,6 +130,12 @@ const withCommonOptions = (y) =>
|
|
|
130
130
|
type: 'boolean',
|
|
131
131
|
default: undefined
|
|
132
132
|
})
|
|
133
|
+
.option('theme', {
|
|
134
|
+
describe: 'Theme to use (name or path)',
|
|
135
|
+
type: 'string',
|
|
136
|
+
requiresArg: true,
|
|
137
|
+
nargs: 1
|
|
138
|
+
})
|
|
133
139
|
.option('pwa', {
|
|
134
140
|
describe: 'Enable or disable PWA support',
|
|
135
141
|
type: 'boolean',
|
|
@@ -167,6 +173,7 @@ export const cli = {
|
|
|
167
173
|
CLI_VERBOSE: Boolean(argv.verbose),
|
|
168
174
|
CLI_BASE: argv.base || null,
|
|
169
175
|
CLI_SEARCH: typeof argv.search === 'boolean' ? argv.search : undefined,
|
|
176
|
+
CLI_THEME: argv.theme || null,
|
|
170
177
|
CLI_PWA: typeof argv.pwa === 'boolean' ? argv.pwa : undefined
|
|
171
178
|
}
|
|
172
179
|
|
|
@@ -192,6 +199,7 @@ export const state = {
|
|
|
192
199
|
USER_VITE_CONFIG: null,
|
|
193
200
|
USER_MDX_CONFIG: null,
|
|
194
201
|
USER_PUBLIC_OVERRIDE: false,
|
|
202
|
+
PAGES_CONTEXT: null,
|
|
195
203
|
SOURCES: [],
|
|
196
204
|
PAGEFIND_ENABLED: false,
|
|
197
205
|
PAGEFIND_OPTIONS: null,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* Copyright Yukino Song, SudoMaker Ltd.
|
|
2
|
+
*
|
|
3
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
* or more contributor license agreements. See the NOTICE file
|
|
5
|
+
* distributed with this work for additional information
|
|
6
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
* to you under the Apache License, Version 2.0 (the
|
|
8
|
+
* "License"); you may not use this file except in compliance
|
|
9
|
+
* with the License. You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing,
|
|
14
|
+
* software distributed under the License is distributed on an
|
|
15
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
* KIND, either express or implied. See the License for the
|
|
17
|
+
* specific language governing permissions and limitations
|
|
18
|
+
* under the License.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const DEFAULT_EXCERPT_LENGTH = 200
|
|
22
|
+
|
|
23
|
+
const collapseWhitespace = (value) => value.replace(/\s+/g, ' ').trim()
|
|
24
|
+
|
|
25
|
+
const stripFirstHeading = (value) => value.replace(/^\s{0,3}#{1,6}\s+.*$/m, ' ')
|
|
26
|
+
|
|
27
|
+
const stripMarkdown = (value) => {
|
|
28
|
+
let text = value
|
|
29
|
+
text = text.replace(/```[\s\S]*?```/g, ' ')
|
|
30
|
+
text = text.replace(/`[^`]*`/g, ' ')
|
|
31
|
+
text = stripFirstHeading(text)
|
|
32
|
+
text = text.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
33
|
+
text = text.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
|
34
|
+
text = text.replace(/<[^>]+>/g, ' ')
|
|
35
|
+
text = text.replace(/^\s{0,3}#{1,6}\s+/gm, '')
|
|
36
|
+
text = text.replace(/^\s{0,3}>\s?/gm, '')
|
|
37
|
+
text = text.replace(/^\s*[-*+]\s+/gm, '')
|
|
38
|
+
text = text.replace(/^\s*\d+\.\s+/gm, '')
|
|
39
|
+
return collapseWhitespace(text)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const extractExcerpt = (page, options = {}) => {
|
|
43
|
+
if (!page) return ''
|
|
44
|
+
const length =
|
|
45
|
+
typeof options.length === 'number' && Number.isFinite(options.length)
|
|
46
|
+
? options.length
|
|
47
|
+
: DEFAULT_EXCERPT_LENGTH
|
|
48
|
+
const raw =
|
|
49
|
+
page.excerpt ||
|
|
50
|
+
page.frontmatter?.excerpt ||
|
|
51
|
+
page.frontmatter?.description ||
|
|
52
|
+
page.content ||
|
|
53
|
+
''
|
|
54
|
+
const sanitized = stripMarkdown(String(raw))
|
|
55
|
+
if (!sanitized) return ''
|
|
56
|
+
if (length > 0 && sanitized.length > length) {
|
|
57
|
+
return `${sanitized.slice(0, length).trim()}...`
|
|
58
|
+
}
|
|
59
|
+
return sanitized
|
|
60
|
+
}
|
package/src/vite-plugins.js
CHANGED
|
@@ -26,6 +26,7 @@ import { normalizePath } from 'vite'
|
|
|
26
26
|
import { state } from './state.js'
|
|
27
27
|
import { resolveBasePrefix } from './config.js'
|
|
28
28
|
import { genRegistryScript } from './components.js'
|
|
29
|
+
import { serializePagesIndex } from './pages-index.js'
|
|
29
30
|
import { INJECT_SCRIPT, LOADER_SCRIPT, PAGEFIND_LOADER_SCRIPT, PWA_INJECT_SCRIPT } from './client/virtual-module/assets.js'
|
|
30
31
|
import { projectRequire } from './node-loader.js'
|
|
31
32
|
|
|
@@ -144,6 +145,14 @@ const virtualModuleMap = {
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
return ''
|
|
148
|
+
},
|
|
149
|
+
get pages() {
|
|
150
|
+
const pages = state.PAGES_CONTEXT?.pages || []
|
|
151
|
+
return `export const pages = ${serializePagesIndex(pages)}\nexport default pages`
|
|
152
|
+
},
|
|
153
|
+
get 'pages.js'() {
|
|
154
|
+
const pages = state.PAGES_CONTEXT?.pages || []
|
|
155
|
+
return `export const pages = ${serializePagesIndex(pages)}\nexport default pages`
|
|
147
156
|
}
|
|
148
157
|
}
|
|
149
158
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Blog Theme
|
|
2
|
+
|
|
3
|
+
A simple, clean blog theme for Methanol.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- Clean typography
|
|
7
|
+
- Post list on homepage
|
|
8
|
+
- Responsive design
|
|
9
|
+
- Dark mode support (via system preference)
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
To use this theme, configure your Methanol project to point to this directory.
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
// methanol.config.js
|
|
17
|
+
export default {
|
|
18
|
+
theme: './themes/blog',
|
|
19
|
+
// ...
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Structure
|
|
24
|
+
- `src/page.jsx`: Main layout template.
|
|
25
|
+
- `sources/style.css`: Stylesheet.
|
|
26
|
+
- `pages/`: Default pages (Home, 404, Offline).
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/* Copyright Yukino Song, SudoMaker Ltd.
|
|
2
|
+
*
|
|
3
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
* or more contributor license agreements. See the NOTICE file
|
|
5
|
+
* distributed with this work for additional information
|
|
6
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
* to you under the Apache License, Version 2.0 (the
|
|
8
|
+
* "License"); you may not use this file except in compliance
|
|
9
|
+
* with the License. You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing,
|
|
14
|
+
* software distributed under the License is distributed on an
|
|
15
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
* KIND, either express or implied. See the License for the
|
|
17
|
+
* specific language governing permissions and limitations
|
|
18
|
+
* under the License.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { signal, $, For, If, onCondition } from 'refui'
|
|
22
|
+
import pages from 'methanol:pages'
|
|
23
|
+
import { formatDate } from '../src/date-utils.js'
|
|
24
|
+
|
|
25
|
+
const rawPages = Array.isArray(pages) ? pages : []
|
|
26
|
+
const isPostPage = (p) => {
|
|
27
|
+
if (!p?.routeHref) return false
|
|
28
|
+
if (p.routeHref === '/' || p.routeHref === '/404' || p.routeHref === '/offline') return false
|
|
29
|
+
if (p.routeHref.startsWith('/.methanol')) return false
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
const resolvePosts = () => {
|
|
33
|
+
const list = rawPages.filter(isPostPage)
|
|
34
|
+
list.sort((a, b) => {
|
|
35
|
+
const dateA = new Date(a.frontmatter?.date || a.stats?.createdAt || 0)
|
|
36
|
+
const dateB = new Date(b.frontmatter?.date || b.stats?.createdAt || 0)
|
|
37
|
+
return dateB - dateA
|
|
38
|
+
})
|
|
39
|
+
return list.map((p) => ({
|
|
40
|
+
title: p.title,
|
|
41
|
+
routeHref: p.routeHref,
|
|
42
|
+
frontmatter: p.frontmatter || {},
|
|
43
|
+
stats: p.stats || {},
|
|
44
|
+
excerpt: p.excerpt || p.frontmatter?.excerpt || ''
|
|
45
|
+
}))
|
|
46
|
+
}
|
|
47
|
+
const allPosts = resolvePosts()
|
|
48
|
+
const defaultCategories = Array.from(
|
|
49
|
+
new Set(
|
|
50
|
+
allPosts.flatMap((p) => {
|
|
51
|
+
const c = p.frontmatter?.categories
|
|
52
|
+
return Array.isArray(c) ? c : c ? [c] : []
|
|
53
|
+
})
|
|
54
|
+
)
|
|
55
|
+
).sort()
|
|
56
|
+
|
|
57
|
+
export default function ({ categories = null } = {}) {
|
|
58
|
+
const currentCategory = signal('')
|
|
59
|
+
const activeCategory = onCondition(currentCategory)
|
|
60
|
+
const visibleCount = signal(10)
|
|
61
|
+
const categoryList = Array.isArray(categories) && categories.length ? categories : defaultCategories
|
|
62
|
+
|
|
63
|
+
if (typeof window !== 'undefined') {
|
|
64
|
+
// Initial setup on client side
|
|
65
|
+
const params = new URLSearchParams(window.location.search)
|
|
66
|
+
const cat = params.get('category')
|
|
67
|
+
if (cat) currentCategory.value = cat
|
|
68
|
+
|
|
69
|
+
window.addEventListener('popstate', () => {
|
|
70
|
+
const p = new URLSearchParams(window.location.search)
|
|
71
|
+
currentCategory.value = p.get('category') || ''
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const filteredPosts = $(() => {
|
|
76
|
+
const cat = currentCategory.value
|
|
77
|
+
if (!cat) return allPosts
|
|
78
|
+
return allPosts.filter((p) => {
|
|
79
|
+
const pCats = p.frontmatter?.categories || []
|
|
80
|
+
return Array.isArray(pCats) ? pCats.includes(cat) : pCats === cat
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const visiblePosts = $(() => filteredPosts.value.slice(0, visibleCount.value))
|
|
85
|
+
const hasMore = $(() => visibleCount.value < filteredPosts.value.length)
|
|
86
|
+
const showEmpty = $(() => filteredPosts.value.length === 0)
|
|
87
|
+
|
|
88
|
+
const loadMore = () => {
|
|
89
|
+
visibleCount.value += 10
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const setCategory = (cat) => {
|
|
93
|
+
currentCategory.value = cat
|
|
94
|
+
visibleCount.value = 10
|
|
95
|
+
const url = new URL(window.location)
|
|
96
|
+
if (cat) {
|
|
97
|
+
url.searchParams.set('category', cat)
|
|
98
|
+
} else {
|
|
99
|
+
url.searchParams.delete('category')
|
|
100
|
+
}
|
|
101
|
+
window.history.pushState({}, '', url)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div class="category-view">
|
|
106
|
+
<div class="category-list">
|
|
107
|
+
<button class="category-tag" class:active={activeCategory('')} on:click={() => setCategory('')}>
|
|
108
|
+
All
|
|
109
|
+
</button>
|
|
110
|
+
<For entries={categoryList}>
|
|
111
|
+
{({ item: cat }) => (
|
|
112
|
+
<button
|
|
113
|
+
class="category-tag"
|
|
114
|
+
class:active={activeCategory(cat.value)}
|
|
115
|
+
on:click={() => setCategory(cat.value)}
|
|
116
|
+
>
|
|
117
|
+
{cat}
|
|
118
|
+
</button>
|
|
119
|
+
)}
|
|
120
|
+
</For>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div class="post-list">
|
|
124
|
+
<For entries={visiblePosts}>
|
|
125
|
+
{({ item: p }) => {
|
|
126
|
+
const dateStr = formatDate(p.frontmatter?.date || p.stats?.createdAt)
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<article class="post-item">
|
|
130
|
+
<div class="post-meta">
|
|
131
|
+
{dateStr && <span class="post-date">{dateStr}</span>}
|
|
132
|
+
{p.frontmatter.categories && (
|
|
133
|
+
<span class="post-categories">
|
|
134
|
+
·{' '}
|
|
135
|
+
{Array.isArray(p.frontmatter.categories)
|
|
136
|
+
? p.frontmatter.categories.join(', ')
|
|
137
|
+
: p.frontmatter.categories}
|
|
138
|
+
</span>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
<h2 class="post-item-title">
|
|
142
|
+
<a href={p.routeHref}>{p.title || 'Untitled'}</a>
|
|
143
|
+
</h2>
|
|
144
|
+
<div class="post-excerpt">{p.frontmatter.excerpt || p.excerpt || 'No excerpt available.'}</div>
|
|
145
|
+
</article>
|
|
146
|
+
)
|
|
147
|
+
}}
|
|
148
|
+
</For>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<If condition={showEmpty}>{() => <p>No posts found in this category.</p>}</If>
|
|
152
|
+
|
|
153
|
+
<If condition={hasMore}>
|
|
154
|
+
{() => (
|
|
155
|
+
<div class="pagination-container">
|
|
156
|
+
<button class="load-more-btn" on:click={loadMore}>
|
|
157
|
+
Load More
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</If>
|
|
162
|
+
</div>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* Copyright Yukino Song, SudoMaker Ltd.
|
|
2
|
+
*
|
|
3
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
* or more contributor license agreements. See the NOTICE file
|
|
5
|
+
* distributed with this work for additional information
|
|
6
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
* to you under the Apache License, Version 2.0 (the
|
|
8
|
+
* "License"); you may not use this file except in compliance
|
|
9
|
+
* with the License. You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing,
|
|
14
|
+
* software distributed under the License is distributed on an
|
|
15
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
* KIND, either express or implied. See the License for the
|
|
17
|
+
* specific language governing permissions and limitations
|
|
18
|
+
* under the License.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export default function ({ categories = [] } = {}, ...children) {
|
|
22
|
+
if (!children.length) return null
|
|
23
|
+
return (
|
|
24
|
+
<div class="category-view">
|
|
25
|
+
<div class="category-list">
|
|
26
|
+
<button class="category-tag active">All</button>
|
|
27
|
+
{categories.map((cat) => (
|
|
28
|
+
<button class="category-tag">{cat}</button>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="post-list">{...children}</div>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|