methanol 0.0.0 → 0.0.1
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/.editorconfig +19 -0
- package/.prettierrc +10 -0
- package/LICENSE +203 -0
- package/banner.txt +6 -0
- package/bin/methanol.js +24 -0
- package/index.js +22 -0
- package/package.json +42 -9
- package/src/assets.js +30 -0
- package/src/build-system.js +200 -0
- package/src/components.js +145 -0
- package/src/config.js +355 -0
- package/src/dev-server.js +559 -0
- package/src/main.js +87 -0
- package/src/mdx.js +254 -0
- package/src/node-loader.js +88 -0
- package/src/pagefind.js +99 -0
- package/src/pages.js +638 -0
- package/src/preview-server.js +58 -0
- package/src/public-assets.js +73 -0
- package/src/register-loader.js +29 -0
- package/src/rehype-plugins/link-resolve.js +89 -0
- package/src/rehype-plugins/methanol-ctx.js +89 -0
- package/src/renderer.js +25 -0
- package/src/rewind.js +117 -0
- package/src/stage-logger.js +59 -0
- package/src/state.js +159 -0
- package/src/virtual-module/inject.js +30 -0
- package/src/virtual-module/loader.js +116 -0
- package/src/virtual-module/pagefind.js +108 -0
- package/src/vite-plugins.js +173 -0
- package/themes/default/components/ThemeColorSwitch.client.jsx +95 -0
- package/themes/default/components/ThemeColorSwitch.static.jsx +23 -0
- package/themes/default/components/ThemeSearchBox.client.jsx +287 -0
- package/themes/default/components/ThemeSearchBox.static.jsx +41 -0
- package/themes/default/components/ThemeToCContainer.client.jsx +154 -0
- package/themes/default/components/ThemeToCContainer.static.jsx +61 -0
- package/themes/default/components/pre.client.jsx +84 -0
- package/themes/default/components/pre.jsx +27 -0
- package/themes/default/heading.jsx +35 -0
- package/themes/default/index.js +50 -0
- package/themes/default/page.jsx +249 -0
- package/themes/default/pages/404.mdx +8 -0
- package/themes/default/pages/index.mdx +9 -0
- package/themes/default/public/logo.png +0 -0
- package/themes/default/resources/style.css +1089 -0
|
@@ -0,0 +1,287 @@
|
|
|
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, $, If, For } from 'refui'
|
|
22
|
+
import { createPortal } from 'refui/extras'
|
|
23
|
+
import { loadPagefind } from '/.methanol_virtual_module/pagefind.js'
|
|
24
|
+
|
|
25
|
+
let keybindReady = false
|
|
26
|
+
let cachedPagefind = null
|
|
27
|
+
|
|
28
|
+
const resolveShortcutLabel = () => {
|
|
29
|
+
if (typeof navigator === 'undefined') return 'Ctrl+K'
|
|
30
|
+
const platform = navigator.platform || ''
|
|
31
|
+
const agent = navigator.userAgent || ''
|
|
32
|
+
const isMac = /Mac|iPhone|iPad|iPod/.test(platform) || /Mac OS X/.test(agent)
|
|
33
|
+
return isMac ? '⌘K' : 'Ctrl+K'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ensurePagefind = async (options) => {
|
|
37
|
+
if (cachedPagefind) return cachedPagefind
|
|
38
|
+
const pagefind = await loadPagefind()
|
|
39
|
+
if (!pagefind) return null
|
|
40
|
+
if (pagefind.options) {
|
|
41
|
+
const nextOptions = { excerptLength: 30, ...(options || {}) }
|
|
42
|
+
await pagefind.options(nextOptions)
|
|
43
|
+
}
|
|
44
|
+
if (pagefind.init) {
|
|
45
|
+
await pagefind.init()
|
|
46
|
+
}
|
|
47
|
+
cachedPagefind = pagefind
|
|
48
|
+
return pagefind
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function ({ options } = {}) {
|
|
52
|
+
const isOpen = signal(false)
|
|
53
|
+
const query = signal('')
|
|
54
|
+
const results = signal([])
|
|
55
|
+
const isLoading = signal(false)
|
|
56
|
+
const activeIndex = signal(-1)
|
|
57
|
+
|
|
58
|
+
const inputRef = signal()
|
|
59
|
+
|
|
60
|
+
let debounceTimer = null
|
|
61
|
+
const shortcutLabel = resolveShortcutLabel()
|
|
62
|
+
const [Inlet, Outlet] = createPortal()
|
|
63
|
+
|
|
64
|
+
const search = async (q) => {
|
|
65
|
+
isLoading.value = true
|
|
66
|
+
results.value = []
|
|
67
|
+
activeIndex.value = -1
|
|
68
|
+
|
|
69
|
+
const pagefind = await ensurePagefind(options)
|
|
70
|
+
if (!pagefind) {
|
|
71
|
+
isLoading.value = false
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const searchResult = await pagefind.search(q)
|
|
77
|
+
if (searchResult?.results?.length) {
|
|
78
|
+
const data = await Promise.all(searchResult.results.slice(0, 10).map((r) => r.data()))
|
|
79
|
+
results.value = data
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error('Search error:', err)
|
|
83
|
+
} finally {
|
|
84
|
+
isLoading.value = false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const onInput = (event) => {
|
|
89
|
+
const value = event.target.value
|
|
90
|
+
query.value = value
|
|
91
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
92
|
+
|
|
93
|
+
if (!value.trim()) {
|
|
94
|
+
results.value = []
|
|
95
|
+
activeIndex.value = -1
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
debounceTimer = setTimeout(() => {
|
|
100
|
+
search(value)
|
|
101
|
+
}, 300)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const focusInput = () => {
|
|
105
|
+
if (inputRef.value) inputRef.value.focus()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const open = async () => {
|
|
109
|
+
isOpen.value = true
|
|
110
|
+
setTimeout(focusInput, 50)
|
|
111
|
+
await ensurePagefind(options)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const close = () => {
|
|
115
|
+
isOpen.value = false
|
|
116
|
+
query.value = ''
|
|
117
|
+
results.value = []
|
|
118
|
+
activeIndex.value = -1
|
|
119
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const scrollActiveIntoView = () => {
|
|
123
|
+
setTimeout(() => {
|
|
124
|
+
const activeEl = document.querySelector('.search-result-item[aria-selected="true"]')
|
|
125
|
+
if (activeEl) {
|
|
126
|
+
activeEl.scrollIntoView({ block: 'nearest' })
|
|
127
|
+
}
|
|
128
|
+
}, 0)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const onKeyDown = (event) => {
|
|
132
|
+
if (event.key === 'Escape') {
|
|
133
|
+
event.preventDefault()
|
|
134
|
+
close()
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (event.key === 'ArrowDown') {
|
|
139
|
+
event.preventDefault()
|
|
140
|
+
if (results.value.length > 0) {
|
|
141
|
+
activeIndex.value = (activeIndex.value + 1) % results.value.length
|
|
142
|
+
scrollActiveIntoView()
|
|
143
|
+
}
|
|
144
|
+
} else if (event.key === 'ArrowUp') {
|
|
145
|
+
event.preventDefault()
|
|
146
|
+
if (results.value.length > 0) {
|
|
147
|
+
activeIndex.value = (activeIndex.value - 1 + results.value.length) % results.value.length
|
|
148
|
+
scrollActiveIntoView()
|
|
149
|
+
}
|
|
150
|
+
} else if (event.key === 'Enter') {
|
|
151
|
+
event.preventDefault()
|
|
152
|
+
const selected = results.value[activeIndex.value]
|
|
153
|
+
const fallback = results.value[0]
|
|
154
|
+
const target = selected || fallback
|
|
155
|
+
if (target?.url) {
|
|
156
|
+
window.location.href = target.url
|
|
157
|
+
close()
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const showEmpty = $(() => !query.value)
|
|
163
|
+
const showNoResults = $(() => {
|
|
164
|
+
const _query = query.value
|
|
165
|
+
const _isLoading = isLoading.value
|
|
166
|
+
const _length = results.value.length
|
|
167
|
+
return _query && !_isLoading && _length === 0
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
if (typeof window !== 'undefined') {
|
|
171
|
+
window.__methanolSearchOpen = open
|
|
172
|
+
window.__methanolSearchClose = close
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (typeof window !== 'undefined' && !keybindReady) {
|
|
176
|
+
keybindReady = true
|
|
177
|
+
window.addEventListener('keydown', (event) => {
|
|
178
|
+
const key = event.key?.toLowerCase?.()
|
|
179
|
+
if ((event.metaKey || event.ctrlKey) && key === 'k') {
|
|
180
|
+
event.preventDefault()
|
|
181
|
+
if (isOpen.value) {
|
|
182
|
+
close()
|
|
183
|
+
} else {
|
|
184
|
+
open()
|
|
185
|
+
}
|
|
186
|
+
} else if (key === 'escape' && isOpen.value) {
|
|
187
|
+
close()
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return (R) => {
|
|
193
|
+
R.render(document.body, Outlet)
|
|
194
|
+
return (
|
|
195
|
+
<button class="search-box" type="button" on:click={open} aria-label="Open search">
|
|
196
|
+
<svg
|
|
197
|
+
attr:width="16"
|
|
198
|
+
attr:height="16"
|
|
199
|
+
attr:viewBox="0 0 24 24"
|
|
200
|
+
attr:fill="none"
|
|
201
|
+
attr:stroke="currentColor"
|
|
202
|
+
attr:stroke-width="2"
|
|
203
|
+
attr:stroke-linecap="round"
|
|
204
|
+
attr:stroke-linejoin="round"
|
|
205
|
+
>
|
|
206
|
+
<circle attr:cx="11" attr:cy="11" attr:r="8"></circle>
|
|
207
|
+
<path attr:d="m21 21-4.3-4.3"></path>
|
|
208
|
+
</svg>
|
|
209
|
+
<span>Search</span>
|
|
210
|
+
<kbd>{shortcutLabel}</kbd>
|
|
211
|
+
<Inlet>
|
|
212
|
+
<div
|
|
213
|
+
class="search-modal"
|
|
214
|
+
class:open={isOpen}
|
|
215
|
+
attr:aria-hidden={$(() => (isOpen.value ? null : 'true'))}
|
|
216
|
+
>
|
|
217
|
+
<div class="search-modal__scrim" on:click={close}></div>
|
|
218
|
+
<div class="search-modal__panel">
|
|
219
|
+
<div class="search-input-wrapper">
|
|
220
|
+
<svg
|
|
221
|
+
attr:width="20"
|
|
222
|
+
attr:height="20"
|
|
223
|
+
attr:viewBox="0 0 24 24"
|
|
224
|
+
attr:fill="none"
|
|
225
|
+
attr:stroke="currentColor"
|
|
226
|
+
attr:stroke-width="2"
|
|
227
|
+
attr:stroke-linecap="round"
|
|
228
|
+
attr:stroke-linejoin="round"
|
|
229
|
+
>
|
|
230
|
+
<circle attr:cx="11" attr:cy="11" attr:r="8"></circle>
|
|
231
|
+
<path attr:d="m21 21-4.3-4.3"></path>
|
|
232
|
+
</svg>
|
|
233
|
+
<input
|
|
234
|
+
class="search-input"
|
|
235
|
+
type="text"
|
|
236
|
+
placeholder="Search documentation..."
|
|
237
|
+
value={query}
|
|
238
|
+
on:input={onInput}
|
|
239
|
+
on:keydown={onKeyDown}
|
|
240
|
+
attr:autocomplete="off"
|
|
241
|
+
attr:autocorrect="off"
|
|
242
|
+
attr:spellcheck="false"
|
|
243
|
+
$ref={inputRef}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
246
|
+
<div class="search-results">
|
|
247
|
+
<If condition={showEmpty}>{() => <div class="search-status">Type to start searching...</div>}</If>
|
|
248
|
+
<If condition={showNoResults}>
|
|
249
|
+
{() => <div class="search-status">No results found for "{query}"</div>}
|
|
250
|
+
</If>
|
|
251
|
+
<If condition={isLoading}>{() => <div class="search-status">Searching...</div>}</If>
|
|
252
|
+
<For entries={results} track="url" indexed={true}>
|
|
253
|
+
{({ item, index }) => (
|
|
254
|
+
<a
|
|
255
|
+
class="search-result-item"
|
|
256
|
+
href={item.url}
|
|
257
|
+
on:click={close}
|
|
258
|
+
attr:aria-selected={$(() => (activeIndex.value === index.value ? 'true' : 'false'))}
|
|
259
|
+
>
|
|
260
|
+
<div class="search-result-title">
|
|
261
|
+
<svg
|
|
262
|
+
attr:width="14"
|
|
263
|
+
attr:height="14"
|
|
264
|
+
attr:viewBox="0 0 24 24"
|
|
265
|
+
attr:fill="none"
|
|
266
|
+
attr:stroke="currentColor"
|
|
267
|
+
attr:stroke-width="2"
|
|
268
|
+
attr:stroke-linecap="round"
|
|
269
|
+
attr:stroke-linejoin="round"
|
|
270
|
+
>
|
|
271
|
+
<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
|
+
<polyline attr:points="14 2 14 8 20 8"></polyline>
|
|
273
|
+
</svg>
|
|
274
|
+
{item?.meta?.title || item?.title || item?.url}
|
|
275
|
+
</div>
|
|
276
|
+
<div class="search-result-excerpt" innerHTML={item.excerpt || ''}></div>
|
|
277
|
+
</a>
|
|
278
|
+
)}
|
|
279
|
+
</For>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</Inlet>
|
|
284
|
+
</button>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
return (
|
|
23
|
+
<button class="search-box" type="button" aria-label="Open search">
|
|
24
|
+
<svg
|
|
25
|
+
width="16"
|
|
26
|
+
height="16"
|
|
27
|
+
viewBox="0 0 24 24"
|
|
28
|
+
fill="none"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
stroke-width="2"
|
|
31
|
+
stroke-linecap="round"
|
|
32
|
+
stroke-linejoin="round"
|
|
33
|
+
>
|
|
34
|
+
<circle cx="11" cy="11" r="8"></circle>
|
|
35
|
+
<path d="m21 21-4.3-4.3"></path>
|
|
36
|
+
</svg>
|
|
37
|
+
<span>Search</span>
|
|
38
|
+
<kbd>Ctrl+K</kbd>
|
|
39
|
+
</button>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
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, t, useEffect } from 'refui'
|
|
22
|
+
|
|
23
|
+
export default function (props, ...children) {
|
|
24
|
+
if (!children.length) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const el = signal()
|
|
29
|
+
const top = signal(0)
|
|
30
|
+
const height = signal(0)
|
|
31
|
+
const opacity = signal(0)
|
|
32
|
+
|
|
33
|
+
const updateActive = () => {
|
|
34
|
+
if (!el.value) return
|
|
35
|
+
|
|
36
|
+
const links = Array.from(el.value.querySelectorAll('a'))
|
|
37
|
+
if (!links.length) return
|
|
38
|
+
|
|
39
|
+
// Map links to their corresponding content anchors
|
|
40
|
+
const anchors = links.map(link => {
|
|
41
|
+
const href = link.getAttribute('href')
|
|
42
|
+
if (!href || !href.startsWith('#')) return null
|
|
43
|
+
return document.getElementById(href.slice(1))
|
|
44
|
+
}).filter(Boolean)
|
|
45
|
+
|
|
46
|
+
if (!anchors.length) return
|
|
47
|
+
|
|
48
|
+
const scrollY = window.scrollY
|
|
49
|
+
const offset = 100 // Header offset
|
|
50
|
+
|
|
51
|
+
// Find all sections that are visible in the viewport
|
|
52
|
+
const visibleAnchors = new Set()
|
|
53
|
+
const windowHeight = window.innerHeight
|
|
54
|
+
const threshold = 100 // Header offset/buffer
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < anchors.length; i++) {
|
|
57
|
+
const anchor = anchors[i]
|
|
58
|
+
const nextAnchor = anchors[i + 1]
|
|
59
|
+
|
|
60
|
+
const sectionStart = anchor.offsetTop - threshold
|
|
61
|
+
const sectionEnd = nextAnchor ? nextAnchor.offsetTop - threshold : document.body.offsetHeight
|
|
62
|
+
|
|
63
|
+
// A section is visible if its range overlaps with the viewport [scrollY, scrollY + windowHeight]
|
|
64
|
+
const isVisible = sectionStart < (scrollY + windowHeight - threshold) && sectionEnd > scrollY
|
|
65
|
+
|
|
66
|
+
if (isVisible) {
|
|
67
|
+
visibleAnchors.add(anchor)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Fallback: if somehow nothing is found, at least highlight the first one
|
|
72
|
+
if (visibleAnchors.size === 0 && anchors.length > 0) {
|
|
73
|
+
visibleAnchors.add(anchors[0])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Update active class on links and find active range
|
|
77
|
+
let firstActiveLink = null
|
|
78
|
+
let lastActiveLink = null
|
|
79
|
+
|
|
80
|
+
links.forEach(l => {
|
|
81
|
+
const href = l.getAttribute('href')
|
|
82
|
+
const anchorId = href ? href.slice(1) : null
|
|
83
|
+
const anchor = anchors.find(a => a.id === anchorId)
|
|
84
|
+
if (visibleAnchors.has(anchor)) {
|
|
85
|
+
l.classList.add('active')
|
|
86
|
+
if (!firstActiveLink) firstActiveLink = l
|
|
87
|
+
lastActiveLink = l
|
|
88
|
+
} else {
|
|
89
|
+
l.classList.remove('active')
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Update indicator position
|
|
94
|
+
if (firstActiveLink && lastActiveLink) {
|
|
95
|
+
const containerRect = el.value.getBoundingClientRect()
|
|
96
|
+
const firstRect = firstActiveLink.getBoundingClientRect()
|
|
97
|
+
const lastRect = lastActiveLink.getBoundingClientRect()
|
|
98
|
+
|
|
99
|
+
const currentTop = firstRect.top - containerRect.top + el.value.scrollTop
|
|
100
|
+
const currentHeight = lastRect.bottom - firstRect.top
|
|
101
|
+
|
|
102
|
+
top.value = currentTop
|
|
103
|
+
height.value = currentHeight
|
|
104
|
+
opacity.value = 1
|
|
105
|
+
|
|
106
|
+
// Scroll into view logic
|
|
107
|
+
const indicatorTop = currentTop
|
|
108
|
+
const indicatorBottom = currentTop + currentHeight
|
|
109
|
+
const scrollTop = el.value.scrollTop
|
|
110
|
+
const clientHeight = el.value.clientHeight
|
|
111
|
+
|
|
112
|
+
if (indicatorTop < scrollTop + 20) {
|
|
113
|
+
el.value.scrollTo({ top: indicatorTop - 20, behavior: 'smooth' })
|
|
114
|
+
} else if (indicatorBottom > scrollTop + clientHeight - 20) {
|
|
115
|
+
el.value.scrollTo({ top: indicatorBottom - clientHeight + 20, behavior: 'smooth' })
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
opacity.value = 0
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Attach listeners
|
|
123
|
+
let ticking = false
|
|
124
|
+
const onScroll = () => {
|
|
125
|
+
if (!ticking) {
|
|
126
|
+
window.requestAnimationFrame(() => {
|
|
127
|
+
updateActive()
|
|
128
|
+
ticking = false
|
|
129
|
+
})
|
|
130
|
+
ticking = true
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Wait for mount/layout
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
updateActive()
|
|
137
|
+
window.addEventListener('scroll', onScroll, { passive: true })
|
|
138
|
+
window.addEventListener('resize', onScroll, { passive: true })
|
|
139
|
+
return () => {
|
|
140
|
+
window.removeEventListener('scroll', onScroll)
|
|
141
|
+
window.removeEventListener('resize', onScroll)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<aside class="toc-panel" $ref={el}>
|
|
147
|
+
<div class="toc-indicator" style:top={t`${top}px`} style:height={t`${height}px`} style:opacity={t`${opacity}`}></div>
|
|
148
|
+
<div class="toc">
|
|
149
|
+
<h4>On this page</h4>
|
|
150
|
+
<ul>{...children}</ul>
|
|
151
|
+
</div>
|
|
152
|
+
</aside>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
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 buildTocItems = (items = []) => {
|
|
22
|
+
const nodes = []
|
|
23
|
+
for (const item of items) {
|
|
24
|
+
const childNodes = item?.children?.length ? buildTocItems(item.children) : []
|
|
25
|
+
const children = childNodes.length ? <ul>{childNodes}</ul> : null
|
|
26
|
+
if (item.depth < 2 || item.depth > 4) {
|
|
27
|
+
if (childNodes.length) {
|
|
28
|
+
nodes.push(...childNodes)
|
|
29
|
+
}
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
nodes.push(
|
|
33
|
+
<li class={`toc-depth-${item.depth}`}>
|
|
34
|
+
<a href={`#${item.id}`}>{item.value}</a>
|
|
35
|
+
{children}
|
|
36
|
+
</li>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
return nodes
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const renderToc = (toc = []) => {
|
|
43
|
+
const items = buildTocItems(toc)
|
|
44
|
+
if (!items.length) return null
|
|
45
|
+
return items
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function (props, ...children) {
|
|
49
|
+
if (!children.length) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<aside class="toc-panel">
|
|
55
|
+
<div class="toc">
|
|
56
|
+
<h4>On this page</h4>
|
|
57
|
+
<ul>{...children}</ul>
|
|
58
|
+
</div>
|
|
59
|
+
</aside>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
export default function (props, ...children) {
|
|
24
|
+
const el = signal()
|
|
25
|
+
const copied = signal(false)
|
|
26
|
+
|
|
27
|
+
const copy = async function () {
|
|
28
|
+
if (el.value) {
|
|
29
|
+
try {
|
|
30
|
+
await navigator.clipboard.writeText(el.value.textContent)
|
|
31
|
+
copied.value = true
|
|
32
|
+
setTimeout(() => {
|
|
33
|
+
copied.value = false
|
|
34
|
+
}, 2000)
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error('Failed to copy: ', err)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const Btn = copied.choose(
|
|
42
|
+
() => (
|
|
43
|
+
<svg
|
|
44
|
+
attr:width="14"
|
|
45
|
+
attr:height="14"
|
|
46
|
+
attr:viewBox="0 0 24 24"
|
|
47
|
+
attr:fill="none"
|
|
48
|
+
attr:stroke="currentColor"
|
|
49
|
+
attr:stroke-width="2.5"
|
|
50
|
+
attr:stroke-linecap="round"
|
|
51
|
+
attr:stroke-linejoin="round"
|
|
52
|
+
class="text-accent"
|
|
53
|
+
>
|
|
54
|
+
<polyline attr:points="20 6 9 17 4 12"></polyline>
|
|
55
|
+
</svg>
|
|
56
|
+
),
|
|
57
|
+
() => (
|
|
58
|
+
<svg
|
|
59
|
+
attr:width="14"
|
|
60
|
+
attr:height="14"
|
|
61
|
+
attr:viewBox="0 0 24 24"
|
|
62
|
+
attr:fill="none"
|
|
63
|
+
attr:stroke="currentColor"
|
|
64
|
+
attr:stroke-width="2"
|
|
65
|
+
attr:stroke-linecap="round"
|
|
66
|
+
attr:stroke-linejoin="round"
|
|
67
|
+
>
|
|
68
|
+
<rect attr:x="9" attr:y="9" attr:width="13" attr:height="13" attr:rx="2" attr:ry="2"></rect>
|
|
69
|
+
<path attr:d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
70
|
+
</svg>
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div class="code-block-container">
|
|
76
|
+
<button class="copy-btn" on:click={copy} aria-label="Copy code">
|
|
77
|
+
<Btn />
|
|
78
|
+
</button>
|
|
79
|
+
<pre {...props} $ref={el}>
|
|
80
|
+
{...children}
|
|
81
|
+
</pre>
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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 (props, ...children) {
|
|
22
|
+
return (
|
|
23
|
+
<div class="code-block-container">
|
|
24
|
+
<pre {...props}>{...children}</pre>
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -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 function heading(Tag) {
|
|
22
|
+
return (props, ...children) => (
|
|
23
|
+
<Tag {...props}>
|
|
24
|
+
{props.id && <a class="heading-anchor" href={`#${props.id}`} aria-hidden />}
|
|
25
|
+
{...children}
|
|
26
|
+
</Tag>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createHeadings() {
|
|
31
|
+
return Object.fromEntries([1,2,3,4,5,6].map((i) => {
|
|
32
|
+
const tag = `h${i}`
|
|
33
|
+
return [tag, heading(tag)]
|
|
34
|
+
}))
|
|
35
|
+
}
|