methanol 0.0.1 → 0.0.3
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/README.md +58 -0
- package/package.json +10 -1
- package/src/config.js +63 -18
- package/src/dev-server.js +80 -7
- package/src/main.js +51 -1
- package/src/mdx.js +176 -24
- package/src/pagefind.js +10 -2
- package/src/pages.js +149 -10
- package/src/register-loader.js +2 -7
- package/src/rehype-plugins/link-resolve.js +35 -8
- package/src/state.js +27 -3
- package/src/vite-plugins.js +2 -2
- package/themes/default/components/ThemeAccentSwitch.client.jsx +95 -0
- package/themes/default/components/ThemeAccentSwitch.static.jsx +23 -0
- package/themes/default/components/ThemeColorSwitch.client.jsx +1 -1
- package/themes/default/components/ThemeSearchBox.client.jsx +71 -34
- package/themes/default/components/ThemeSearchBox.static.jsx +0 -1
- package/themes/default/components/pre.client.jsx +1 -1
- package/themes/default/components/{pre.jsx → pre.static.jsx} +1 -1
- package/themes/default/index.js +4 -13
- package/themes/default/page.jsx +61 -7
- package/themes/default/pages/index.mdx +24 -2
- package/themes/default/public/favicon.png +0 -0
- package/themes/default/sources/prefetch.js +49 -0
- package/themes/default/{resources → sources}/style.css +600 -29
- package/.editorconfig +0 -19
- package/.prettierrc +0 -10
package/src/state.js
CHANGED
|
@@ -101,6 +101,19 @@ const withCommonOptions = (y) =>
|
|
|
101
101
|
type: 'boolean',
|
|
102
102
|
default: false
|
|
103
103
|
})
|
|
104
|
+
.option('code-highlighting', {
|
|
105
|
+
describe: 'Enable or disable code highlighting',
|
|
106
|
+
type: 'string',
|
|
107
|
+
coerce: (value) => {
|
|
108
|
+
if (value == null) return null
|
|
109
|
+
if (value === true || value === '') return true
|
|
110
|
+
if (typeof value === 'boolean') return value
|
|
111
|
+
const normalized = String(value).trim().toLowerCase()
|
|
112
|
+
if (normalized === 'true') return true
|
|
113
|
+
if (normalized === 'false') return false
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
})
|
|
104
117
|
|
|
105
118
|
const parser = yargs(hideBin(process.argv))
|
|
106
119
|
.scriptName('methanol')
|
|
@@ -127,7 +140,8 @@ export const cli = {
|
|
|
127
140
|
CLI_ASSETS_DIR: argv.assets || null,
|
|
128
141
|
CLI_OUTPUT_DIR: argv.output || null,
|
|
129
142
|
CLI_CONFIG_PATH: argv.config || null,
|
|
130
|
-
CLI_SITE_NAME: argv['site-name'] || null
|
|
143
|
+
CLI_SITE_NAME: argv['site-name'] || null,
|
|
144
|
+
CLI_CODE_HIGHLIGHTING: typeof argv['code-highlighting'] === 'boolean' ? argv['code-highlighting'] : null
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
export const state = {
|
|
@@ -149,10 +163,20 @@ export const state = {
|
|
|
149
163
|
USER_VITE_CONFIG: null,
|
|
150
164
|
USER_MDX_CONFIG: null,
|
|
151
165
|
USER_PUBLIC_OVERRIDE: false,
|
|
152
|
-
|
|
166
|
+
SOURCES: [],
|
|
153
167
|
PAGEFIND_ENABLED: false,
|
|
154
168
|
PAGEFIND_OPTIONS: null,
|
|
155
|
-
|
|
169
|
+
PAGEFIND_BUILD: null,
|
|
170
|
+
USER_PRE_BUILD_HOOKS: [],
|
|
171
|
+
USER_POST_BUILD_HOOKS: [],
|
|
172
|
+
USER_PRE_BUNDLE_HOOKS: [],
|
|
173
|
+
USER_POST_BUNDLE_HOOKS: [],
|
|
174
|
+
THEME_PRE_BUILD_HOOKS: [],
|
|
175
|
+
THEME_POST_BUILD_HOOKS: [],
|
|
176
|
+
THEME_PRE_BUNDLE_HOOKS: [],
|
|
177
|
+
THEME_POST_BUNDLE_HOOKS: [],
|
|
178
|
+
STARRY_NIGHT_ENABLED: false,
|
|
179
|
+
STARRY_NIGHT_OPTIONS: null,
|
|
156
180
|
CURRENT_MODE: 'production',
|
|
157
181
|
RESOLVED_MDX_CONFIG: undefined,
|
|
158
182
|
RESOLVED_VITE_CONFIG: undefined
|
package/src/vite-plugins.js
CHANGED
|
@@ -146,9 +146,9 @@ export const methanolResolverPlugin = () => {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
if (state.
|
|
149
|
+
if (state.SOURCES.length) {
|
|
150
150
|
const { pathname, search } = new URL(id, 'http://methanol')
|
|
151
|
-
for (const entry of state.
|
|
151
|
+
for (const entry of state.SOURCES) {
|
|
152
152
|
const { find, replacement } = entry
|
|
153
153
|
if (!find || !replacement) continue
|
|
154
154
|
if (typeof find === 'string') {
|
|
@@ -0,0 +1,95 @@
|
|
|
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, $ } from 'refui'
|
|
22
|
+
|
|
23
|
+
const ACCENTS = [
|
|
24
|
+
{ id: 'default', label: 'Amber', color: '#ffa000' },
|
|
25
|
+
{ id: 'rose', label: 'Rose', color: '#f43f5e' },
|
|
26
|
+
{ id: 'blue', label: 'Indigo', color: '#818cf8' },
|
|
27
|
+
{ id: 'green', label: 'Teal', color: '#2dd4bf' },
|
|
28
|
+
{ id: 'purple', label: 'Violet', color: '#a78bfa' }
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
export default function () {
|
|
32
|
+
const currentAccent = signal('default')
|
|
33
|
+
const isOpen = signal(false)
|
|
34
|
+
|
|
35
|
+
// Initialize theme from localStorage
|
|
36
|
+
if (typeof window !== 'undefined') {
|
|
37
|
+
const saved = localStorage.getItem('methanol-accent')
|
|
38
|
+
if (saved && ACCENTS.some((a) => a.id === saved)) {
|
|
39
|
+
currentAccent.value = saved
|
|
40
|
+
if (saved !== 'default') {
|
|
41
|
+
document.documentElement.classList.add(`accent-${saved}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Close popup when clicking outside
|
|
46
|
+
document.addEventListener('click', (e) => {
|
|
47
|
+
if (!e.target.closest('.theme-switch-wrapper')) {
|
|
48
|
+
isOpen.value = false
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const setAccent = (id) => {
|
|
54
|
+
const oldId = currentAccent.value
|
|
55
|
+
|
|
56
|
+
// Remove old
|
|
57
|
+
if (oldId !== 'default') {
|
|
58
|
+
document.documentElement.classList.remove(`accent-${oldId}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Add new
|
|
62
|
+
if (id !== 'default') {
|
|
63
|
+
document.documentElement.classList.add(`accent-${id}`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
currentAccent.value = id
|
|
67
|
+
localStorage.setItem('methanol-accent', id)
|
|
68
|
+
isOpen.value = false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const togglePopup = () => {
|
|
72
|
+
isOpen.value = !isOpen.value
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div class="theme-switch-container">
|
|
77
|
+
<div class="theme-switch-wrapper">
|
|
78
|
+
<div class={$(() => `accent-popup ${isOpen.value ? 'open' : ''}`)}>
|
|
79
|
+
{ACCENTS.map((accent) => (
|
|
80
|
+
<button
|
|
81
|
+
class={$(() => `accent-option ${currentAccent.value === accent.id ? 'active' : ''}`)}
|
|
82
|
+
on:click={() => setAccent(accent.id)}
|
|
83
|
+
>
|
|
84
|
+
<span class="option-circle" style={`background-color: ${accent.color}`}></span>
|
|
85
|
+
{accent.label}
|
|
86
|
+
</button>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
<button class="theme-switch-btn" on:click={togglePopup} attr:aria-label="Select accent color">
|
|
90
|
+
<div class="accent-circle"></div>
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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 () {
|
|
22
|
+
// render nothing on server side
|
|
23
|
+
}
|
|
@@ -87,7 +87,7 @@ export default function () {
|
|
|
87
87
|
|
|
88
88
|
return (
|
|
89
89
|
<div class="theme-switch-container">
|
|
90
|
-
<button class="theme-switch-btn" on:click={toggle} aria-label="Toggle theme">
|
|
90
|
+
<button class="theme-switch-btn" on:click={toggle} attr:aria-label="Toggle theme">
|
|
91
91
|
<CurrentIcon />
|
|
92
92
|
</button>
|
|
93
93
|
</div>
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* under the License.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { signal, $, If, For } from 'refui'
|
|
21
|
+
import { signal, $, t, If, For, onCondition } from 'refui'
|
|
22
22
|
import { createPortal } from 'refui/extras'
|
|
23
23
|
import { loadPagefind } from '/.methanol_virtual_module/pagefind.js'
|
|
24
24
|
|
|
@@ -55,7 +55,10 @@ export default function ({ options } = {}) {
|
|
|
55
55
|
const isLoading = signal(false)
|
|
56
56
|
const activeIndex = signal(-1)
|
|
57
57
|
|
|
58
|
+
const buttonRef = signal()
|
|
58
59
|
const inputRef = signal()
|
|
60
|
+
const resultIdPrefix = `search-result-${Math.random().toString(36).slice(2)}`
|
|
61
|
+
const activeMatch = onCondition(activeIndex)
|
|
59
62
|
|
|
60
63
|
let debounceTimer = null
|
|
61
64
|
const shortcutLabel = resolveShortcutLabel()
|
|
@@ -76,7 +79,7 @@ export default function ({ options } = {}) {
|
|
|
76
79
|
const searchResult = await pagefind.search(q)
|
|
77
80
|
if (searchResult?.results?.length) {
|
|
78
81
|
const data = await Promise.all(searchResult.results.slice(0, 10).map((r) => r.data()))
|
|
79
|
-
results.value = data
|
|
82
|
+
results.value = data.map((value) => ({ value, el: signal() }))
|
|
80
83
|
}
|
|
81
84
|
} catch (err) {
|
|
82
85
|
console.error('Search error:', err)
|
|
@@ -117,11 +120,13 @@ export default function ({ options } = {}) {
|
|
|
117
120
|
results.value = []
|
|
118
121
|
activeIndex.value = -1
|
|
119
122
|
if (debounceTimer) clearTimeout(debounceTimer)
|
|
123
|
+
if (inputRef.value) inputRef.value.blur()
|
|
124
|
+
if (buttonRef.value) buttonRef.value.focus()
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
const scrollActiveIntoView = () => {
|
|
123
128
|
setTimeout(() => {
|
|
124
|
-
const activeEl =
|
|
129
|
+
const activeEl = results.value[activeIndex.value]?.el.value
|
|
125
130
|
if (activeEl) {
|
|
126
131
|
activeEl.scrollIntoView({ block: 'nearest' })
|
|
127
132
|
}
|
|
@@ -138,19 +143,21 @@ export default function ({ options } = {}) {
|
|
|
138
143
|
if (event.key === 'ArrowDown') {
|
|
139
144
|
event.preventDefault()
|
|
140
145
|
if (results.value.length > 0) {
|
|
141
|
-
activeIndex.value
|
|
146
|
+
const nextIndex = activeIndex.value >= 0 ? (activeIndex.value + 1) % results.value.length : 0
|
|
147
|
+
activeIndex.value = nextIndex
|
|
142
148
|
scrollActiveIntoView()
|
|
143
149
|
}
|
|
144
150
|
} else if (event.key === 'ArrowUp') {
|
|
145
151
|
event.preventDefault()
|
|
146
152
|
if (results.value.length > 0) {
|
|
147
|
-
activeIndex.value
|
|
153
|
+
const nextIndex = activeIndex.value > 0 ? activeIndex.value - 1 : results.value.length - 1
|
|
154
|
+
activeIndex.value = nextIndex
|
|
148
155
|
scrollActiveIntoView()
|
|
149
156
|
}
|
|
150
157
|
} else if (event.key === 'Enter') {
|
|
151
158
|
event.preventDefault()
|
|
152
|
-
const selected = results.value[activeIndex.value]
|
|
153
|
-
const fallback = results.value[0]
|
|
159
|
+
const selected = results.value[activeIndex.value]?.value
|
|
160
|
+
const fallback = results.value[0]?.value
|
|
154
161
|
const target = selected || fallback
|
|
155
162
|
if (target?.url) {
|
|
156
163
|
window.location.href = target.url
|
|
@@ -159,6 +166,30 @@ export default function ({ options } = {}) {
|
|
|
159
166
|
}
|
|
160
167
|
}
|
|
161
168
|
|
|
169
|
+
const onResultKeyDown = (event, indexValue) => {
|
|
170
|
+
if (event.key === 'Escape') {
|
|
171
|
+
event.preventDefault()
|
|
172
|
+
close()
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
if (event.key === 'ArrowDown') {
|
|
176
|
+
event.preventDefault()
|
|
177
|
+
const nextIndex = (indexValue + 1) % results.value.length
|
|
178
|
+
activeIndex.value = nextIndex
|
|
179
|
+
scrollActiveIntoView()
|
|
180
|
+
} else if (event.key === 'ArrowUp') {
|
|
181
|
+
event.preventDefault()
|
|
182
|
+
if (indexValue === 0) {
|
|
183
|
+
activeIndex.value = -1
|
|
184
|
+
focusInput()
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
const nextIndex = indexValue - 1
|
|
188
|
+
activeIndex.value = nextIndex
|
|
189
|
+
scrollActiveIntoView()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
162
193
|
const showEmpty = $(() => !query.value)
|
|
163
194
|
const showNoResults = $(() => {
|
|
164
195
|
const _query = query.value
|
|
@@ -192,28 +223,24 @@ export default function ({ options } = {}) {
|
|
|
192
223
|
return (R) => {
|
|
193
224
|
R.render(document.body, Outlet)
|
|
194
225
|
return (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
226
|
+
<button class="search-box" type="button" on:click={open} attr:aria-label="Open search" $ref={buttonRef}>
|
|
227
|
+
<svg
|
|
228
|
+
attr:width="16"
|
|
229
|
+
attr:height="16"
|
|
230
|
+
attr:viewBox="0 0 24 24"
|
|
231
|
+
attr:fill="none"
|
|
232
|
+
attr:stroke="currentColor"
|
|
233
|
+
attr:stroke-width="2"
|
|
234
|
+
attr:stroke-linecap="round"
|
|
235
|
+
attr:stroke-linejoin="round"
|
|
236
|
+
>
|
|
237
|
+
<circle attr:cx="11" attr:cy="11" attr:r="8"></circle>
|
|
238
|
+
<path attr:d="m21 21-4.3-4.3"></path>
|
|
239
|
+
</svg>
|
|
240
|
+
<span>Search</span>
|
|
241
|
+
<kbd>{shortcutLabel}</kbd>
|
|
211
242
|
<Inlet>
|
|
212
|
-
<div
|
|
213
|
-
class="search-modal"
|
|
214
|
-
class:open={isOpen}
|
|
215
|
-
attr:aria-hidden={$(() => (isOpen.value ? null : 'true'))}
|
|
216
|
-
>
|
|
243
|
+
<div class="search-modal" class:open={isOpen} attr:inert={$(() => (isOpen.value ? null : ''))}>
|
|
217
244
|
<div class="search-modal__scrim" on:click={close}></div>
|
|
218
245
|
<div class="search-modal__panel">
|
|
219
246
|
<div class="search-input-wrapper">
|
|
@@ -237,6 +264,9 @@ export default function ({ options } = {}) {
|
|
|
237
264
|
value={query}
|
|
238
265
|
on:input={onInput}
|
|
239
266
|
on:keydown={onKeyDown}
|
|
267
|
+
attr:aria-activedescendant={$(() =>
|
|
268
|
+
activeIndex.value >= 0 ? `${resultIdPrefix}-${activeIndex.value}` : null
|
|
269
|
+
)}
|
|
240
270
|
attr:autocomplete="off"
|
|
241
271
|
attr:autocorrect="off"
|
|
242
272
|
attr:spellcheck="false"
|
|
@@ -249,13 +279,20 @@ export default function ({ options } = {}) {
|
|
|
249
279
|
{() => <div class="search-status">No results found for "{query}"</div>}
|
|
250
280
|
</If>
|
|
251
281
|
<If condition={isLoading}>{() => <div class="search-status">Searching...</div>}</If>
|
|
252
|
-
<For entries={results}
|
|
253
|
-
{({ item, index }) => (
|
|
282
|
+
<For entries={results} indexed>
|
|
283
|
+
{({ item: { value, el }, index }) => (
|
|
254
284
|
<a
|
|
255
285
|
class="search-result-item"
|
|
256
|
-
|
|
286
|
+
class:active={activeMatch(index)}
|
|
287
|
+
href={value.url}
|
|
257
288
|
on:click={close}
|
|
289
|
+
on:keydown={(event) => onResultKeyDown(event, index.value)}
|
|
290
|
+
on:focus={() => {
|
|
291
|
+
activeIndex.value = index.value
|
|
292
|
+
}}
|
|
258
293
|
attr:aria-selected={$(() => (activeIndex.value === index.value ? 'true' : 'false'))}
|
|
294
|
+
attr:id={t`${resultIdPrefix}-${index.value}`}
|
|
295
|
+
$ref={el}
|
|
259
296
|
>
|
|
260
297
|
<div class="search-result-title">
|
|
261
298
|
<svg
|
|
@@ -271,9 +308,9 @@ export default function ({ options } = {}) {
|
|
|
271
308
|
<path attr:d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
272
309
|
<polyline attr:points="14 2 14 8 20 8"></polyline>
|
|
273
310
|
</svg>
|
|
274
|
-
{
|
|
311
|
+
{value?.meta?.title || value?.title || value?.url}
|
|
275
312
|
</div>
|
|
276
|
-
<div class="search-result-excerpt" innerHTML={
|
|
313
|
+
<div class="search-result-excerpt" innerHTML={value.excerpt || ''}></div>
|
|
277
314
|
</a>
|
|
278
315
|
)}
|
|
279
316
|
</For>
|
|
@@ -281,7 +318,7 @@ export default function ({ options } = {}) {
|
|
|
281
318
|
</div>
|
|
282
319
|
</div>
|
|
283
320
|
</Inlet>
|
|
284
|
-
|
|
321
|
+
</button>
|
|
285
322
|
)
|
|
286
323
|
}
|
|
287
324
|
}
|
|
@@ -73,7 +73,7 @@ export default function (props, ...children) {
|
|
|
73
73
|
|
|
74
74
|
return (
|
|
75
75
|
<div class="code-block-container">
|
|
76
|
-
<button class="copy-btn" on:click={copy} aria-label="Copy code">
|
|
76
|
+
<button class="copy-btn" on:click={copy} attr:aria-label="Copy code">
|
|
77
77
|
<Btn />
|
|
78
78
|
</button>
|
|
79
79
|
<pre {...props} $ref={el}>
|
package/themes/default/index.js
CHANGED
|
@@ -21,30 +21,21 @@
|
|
|
21
21
|
import { readFileSync } from 'fs'
|
|
22
22
|
import { fileURLToPath } from 'url'
|
|
23
23
|
import { dirname, resolve } from 'path'
|
|
24
|
-
import rehypeStarryNight from 'rehype-starry-night'
|
|
25
|
-
|
|
26
24
|
import PAGE_TEMPLATE from './page.jsx'
|
|
27
25
|
import { createHeadings } from './heading.jsx'
|
|
28
26
|
|
|
29
27
|
const __filename = fileURLToPath(import.meta.url)
|
|
30
28
|
const __dirname = dirname(__filename)
|
|
31
29
|
|
|
32
|
-
export default (
|
|
30
|
+
export default () => {
|
|
33
31
|
return {
|
|
34
32
|
root: __dirname,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
resources: {
|
|
38
|
-
'/.methanol_theme_default': './resources'
|
|
33
|
+
sources: {
|
|
34
|
+
'/.methanol_theme_default': './sources'
|
|
39
35
|
},
|
|
40
36
|
components: {
|
|
41
37
|
...createHeadings()
|
|
42
38
|
},
|
|
43
|
-
template: PAGE_TEMPLATE
|
|
44
|
-
mdx: {
|
|
45
|
-
rehypePlugins: [
|
|
46
|
-
starryNight && [rehypeStarryNight, starryNightOptions]
|
|
47
|
-
].filter(Boolean)
|
|
48
|
-
}
|
|
39
|
+
template: PAGE_TEMPLATE
|
|
49
40
|
}
|
|
50
41
|
}
|
package/themes/default/page.jsx
CHANGED
|
@@ -78,13 +78,14 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
|
|
|
78
78
|
const hasToc = Boolean(toc)
|
|
79
79
|
const layoutClass = hasToc ? 'layout-container' : 'layout-container no-toc'
|
|
80
80
|
const tree = renderPageTree(pagesTree, currentRoute, 0)
|
|
81
|
-
const { ThemeSearchBox, ThemeColorSwitch, ThemeToCContainer } = components
|
|
81
|
+
const { ThemeSearchBox, ThemeColorSwitch, ThemeAccentSwitch, ThemeToCContainer } = components
|
|
82
82
|
const rootPage = pagesByRoute?.get?.('/') || pages.find((entry) => entry.routePath === '/')
|
|
83
83
|
const pageFrontmatter = page?.frontmatter || {}
|
|
84
84
|
const rootFrontmatter = rootPage?.frontmatter || {}
|
|
85
85
|
const themeLogo = '/logo.png'
|
|
86
|
+
const themeFavIcon = '/favicon.png'
|
|
86
87
|
const logo = pageFrontmatter.logo ?? rootFrontmatter.logo ?? ctx.site?.logo ?? themeLogo
|
|
87
|
-
const favicon = pageFrontmatter.favicon ?? rootFrontmatter.favicon ?? ctx.site?.favicon ??
|
|
88
|
+
const favicon = pageFrontmatter.favicon ?? rootFrontmatter.favicon ?? ctx.site?.favicon ?? themeFavIcon
|
|
88
89
|
const excerpt = pageFrontmatter.excerpt ?? null
|
|
89
90
|
const ogTitle = pageFrontmatter.ogTitle ?? null
|
|
90
91
|
const ogDescription = pageFrontmatter.ogDescription ?? null
|
|
@@ -94,8 +95,14 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
|
|
|
94
95
|
const twitterDescription = pageFrontmatter.twitterDescription ?? ogDescription ?? excerpt
|
|
95
96
|
const twitterImage = pageFrontmatter.twitterImage ?? ogImage
|
|
96
97
|
const twitterCard = pageFrontmatter.twitterCard ?? (twitterImage ? 'summary_large_image' : null)
|
|
98
|
+
const siblings = typeof page?.getSiblings === 'function' ? page.getSiblings() : null
|
|
99
|
+
const prevPage = siblings?.prev || null
|
|
100
|
+
const nextPage = siblings?.next || null
|
|
97
101
|
const languages = Array.isArray(ctx.languages) ? ctx.languages : []
|
|
98
102
|
const currentLanguageHref = ctx.language?.href || ctx.language?.routePath || null
|
|
103
|
+
const languageCode =
|
|
104
|
+
pageFrontmatter.langCode ?? rootFrontmatter.langCode ?? ctx.language?.code ?? 'en'
|
|
105
|
+
const htmlLang = typeof languageCode === 'string' && languageCode.trim() ? languageCode : 'en'
|
|
99
106
|
const pagefindEnabled = ctx.site?.pagefind?.enabled !== false
|
|
100
107
|
const pagefindOptions = ctx.site?.pagefind?.options || null
|
|
101
108
|
const languageSelector = languages.length ? (
|
|
@@ -137,7 +144,7 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
|
|
|
137
144
|
return (
|
|
138
145
|
<>
|
|
139
146
|
{R.rawHTML`<!DOCTYPE html>`}
|
|
140
|
-
<html lang=
|
|
147
|
+
<html lang={htmlLang}>
|
|
141
148
|
<head>
|
|
142
149
|
<meta charset="UTF-8" />
|
|
143
150
|
<meta name="viewport" content="width=device-width" />
|
|
@@ -155,6 +162,7 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
|
|
|
155
162
|
{twitterTitle ? <meta name="twitter:title" content={twitterTitle} /> : null}
|
|
156
163
|
{twitterDescription ? <meta name="twitter:description" content={twitterDescription} /> : null}
|
|
157
164
|
{twitterImage ? <meta name="twitter:image" content={twitterImage} /> : null}
|
|
165
|
+
<ExtraHead />
|
|
158
166
|
<link rel="preload stylesheet" as="style" href="/.methanol_theme_default/style.css" />
|
|
159
167
|
{R.rawHTML`
|
|
160
168
|
<script>
|
|
@@ -164,10 +172,15 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
|
|
|
164
172
|
const theme = savedTheme || systemTheme;
|
|
165
173
|
document.documentElement.classList.toggle('light', theme === 'light');
|
|
166
174
|
document.documentElement.classList.toggle('dark', theme === 'dark');
|
|
175
|
+
|
|
176
|
+
const savedAccent = localStorage.getItem('methanol-accent');
|
|
177
|
+
if (savedAccent && savedAccent !== 'default') {
|
|
178
|
+
document.documentElement.classList.add('accent-' + savedAccent);
|
|
179
|
+
}
|
|
167
180
|
})();
|
|
168
181
|
</script>
|
|
169
182
|
`}
|
|
170
|
-
<
|
|
183
|
+
<script type="module" src="/.methanol_theme_default/prefetch.js" defer></script>
|
|
171
184
|
</head>
|
|
172
185
|
<body>
|
|
173
186
|
<input type="checkbox" id="nav-toggle" class="nav-toggle" />
|
|
@@ -187,6 +200,27 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
|
|
|
187
200
|
<line x1="3" y1="18" x2="21" y2="18"></line>
|
|
188
201
|
</svg>
|
|
189
202
|
</label>
|
|
203
|
+
{pagefindEnabled ? (
|
|
204
|
+
<button
|
|
205
|
+
class="search-toggle-label"
|
|
206
|
+
aria-label="Open search"
|
|
207
|
+
onclick="window.__methanolSearchOpen()"
|
|
208
|
+
>
|
|
209
|
+
<svg
|
|
210
|
+
width="24"
|
|
211
|
+
height="24"
|
|
212
|
+
viewBox="0 0 24 24"
|
|
213
|
+
fill="none"
|
|
214
|
+
stroke="currentColor"
|
|
215
|
+
stroke-width="2"
|
|
216
|
+
stroke-linecap="round"
|
|
217
|
+
stroke-linejoin="round"
|
|
218
|
+
>
|
|
219
|
+
<circle cx="11" cy="11" r="8"></circle>
|
|
220
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
221
|
+
</svg>
|
|
222
|
+
</button>
|
|
223
|
+
) : null}
|
|
190
224
|
{hasToc ? (
|
|
191
225
|
<>
|
|
192
226
|
<input type="checkbox" id="toc-toggle" class="toc-toggle" />
|
|
@@ -226,15 +260,35 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
|
|
|
226
260
|
<div class="sidebar-footer">
|
|
227
261
|
{languageSelector}
|
|
228
262
|
<ThemeColorSwitch />
|
|
263
|
+
<ThemeAccentSwitch />
|
|
229
264
|
</div>
|
|
230
265
|
</aside>
|
|
231
266
|
<main class="main-content" data-pagefind-body={pagefindEnabled ? '' : null}>
|
|
232
267
|
<Page />
|
|
268
|
+
{prevPage || nextPage ? (
|
|
269
|
+
<nav class="page-nav">
|
|
270
|
+
{prevPage ? (
|
|
271
|
+
<a class="page-nav-card prev" href={prevPage.routeHref || prevPage.routePath}>
|
|
272
|
+
<span class="page-nav-label">Previous</span>
|
|
273
|
+
<span class="page-nav-title">{prevPage.title || prevPage.routePath}</span>
|
|
274
|
+
</a>
|
|
275
|
+
) : <div class="page-nav-spacer"></div>}
|
|
276
|
+
{nextPage ? (
|
|
277
|
+
<a class="page-nav-card next" href={nextPage.routeHref || nextPage.routePath}>
|
|
278
|
+
<span class="page-nav-label">Next</span>
|
|
279
|
+
<span class="page-nav-title">{nextPage.title || nextPage.routePath}</span>
|
|
280
|
+
</a>
|
|
281
|
+
) : null}
|
|
282
|
+
</nav>
|
|
283
|
+
) : null}
|
|
233
284
|
{page ? (
|
|
234
285
|
<footer class="page-meta">
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
286
|
+
<div class="page-meta-item">
|
|
287
|
+
Updated: {page.updatedAt || '-'}
|
|
288
|
+
</div>
|
|
289
|
+
<div class="page-meta-item">
|
|
290
|
+
Powered by <a href="https://github.com/SudoMaker/Methanol" target="_blank" rel="noopener noreferrer" class="methanol-link">Methanol</a>
|
|
291
|
+
</div>
|
|
238
292
|
</footer>
|
|
239
293
|
) : null}
|
|
240
294
|
</main>
|
|
@@ -4,6 +4,28 @@ title: Welcome
|
|
|
4
4
|
|
|
5
5
|
# Welcome to Methanol
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Methanol turns your content folder into a static site with rEFui and MDX, file-based routing, and a fast dev server.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx methanol dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Open `http://localhost:5173`.
|
|
16
|
+
|
|
17
|
+
## Your Content Folder
|
|
18
|
+
|
|
19
|
+
Pages are currently read from the configured `pagesDir`:
|
|
20
|
+
|
|
21
|
+
<p><code>{ctx.site?.pagesDir || 'pages/'}</code></p>
|
|
22
|
+
|
|
23
|
+
You can change this with `pagesDir` in your config or `--input` on the CLI. Create an `index.mdx` in that folder to replace this page.
|
|
24
|
+
|
|
25
|
+
## Common Folders
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
components/ # JSX/TSX components for MDX
|
|
29
|
+
public/ # static assets copied as-is
|
|
30
|
+
dist/ # production output
|
|
31
|
+
```
|
|
Binary file
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
(() => {
|
|
22
|
+
const prefetched = new Set()
|
|
23
|
+
const canPrefetch = (anchor) => {
|
|
24
|
+
if (!anchor || !anchor.href) return false
|
|
25
|
+
if (anchor.dataset && anchor.dataset.prefetch === 'false') return false
|
|
26
|
+
if (anchor.hasAttribute('download')) return false
|
|
27
|
+
if (anchor.target && anchor.target !== '_self') return false
|
|
28
|
+
const url = new URL(anchor.href, window.location.href)
|
|
29
|
+
if (url.origin !== window.location.origin) return false
|
|
30
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return false
|
|
31
|
+
if (url.pathname === window.location.pathname && url.search === window.location.search) {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
const onHover = (event) => {
|
|
37
|
+
const anchor = event.target && event.target.closest ? event.target.closest('a') : null
|
|
38
|
+
if (!canPrefetch(anchor)) return
|
|
39
|
+
const href = anchor.href
|
|
40
|
+
if (prefetched.has(href)) return
|
|
41
|
+
prefetched.add(href)
|
|
42
|
+
const link = document.createElement('link')
|
|
43
|
+
link.rel = 'prefetch'
|
|
44
|
+
link.as = 'document'
|
|
45
|
+
link.href = href
|
|
46
|
+
document.head.appendChild(link)
|
|
47
|
+
}
|
|
48
|
+
document.addEventListener('pointerover', onHover, { capture: true, passive: true })
|
|
49
|
+
})()
|