methanol 0.0.14 → 0.0.16
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/client/virtual-module/assets.js +7 -6
- package/src/config.js +33 -3
- package/src/dev-server.js +68 -26
- package/src/error-page.jsx +49 -0
- package/src/mdx.js +235 -8
- 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 +12 -27
- 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 +65 -0
- package/themes/blog/src/layout-collections.jsx +64 -0
- package/themes/blog/src/layout-home.jsx +65 -0
- package/themes/blog/src/layout-post.jsx +40 -0
- package/themes/blog/src/page.jsx +152 -0
- package/themes/blog/src/post-utils.js +83 -0
- package/themes/default/src/page.jsx +2 -2
|
@@ -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,7 +26,8 @@ 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 {
|
|
29
|
+
import { serializePagesIndex } from './pages-index.js'
|
|
30
|
+
import { virtualModuleDir, 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
|
|
|
32
33
|
const require = createRequire(import.meta.url)
|
|
@@ -126,13 +127,10 @@ const virtualModuleMap = {
|
|
|
126
127
|
get registry() {
|
|
127
128
|
return `export const registry = ${genRegistryScript()}`
|
|
128
129
|
},
|
|
129
|
-
get 'registry.js'() {
|
|
130
|
-
return `export const registry = ${genRegistryScript()}`
|
|
131
|
-
},
|
|
132
130
|
get loader() {
|
|
133
131
|
return LOADER_SCRIPT()
|
|
134
132
|
},
|
|
135
|
-
get 'inject
|
|
133
|
+
get 'inject'() {
|
|
136
134
|
return INJECT_SCRIPT()
|
|
137
135
|
},
|
|
138
136
|
get 'pagefind-loader'() {
|
|
@@ -144,6 +142,10 @@ const virtualModuleMap = {
|
|
|
144
142
|
}
|
|
145
143
|
|
|
146
144
|
return ''
|
|
145
|
+
},
|
|
146
|
+
get pages() {
|
|
147
|
+
const pages = state.PAGES_CONTEXT?.pages || []
|
|
148
|
+
return `export const pages = ${serializePagesIndex(pages)}\nexport default pages`
|
|
147
149
|
}
|
|
148
150
|
}
|
|
149
151
|
|
|
@@ -156,20 +158,6 @@ const getSchemeModuleKey = (id) => {
|
|
|
156
158
|
return id.slice(virtualModuleScheme.length)
|
|
157
159
|
}
|
|
158
160
|
|
|
159
|
-
const resolveVirtualModuleId = (id) => {
|
|
160
|
-
if (id.startsWith(virtualModulePrefix)) {
|
|
161
|
-
return id
|
|
162
|
-
}
|
|
163
|
-
const basePrefix = resolveBasePrefix()
|
|
164
|
-
if (basePrefix) {
|
|
165
|
-
const prefixed = `${basePrefix}${virtualModulePrefix}`
|
|
166
|
-
if (id.startsWith(prefixed)) {
|
|
167
|
-
return id.slice(basePrefix.length)
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return null
|
|
171
|
-
}
|
|
172
|
-
|
|
173
161
|
export const methanolResolverPlugin = () => {
|
|
174
162
|
return {
|
|
175
163
|
name: 'methanol-resolver',
|
|
@@ -186,19 +174,16 @@ export const methanolResolverPlugin = () => {
|
|
|
186
174
|
return require.resolve(id)
|
|
187
175
|
}
|
|
188
176
|
|
|
177
|
+
// Very weird workaround for Vite
|
|
178
|
+
if (id.startsWith(virtualModulePrefix)) {
|
|
179
|
+
return resolve(virtualModuleDir, id.slice(virtualModulePrefix.length))
|
|
180
|
+
}
|
|
181
|
+
|
|
189
182
|
const schemeKey = getSchemeModuleKey(id)
|
|
190
183
|
if (schemeKey && Object.prototype.hasOwnProperty.call(virtualModuleMap, schemeKey)) {
|
|
191
184
|
return '\0' + id
|
|
192
185
|
}
|
|
193
186
|
|
|
194
|
-
const virtualId = resolveVirtualModuleId(id)
|
|
195
|
-
if (virtualId) {
|
|
196
|
-
const _moduleId = getModuleIdSegment(virtualId, virtualModulePrefix.length)
|
|
197
|
-
if (Object.prototype.hasOwnProperty.call(virtualModuleMap, _moduleId)) {
|
|
198
|
-
return '\0' + virtualId
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
187
|
if (state.SOURCES.length) {
|
|
203
188
|
const { pathname, search } = new URL(id, 'http://methanol')
|
|
204
189
|
for (const entry of state.SOURCES) {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
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 getCollection = (p) => {
|
|
33
|
+
if (p.collection) return p.collection
|
|
34
|
+
if (!p.dir) return null
|
|
35
|
+
return p.dir.split('/')[0]
|
|
36
|
+
}
|
|
37
|
+
const resolvePosts = () => {
|
|
38
|
+
const list = rawPages.filter(isPostPage)
|
|
39
|
+
list.sort((a, b) => {
|
|
40
|
+
const dateA = new Date(a.frontmatter?.date || a.stats?.createdAt || 0)
|
|
41
|
+
const dateB = new Date(b.frontmatter?.date || b.stats?.createdAt || 0)
|
|
42
|
+
return dateB - dateA
|
|
43
|
+
})
|
|
44
|
+
return list.map((p) => ({
|
|
45
|
+
title: p.title,
|
|
46
|
+
routeHref: p.routeHref,
|
|
47
|
+
frontmatter: p.frontmatter || {},
|
|
48
|
+
stats: p.stats || {},
|
|
49
|
+
excerpt: p.excerpt || p.frontmatter?.excerpt || '',
|
|
50
|
+
collection: getCollection(p)
|
|
51
|
+
}))
|
|
52
|
+
}
|
|
53
|
+
const allPosts = resolvePosts()
|
|
54
|
+
const defaultCollections = Array.from(new Set(allPosts.map((p) => p.collection).filter(Boolean))).sort()
|
|
55
|
+
|
|
56
|
+
export default function ({ collectionTitles = {} } = {}) {
|
|
57
|
+
const currentCollection = signal('')
|
|
58
|
+
const activeCollection = onCondition(currentCollection)
|
|
59
|
+
const visibleCount = signal(10)
|
|
60
|
+
const collectionList = Object.keys(collectionTitles || {}).length
|
|
61
|
+
? Object.keys(collectionTitles).sort()
|
|
62
|
+
: defaultCollections
|
|
63
|
+
const resolveLabel = (value) => collectionTitles?.[value] || value
|
|
64
|
+
|
|
65
|
+
if (typeof window !== 'undefined') {
|
|
66
|
+
const params = new URLSearchParams(window.location.search)
|
|
67
|
+
const col = params.get('collection')
|
|
68
|
+
if (col) currentCollection.value = col
|
|
69
|
+
|
|
70
|
+
window.addEventListener('popstate', () => {
|
|
71
|
+
const p = new URLSearchParams(window.location.search)
|
|
72
|
+
currentCollection.value = p.get('collection') || ''
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const filteredPosts = $(() => {
|
|
77
|
+
const col = currentCollection.value
|
|
78
|
+
if (!col) return allPosts
|
|
79
|
+
return allPosts.filter((p) => p.collection === col)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const visiblePosts = $(() => filteredPosts.value.slice(0, visibleCount.value))
|
|
83
|
+
const hasMore = $(() => visibleCount.value < filteredPosts.value.length)
|
|
84
|
+
const showEmpty = $(() => filteredPosts.value.length === 0)
|
|
85
|
+
|
|
86
|
+
const loadMore = () => {
|
|
87
|
+
visibleCount.value += 10
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const setCollection = (col) => {
|
|
91
|
+
currentCollection.value = col
|
|
92
|
+
visibleCount.value = 10
|
|
93
|
+
const url = new URL(window.location)
|
|
94
|
+
if (col) {
|
|
95
|
+
url.searchParams.set('collection', col)
|
|
96
|
+
} else {
|
|
97
|
+
url.searchParams.delete('collection')
|
|
98
|
+
}
|
|
99
|
+
window.history.pushState({}, '', url)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div class="category-view">
|
|
104
|
+
<div class="category-list">
|
|
105
|
+
<button class="category-tag" class:active={activeCollection('')} on:click={() => setCollection('')}>
|
|
106
|
+
All
|
|
107
|
+
</button>
|
|
108
|
+
<For entries={collectionList}>
|
|
109
|
+
{({ item: col }) => (
|
|
110
|
+
<button class="category-tag" class:active={activeCollection(col)} on:click={() => setCollection(col)}>
|
|
111
|
+
{resolveLabel(col)}
|
|
112
|
+
</button>
|
|
113
|
+
)}
|
|
114
|
+
</For>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div class="post-list">
|
|
118
|
+
<For entries={visiblePosts}>
|
|
119
|
+
{({ item: p }) => {
|
|
120
|
+
const dateStr = formatDate(p.frontmatter?.date || p.stats?.createdAt)
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<article class="post-item">
|
|
124
|
+
<div class="post-meta">
|
|
125
|
+
{dateStr && <span class="post-date">{dateStr}</span>}
|
|
126
|
+
{p.collection && <span class="post-categories"> · {resolveLabel(p.collection)}</span>}
|
|
127
|
+
</div>
|
|
128
|
+
<h2 class="post-item-title">
|
|
129
|
+
<a href={p.routeHref}>{p.title || 'Untitled'}</a>
|
|
130
|
+
</h2>
|
|
131
|
+
<div class="post-excerpt">{p.frontmatter.excerpt || p.excerpt || 'No excerpt available.'}</div>
|
|
132
|
+
</article>
|
|
133
|
+
)
|
|
134
|
+
}}
|
|
135
|
+
</For>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<If condition={showEmpty}>{() => <p>No posts found in this collection.</p>}</If>
|
|
139
|
+
|
|
140
|
+
<If condition={hasMore}>
|
|
141
|
+
{() => (
|
|
142
|
+
<div class="pagination-container">
|
|
143
|
+
<button class="load-more-btn" on:click={loadMore}>
|
|
144
|
+
Load More
|
|
145
|
+
</button>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
</If>
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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 ({ collectionTitles = {} } = {}, ...children) {
|
|
22
|
+
if (!children.length) return null
|
|
23
|
+
const resolveLabel = (value) => collectionTitles?.[value] || value
|
|
24
|
+
const collections = Object.keys(collectionTitles || {}).sort()
|
|
25
|
+
return (
|
|
26
|
+
<div class="category-view">
|
|
27
|
+
<div class="category-list">
|
|
28
|
+
<button class="category-tag active">All</button>
|
|
29
|
+
{collections.map((col) => (
|
|
30
|
+
<button class="category-tag">{resolveLabel(col)}</button>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="post-list">{...children}</div>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|