slidev-theme-watabegg 1.1.3 → 1.1.4
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.ja.md +13 -5
- package/README.md +13 -4
- package/components/KaTexReveal.vue +25 -24
- package/components/QuestionList.vue +246 -120
- package/components/TextBox.vue +5 -5
- package/global-bottom.vue +15 -7
- package/layouts/cover.vue +14 -3
- package/layouts/image-scroll.vue +3 -1
- package/layouts/image.vue +9 -3
- package/package.json +41 -12
- package/styles/globals.css.d.ts +3 -0
- package/styles/layout.css.d.ts +3 -0
- package/utils/colors.ts +1 -3
- package/utils/link.ts +11 -8
- package/utils/render.ts +42 -4
- package/utils/safeUrl.ts +27 -0
- package/utils/themeConfig.ts +18 -0
- package/utils/useThemePalette.ts +21 -25
- package/example.md +0 -197
package/README.ja.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/slidev-theme-watabegg)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
watabegg 向けに作っている個人用 Slidev テーマです。日本語表示、構造化されたコンテンツ、キーボード操作を重視しています。
|
|
6
6
|
|
|
7
7
|
## 特徴
|
|
8
8
|
- 教育特化のミニマルデザイン
|
|
@@ -31,16 +31,21 @@ theme: ./
|
|
|
31
31
|
|
|
32
32
|
## フロントマター例
|
|
33
33
|
```yaml
|
|
34
|
+
themeConfig:
|
|
35
|
+
watabegg:
|
|
36
|
+
color: green
|
|
37
|
+
link: 'https://example.com'
|
|
38
|
+
|
|
34
39
|
title: Theme Demo
|
|
35
40
|
subtitle: サブタイトル
|
|
36
41
|
author: 講師名
|
|
37
42
|
date: '2025/08/03'
|
|
38
|
-
color:
|
|
39
|
-
link: 'https://example.com' #
|
|
43
|
+
color: blue # スライド単位の上書き(省略可)
|
|
44
|
+
link: 'https://example.com/slide' # スライド単位の上書き(省略可)
|
|
40
45
|
transition: fade
|
|
41
46
|
```
|
|
42
47
|
|
|
43
|
-
`color`
|
|
48
|
+
`themeConfig.watabegg.color` と `themeConfig.watabegg.link` で deck 全体の既定値を設定できます。スライド側に `color` や `link` があればそちらが優先されます。
|
|
44
49
|
|
|
45
50
|
下図は`blue`指定の例。
|
|
46
51
|
|
|
@@ -118,7 +123,7 @@ imageScroll:
|
|
|
118
123
|
```
|
|
119
124
|
カウンター種別: `decimal | hiragana | katakana | kanji | upperalpha | loweralpha | none`
|
|
120
125
|
デコレータ: `circle | square | paren | dot | q | big-q | none`
|
|
121
|
-
アイテム内 `label`
|
|
126
|
+
アイテム内 `label` があればそれを優先表示。`text` 内の生 HTML はエスケープされ、Markdown リンクも `http` / `https` / `mailto` / `tel` と相対 URL のみ許可されます。
|
|
122
127
|
|
|
123
128
|

|
|
124
129
|
|
|
@@ -154,11 +159,14 @@ Props: `formula`(必須), `block`(既定 false), `tag`(省略時 `div`/`span`
|
|
|
154
159
|
```bash
|
|
155
160
|
pnpm install
|
|
156
161
|
pnpm dev
|
|
162
|
+
pnpm dev:polling
|
|
157
163
|
pnpm build
|
|
158
164
|
pnpm export
|
|
159
165
|
pnpm screenshot
|
|
160
166
|
```
|
|
161
167
|
|
|
168
|
+
Linux で `pnpm dev` が file watcher 上限 (`ENOSPC`) に当たる場合は `pnpm dev:polling` を使用してください。
|
|
169
|
+
|
|
162
170
|
## FAQ
|
|
163
171
|
**Q. フォント設定は必要?** → いいえ、テーマ内で Google Fonts を読み込みます。
|
|
164
172
|
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
English README (Japanese version: see `README.ja.md`).
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
8
|
-
|
|
8
|
+
A personal Slidev theme for watabegg, tuned for Japanese typography, structured content, and keyboard-first navigation.
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
- Minimal design specialized for teaching
|
|
@@ -35,16 +35,21 @@ theme: ./
|
|
|
35
35
|
|
|
36
36
|
## Frontmatter Example
|
|
37
37
|
```yaml
|
|
38
|
+
themeConfig:
|
|
39
|
+
watabegg:
|
|
40
|
+
color: green
|
|
41
|
+
link: 'https://example.com'
|
|
42
|
+
|
|
38
43
|
title: Theme Demo
|
|
39
44
|
subtitle: Subtitle
|
|
40
45
|
author: Instructor Name
|
|
41
46
|
date: '2025/08/03'
|
|
42
|
-
color:
|
|
43
|
-
link: 'https://example.com' # optional
|
|
47
|
+
color: blue # optional slide-level override
|
|
48
|
+
link: 'https://example.com/slide' # optional slide-level override
|
|
44
49
|
transition: fade
|
|
45
50
|
```
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
`themeConfig.watabegg.color` and `themeConfig.watabegg.link` define deck-level defaults. Slide-level `color` and `link` override them when present.
|
|
48
53
|
|
|
49
54
|
Below: `color: blue` example.
|
|
50
55
|
|
|
@@ -123,6 +128,7 @@ Nested questions/answers with per-level styles and Markdown rendering.
|
|
|
123
128
|
Counter types: `decimal | hiragana | katakana | kanji | upperalpha | loweralpha | none`
|
|
124
129
|
Decorators: `circle | square | paren | dot | q | big-q | none`
|
|
125
130
|
If an item includes a `label`, it overrides the computed label.
|
|
131
|
+
Raw HTML inside `text` is escaped. Markdown links only allow safe protocols (`http`, `https`, `mailto`, `tel`) plus relative URLs.
|
|
126
132
|
|
|
127
133
|

|
|
128
134
|
|
|
@@ -158,11 +164,14 @@ Props: `formula` (required), `block` (default `false`), `tag` (auto `div`/`span`
|
|
|
158
164
|
```bash
|
|
159
165
|
pnpm install
|
|
160
166
|
pnpm dev
|
|
167
|
+
pnpm dev:polling
|
|
161
168
|
pnpm build
|
|
162
169
|
pnpm export
|
|
163
170
|
pnpm screenshot
|
|
164
171
|
```
|
|
165
172
|
|
|
173
|
+
If `pnpm dev` hits Linux file watcher limits (`ENOSPC`), use `pnpm dev:polling`.
|
|
174
|
+
|
|
166
175
|
## FAQ
|
|
167
176
|
**Q. Do I need to configure fonts manually?**
|
|
168
177
|
No. Google Fonts are loaded inside the theme.
|
|
@@ -8,53 +8,54 @@
|
|
|
8
8
|
</template>
|
|
9
9
|
|
|
10
10
|
<script setup lang="ts">
|
|
11
|
-
import { computed, onMounted, onUpdated, ref, useAttrs } from 'vue'
|
|
12
|
-
import katex from 'katex'
|
|
13
|
-
import 'katex/dist/katex.min.css'
|
|
11
|
+
import { computed, onMounted, onUpdated, ref, useAttrs } from 'vue'
|
|
12
|
+
import katex from 'katex'
|
|
13
|
+
import 'katex/dist/katex.min.css'
|
|
14
14
|
|
|
15
|
-
defineOptions({ inheritAttrs: false })
|
|
15
|
+
defineOptions({ inheritAttrs: false })
|
|
16
16
|
|
|
17
17
|
const props = withDefaults(defineProps<{
|
|
18
|
-
formula: string
|
|
19
|
-
block?: boolean
|
|
20
|
-
tag?: string
|
|
18
|
+
formula: string
|
|
19
|
+
block?: boolean
|
|
20
|
+
tag?: string
|
|
21
21
|
}>(), {
|
|
22
22
|
block: false,
|
|
23
23
|
tag: undefined,
|
|
24
|
-
})
|
|
24
|
+
})
|
|
25
25
|
|
|
26
|
-
const attrs = useAttrs()
|
|
27
|
-
const root = ref<HTMLElement | null>(null)
|
|
26
|
+
const attrs = useAttrs()
|
|
27
|
+
const root = ref<HTMLElement | null>(null)
|
|
28
28
|
|
|
29
29
|
const forwardedAttrs = computed(() => {
|
|
30
|
-
const { class: _class, ...rest } = attrs as Record<string,
|
|
31
|
-
return rest
|
|
32
|
-
})
|
|
30
|
+
const { class: _class, ...rest } = attrs as Record<string, unknown>
|
|
31
|
+
return rest
|
|
32
|
+
})
|
|
33
33
|
|
|
34
|
-
const resolvedTag = computed(() => props.tag ?? (props.block ? 'div' : 'span'))
|
|
34
|
+
const resolvedTag = computed(() => props.tag ?? (props.block ? 'div' : 'span'))
|
|
35
35
|
|
|
36
36
|
const renderFormula = () => {
|
|
37
|
-
const target = root.value
|
|
38
|
-
if (!target)
|
|
37
|
+
const target = root.value
|
|
38
|
+
if (!target)
|
|
39
|
+
return
|
|
39
40
|
|
|
40
41
|
if (!props.formula) {
|
|
41
|
-
target.innerHTML = ''
|
|
42
|
-
return
|
|
42
|
+
target.innerHTML = ''
|
|
43
|
+
return
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
try {
|
|
46
47
|
katex.render(props.formula, target, {
|
|
47
48
|
displayMode: props.block,
|
|
48
49
|
throwOnError: false,
|
|
49
|
-
})
|
|
50
|
+
})
|
|
50
51
|
} catch (error) {
|
|
51
|
-
console.warn('[KaTexReveal] failed to render formula', error)
|
|
52
|
-
target.textContent = props.formula
|
|
52
|
+
console.warn('[KaTexReveal] failed to render formula', error)
|
|
53
|
+
target.textContent = props.formula
|
|
53
54
|
}
|
|
54
|
-
}
|
|
55
|
+
}
|
|
55
56
|
|
|
56
|
-
onMounted(renderFormula)
|
|
57
|
-
onUpdated(renderFormula)
|
|
57
|
+
onMounted(renderFormula)
|
|
58
|
+
onUpdated(renderFormula)
|
|
58
59
|
</script>
|
|
59
60
|
|
|
60
61
|
<style scoped>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div class="question-list" :style="{ '--level': currentLevel }">
|
|
3
3
|
<div v-for="(item, index) in items" :key="index" class="question-item">
|
|
4
4
|
<div class="item-content">
|
|
5
|
-
<span class="item-label"
|
|
5
|
+
<span class="item-label">{{ getLabel(index) }}</span>
|
|
6
6
|
<span v-if="hasRenderableContent(item)" class="item-text">
|
|
7
7
|
<KaTexReveal
|
|
8
8
|
v-if="isKatexItem(item)"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
:block="isBlockFormula(item)"
|
|
11
11
|
v-bind="getKatexAttrs(item)"
|
|
12
12
|
/>
|
|
13
|
-
<
|
|
13
|
+
<span v-else v-html="renderItemText(item)" />
|
|
14
14
|
</span>
|
|
15
15
|
</div>
|
|
16
16
|
<div v-if="hasSubItems(item)" class="sub-list" :style="{ marginTop: getItemText(item) ? '1rem' : '0' }">
|
|
@@ -26,198 +26,324 @@
|
|
|
26
26
|
</template>
|
|
27
27
|
|
|
28
28
|
<script setup lang="ts">
|
|
29
|
-
import { computed } from 'vue'
|
|
30
|
-
|
|
31
|
-
import
|
|
29
|
+
import { computed } from 'vue'
|
|
30
|
+
|
|
31
|
+
import KaTexReveal from './KaTexReveal.vue'
|
|
32
|
+
import { renderInlineMarkdown } from '../utils/render'
|
|
33
|
+
|
|
34
|
+
type QuestionListItem = string | QuestionListItemObject
|
|
35
|
+
|
|
36
|
+
interface QuestionListItemObject {
|
|
37
|
+
attrs?: Record<string, unknown>
|
|
38
|
+
block?: boolean
|
|
39
|
+
formula?: string
|
|
40
|
+
items?: QuestionListItem[]
|
|
41
|
+
label?: string
|
|
42
|
+
text?: string
|
|
43
|
+
tex?: boolean
|
|
44
|
+
type?: string
|
|
45
|
+
[key: string]: unknown
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type CounterType =
|
|
49
|
+
| 'decimal'
|
|
50
|
+
| 'hiragana'
|
|
51
|
+
| 'katakana'
|
|
52
|
+
| 'kanji'
|
|
53
|
+
| 'upperalpha'
|
|
54
|
+
| 'loweralpha'
|
|
55
|
+
| 'none'
|
|
56
|
+
|
|
57
|
+
type DecoratorType =
|
|
58
|
+
| 'circle'
|
|
59
|
+
| 'square'
|
|
60
|
+
| 'paren'
|
|
61
|
+
| 'dot'
|
|
62
|
+
| 'q'
|
|
63
|
+
| 'big-q'
|
|
64
|
+
| 'none'
|
|
32
65
|
|
|
33
66
|
const props = withDefaults(defineProps<{
|
|
34
|
-
items:
|
|
35
|
-
styles?: string[]
|
|
36
|
-
level?: number
|
|
37
|
-
start?: (number | string)[]
|
|
67
|
+
items: QuestionListItem[]
|
|
68
|
+
styles?: string[]
|
|
69
|
+
level?: number
|
|
70
|
+
start?: (number | string)[]
|
|
38
71
|
}>(), {
|
|
39
|
-
styles: () => ['decimal-circle', 'katakana-paren', '
|
|
72
|
+
styles: () => ['decimal-circle', 'katakana-paren', 'loweralpha-paren', 'decimal-dot'],
|
|
40
73
|
level: 0,
|
|
41
74
|
start: () => [],
|
|
42
|
-
})
|
|
75
|
+
})
|
|
43
76
|
|
|
44
|
-
const currentLevel = computed(() => props.level ?? 0)
|
|
77
|
+
const currentLevel = computed(() => props.level ?? 0)
|
|
78
|
+
|
|
79
|
+
const counterAliases: Record<string, CounterType> = {
|
|
80
|
+
decimal: 'decimal',
|
|
81
|
+
hiragana: 'hiragana',
|
|
82
|
+
katakana: 'katakana',
|
|
83
|
+
kanji: 'kanji',
|
|
84
|
+
loweralpha: 'loweralpha',
|
|
85
|
+
'lower-alpha': 'loweralpha',
|
|
86
|
+
none: 'none',
|
|
87
|
+
upperalpha: 'upperalpha',
|
|
88
|
+
'upper-alpha': 'upperalpha',
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const supportedCounters = Object.keys(counterAliases)
|
|
92
|
+
.sort((left, right) => right.length - left.length)
|
|
93
|
+
|
|
94
|
+
const supportedDecorators: DecoratorType[] = ['big-q', 'circle', 'square', 'paren', 'dot', 'q', 'none']
|
|
95
|
+
|
|
96
|
+
const circledNumbers = [
|
|
97
|
+
'①',
|
|
98
|
+
'②',
|
|
99
|
+
'③',
|
|
100
|
+
'④',
|
|
101
|
+
'⑤',
|
|
102
|
+
'⑥',
|
|
103
|
+
'⑦',
|
|
104
|
+
'⑧',
|
|
105
|
+
'⑨',
|
|
106
|
+
'⑩',
|
|
107
|
+
'⑪',
|
|
108
|
+
'⑫',
|
|
109
|
+
'⑬',
|
|
110
|
+
'⑭',
|
|
111
|
+
'⑮',
|
|
112
|
+
'⑯',
|
|
113
|
+
'⑰',
|
|
114
|
+
'⑱',
|
|
115
|
+
'⑲',
|
|
116
|
+
'⑳',
|
|
117
|
+
]
|
|
45
118
|
|
|
46
119
|
const TEX_WRAPPERS = [
|
|
47
120
|
{ start: '$$', end: '$$' },
|
|
48
121
|
{ start: '\\[', end: '\\]' },
|
|
49
122
|
{ start: '\\(', end: '\\)' },
|
|
50
|
-
]
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
function isObjectItem(item: QuestionListItem): item is QuestionListItemObject {
|
|
126
|
+
return typeof item === 'object' && item !== null
|
|
127
|
+
}
|
|
51
128
|
|
|
52
129
|
const stripTexDelimiters = (value: string): string => {
|
|
53
|
-
const trimmed = value.trim()
|
|
130
|
+
const trimmed = value.trim()
|
|
54
131
|
for (const { start, end } of TEX_WRAPPERS) {
|
|
55
132
|
if (trimmed.startsWith(start) && trimmed.endsWith(end)) {
|
|
56
|
-
return trimmed.slice(start.length, trimmed.length - end.length).trim()
|
|
133
|
+
return trimmed.slice(start.length, trimmed.length - end.length).trim()
|
|
57
134
|
}
|
|
58
135
|
}
|
|
59
|
-
return trimmed
|
|
60
|
-
}
|
|
136
|
+
return trimmed
|
|
137
|
+
}
|
|
61
138
|
|
|
62
139
|
const extractFormulaFromText = (value?: string): string => {
|
|
63
|
-
if (!value) return ''
|
|
64
|
-
const trimmed = value.trim()
|
|
140
|
+
if (!value) return ''
|
|
141
|
+
const trimmed = value.trim()
|
|
65
142
|
for (const { start, end } of TEX_WRAPPERS) {
|
|
66
143
|
if (trimmed.startsWith(start) && trimmed.endsWith(end)) {
|
|
67
|
-
return stripTexDelimiters(trimmed)
|
|
144
|
+
return stripTexDelimiters(trimmed)
|
|
68
145
|
}
|
|
69
146
|
}
|
|
70
|
-
return ''
|
|
71
|
-
}
|
|
147
|
+
return ''
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseStyle(style: string | undefined): { counterType: CounterType, decoratorType: DecoratorType } {
|
|
151
|
+
const normalized = style?.trim().toLowerCase() ?? ''
|
|
152
|
+
if (!normalized)
|
|
153
|
+
return { counterType: 'decimal', decoratorType: 'dot' }
|
|
154
|
+
|
|
155
|
+
if (normalized === 'none')
|
|
156
|
+
return { counterType: 'none', decoratorType: 'none' }
|
|
157
|
+
|
|
158
|
+
const counterKey = supportedCounters.find((candidate) => normalized === candidate || normalized.startsWith(`${candidate}-`))
|
|
159
|
+
const counterType = counterKey
|
|
160
|
+
? counterAliases[counterKey]
|
|
161
|
+
: 'decimal'
|
|
162
|
+
|
|
163
|
+
const rawDecorator = counterKey && normalized.length > counterKey.length
|
|
164
|
+
? normalized.slice(counterKey.length + 1)
|
|
165
|
+
: 'dot'
|
|
166
|
+
|
|
167
|
+
const decoratorType = supportedDecorators.includes(rawDecorator as DecoratorType)
|
|
168
|
+
? rawDecorator as DecoratorType
|
|
169
|
+
: 'dot'
|
|
170
|
+
|
|
171
|
+
return { counterType, decoratorType }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getTextContent(item: QuestionListItemObject | string | undefined) {
|
|
175
|
+
if (typeof item === 'string')
|
|
176
|
+
return item
|
|
177
|
+
|
|
178
|
+
return typeof item?.text === 'string'
|
|
179
|
+
? item.text
|
|
180
|
+
: ''
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getStartIndex(counterType: CounterType, value: number | string | undefined) {
|
|
184
|
+
if (typeof value === 'number')
|
|
185
|
+
return value - 1
|
|
186
|
+
|
|
187
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
188
|
+
const firstChar = value[0]
|
|
189
|
+
if (counterType === 'upperalpha')
|
|
190
|
+
return firstChar.toUpperCase().charCodeAt(0) - 'A'.charCodeAt(0)
|
|
191
|
+
|
|
192
|
+
if (counterType === 'loweralpha')
|
|
193
|
+
return firstChar.toLowerCase().charCodeAt(0) - 'a'.charCodeAt(0)
|
|
194
|
+
}
|
|
72
195
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
196
|
+
return 0
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const hasRenderableContent = (item: QuestionListItem): boolean => {
|
|
200
|
+
if (isKatexItem(item)) return true
|
|
201
|
+
return Boolean(getItemText(item).trim())
|
|
202
|
+
}
|
|
77
203
|
|
|
78
|
-
const normalizedItemType = (item:
|
|
79
|
-
if (typeof item.type !== 'string') return ''
|
|
80
|
-
return item.type.toLowerCase()
|
|
81
|
-
}
|
|
204
|
+
const normalizedItemType = (item: QuestionListItemObject): string => {
|
|
205
|
+
if (typeof item.type !== 'string') return ''
|
|
206
|
+
return item.type.toLowerCase()
|
|
207
|
+
}
|
|
82
208
|
|
|
83
|
-
const getFormula = (item:
|
|
84
|
-
if (
|
|
209
|
+
const getFormula = (item: QuestionListItem): string => {
|
|
210
|
+
if (isObjectItem(item)) {
|
|
85
211
|
if (typeof item.formula === 'string' && item.formula.trim()) {
|
|
86
|
-
return stripTexDelimiters(item.formula)
|
|
212
|
+
return stripTexDelimiters(item.formula)
|
|
87
213
|
}
|
|
88
|
-
const hint = item.tex === true || ['katex', 'tex', 'math'].includes(normalizedItemType(item))
|
|
214
|
+
const hint = item.tex === true || ['katex', 'tex', 'math'].includes(normalizedItemType(item))
|
|
89
215
|
if (hint && typeof item.text === 'string') {
|
|
90
|
-
return stripTexDelimiters(item.text)
|
|
216
|
+
return stripTexDelimiters(item.text)
|
|
91
217
|
}
|
|
92
|
-
|
|
93
|
-
return extracted;
|
|
218
|
+
return extractFormulaFromText(item.text)
|
|
94
219
|
}
|
|
95
|
-
return extractFormulaFromText(item) || ''
|
|
96
|
-
}
|
|
220
|
+
return extractFormulaFromText(item) || ''
|
|
221
|
+
}
|
|
97
222
|
|
|
98
|
-
const isKatexItem = (item:
|
|
99
|
-
return Boolean(getFormula(item));
|
|
100
|
-
};
|
|
223
|
+
const isKatexItem = (item: QuestionListItem): boolean => Boolean(getFormula(item))
|
|
101
224
|
|
|
102
|
-
const isBlockFormula = (item:
|
|
103
|
-
if (
|
|
104
|
-
return item.block
|
|
225
|
+
const isBlockFormula = (item: QuestionListItem): boolean => {
|
|
226
|
+
if (isObjectItem(item) && typeof item.block === 'boolean') {
|
|
227
|
+
return item.block
|
|
105
228
|
}
|
|
106
229
|
|
|
107
|
-
const source =
|
|
108
|
-
? item
|
|
109
|
-
: typeof item.formula === 'string'
|
|
230
|
+
const source = isObjectItem(item)
|
|
231
|
+
? typeof item.formula === 'string'
|
|
110
232
|
? item.formula
|
|
111
233
|
: typeof item.text === 'string'
|
|
112
234
|
? item.text
|
|
113
|
-
: ''
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (trimmed
|
|
117
|
-
if (trimmed.startsWith('
|
|
118
|
-
if (trimmed.startsWith('
|
|
119
|
-
if (trimmed.startsWith('
|
|
120
|
-
return true
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
235
|
+
: ''
|
|
236
|
+
: item
|
|
237
|
+
const trimmed = source.trim()
|
|
238
|
+
if (!trimmed) return true
|
|
239
|
+
if (trimmed.startsWith('\\(') && trimmed.endsWith('\\)')) return false
|
|
240
|
+
if (trimmed.startsWith('$') && trimmed.endsWith('$') && !trimmed.startsWith('$$')) return false
|
|
241
|
+
if (trimmed.startsWith('$$') && trimmed.endsWith('$$')) return true
|
|
242
|
+
if (trimmed.startsWith('\\[') && trimmed.endsWith('\\]')) return true
|
|
243
|
+
return true
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const reservedItemKeys = new Set(['text', 'items', 'label', 'block', 'type', 'tex', 'formula', 'attrs'])
|
|
247
|
+
|
|
248
|
+
const getKatexAttrs = (item: QuestionListItem): Record<string, unknown> => {
|
|
249
|
+
if (!isObjectItem(item)) {
|
|
250
|
+
return {}
|
|
128
251
|
}
|
|
129
252
|
if (item.attrs && typeof item.attrs === 'object') {
|
|
130
|
-
return item.attrs
|
|
253
|
+
return item.attrs
|
|
131
254
|
}
|
|
132
|
-
const attrs: Record<string,
|
|
255
|
+
const attrs: Record<string, unknown> = {}
|
|
133
256
|
Object.keys(item).forEach((key) => {
|
|
134
257
|
if (!reservedItemKeys.has(key)) {
|
|
135
|
-
attrs[key] = item[key]
|
|
258
|
+
attrs[key] = item[key]
|
|
136
259
|
}
|
|
137
|
-
})
|
|
138
|
-
return attrs
|
|
139
|
-
}
|
|
260
|
+
})
|
|
261
|
+
return attrs
|
|
262
|
+
}
|
|
140
263
|
|
|
141
|
-
const getItemText = (item:
|
|
142
|
-
(typeof item === 'string' ? item : item.text) || '';
|
|
264
|
+
const getItemText = (item: QuestionListItem): string => getTextContent(item)
|
|
143
265
|
|
|
144
|
-
const hasSubItems = (item:
|
|
145
|
-
typeof item === 'object' && Array.isArray(item.items);
|
|
266
|
+
const hasSubItems = (item: QuestionListItem): boolean => isObjectItem(item) && Array.isArray(item.items)
|
|
146
267
|
|
|
147
|
-
const getSubItems = (item:
|
|
148
|
-
(
|
|
268
|
+
const getSubItems = (item: QuestionListItem): QuestionListItem[] =>
|
|
269
|
+
isObjectItem(item) && Array.isArray(item.items)
|
|
270
|
+
? item.items
|
|
271
|
+
: []
|
|
149
272
|
|
|
150
273
|
const getLabel = (index: number): string => {
|
|
151
|
-
const item = props.items[index]
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return item.label;
|
|
274
|
+
const item = props.items[index]
|
|
275
|
+
if (isObjectItem(item) && typeof item.label === 'string') {
|
|
276
|
+
return item.label
|
|
155
277
|
}
|
|
156
278
|
|
|
157
|
-
const
|
|
158
|
-
|
|
279
|
+
const { counterType, decoratorType } = parseStyle(props.styles[currentLevel.value])
|
|
280
|
+
if (counterType === 'none' || decoratorType === 'none')
|
|
281
|
+
return ''
|
|
159
282
|
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
let counter = '';
|
|
283
|
+
const startIndex = getStartIndex(counterType, props.start[currentLevel.value])
|
|
284
|
+
const currentIndex = startIndex + index
|
|
285
|
+
|
|
286
|
+
let counter = ''
|
|
165
287
|
switch (counterType) {
|
|
166
288
|
case 'hiragana':
|
|
167
|
-
counter = toHiragana(currentIndex)
|
|
289
|
+
counter = toHiragana(currentIndex)
|
|
290
|
+
break
|
|
168
291
|
case 'katakana':
|
|
169
|
-
counter = toKatakana(currentIndex)
|
|
292
|
+
counter = toKatakana(currentIndex)
|
|
293
|
+
break
|
|
170
294
|
case 'kanji':
|
|
171
|
-
counter = toKanji(currentIndex + 1)
|
|
295
|
+
counter = toKanji(currentIndex + 1)
|
|
296
|
+
break
|
|
172
297
|
case 'upperalpha':
|
|
173
|
-
counter = String.fromCharCode('A'.charCodeAt(0) + currentIndex)
|
|
298
|
+
counter = String.fromCharCode('A'.charCodeAt(0) + currentIndex)
|
|
299
|
+
break
|
|
174
300
|
case 'loweralpha':
|
|
175
|
-
counter = String.fromCharCode('a'.charCodeAt(0) + currentIndex)
|
|
301
|
+
counter = String.fromCharCode('a'.charCodeAt(0) + currentIndex)
|
|
302
|
+
break
|
|
176
303
|
case 'decimal':
|
|
177
|
-
counter = (currentIndex + 1).toString()
|
|
178
|
-
|
|
179
|
-
return ''; // No label
|
|
304
|
+
counter = (currentIndex + 1).toString()
|
|
305
|
+
break
|
|
180
306
|
default:
|
|
181
|
-
counter = (currentIndex + 1).toString()
|
|
307
|
+
counter = (currentIndex + 1).toString()
|
|
182
308
|
}
|
|
183
309
|
|
|
184
|
-
// 3. Apply decorator
|
|
185
310
|
switch (decoratorType) {
|
|
186
311
|
case 'circle':
|
|
187
|
-
return
|
|
312
|
+
return circledNumbers[currentIndex] ?? `${counter}.`
|
|
188
313
|
case 'square':
|
|
189
|
-
return `[${counter}]
|
|
314
|
+
return `[${counter}]`
|
|
190
315
|
case 'paren':
|
|
191
|
-
return `(${counter})
|
|
316
|
+
return `(${counter})`
|
|
192
317
|
case 'dot':
|
|
193
|
-
return `${counter}
|
|
318
|
+
return `${counter}.`
|
|
194
319
|
case 'q':
|
|
195
|
-
return `問${counter}
|
|
320
|
+
return `問${counter}`
|
|
196
321
|
case 'big-q':
|
|
197
|
-
return `大問${counter}
|
|
198
|
-
case 'none':
|
|
199
|
-
return '';
|
|
322
|
+
return `大問${counter}`
|
|
200
323
|
default:
|
|
201
|
-
return counter
|
|
324
|
+
return counter
|
|
202
325
|
}
|
|
203
|
-
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const renderItemText = (item: QuestionListItem) => renderInlineMarkdown(getItemText(item))
|
|
204
329
|
|
|
205
|
-
// --- Helper functions for counters ---
|
|
206
330
|
const toKatakana = (n: number) => {
|
|
207
|
-
const katakana = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン'
|
|
208
|
-
return katakana[n % katakana.length]
|
|
209
|
-
}
|
|
331
|
+
const katakana = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン'
|
|
332
|
+
return katakana[((n % katakana.length) + katakana.length) % katakana.length]
|
|
333
|
+
}
|
|
334
|
+
|
|
210
335
|
const toHiragana = (n: number) => {
|
|
211
|
-
const hiragana = 'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん'
|
|
212
|
-
return hiragana[n % hiragana.length]
|
|
213
|
-
}
|
|
336
|
+
const hiragana = 'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん'
|
|
337
|
+
return hiragana[((n % hiragana.length) + hiragana.length) % hiragana.length]
|
|
338
|
+
}
|
|
339
|
+
|
|
214
340
|
const toKanji = (n: number) => {
|
|
215
|
-
const kanji = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
|
|
216
|
-
if (n <=
|
|
217
|
-
|
|
218
|
-
if (n
|
|
219
|
-
return n.toString()
|
|
220
|
-
}
|
|
341
|
+
const kanji = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
|
|
342
|
+
if (n <= 0) return n.toString()
|
|
343
|
+
if (n <= 10) return kanji[n - 1]
|
|
344
|
+
if (n < 20) return `十${kanji[n - 11]}`
|
|
345
|
+
return n.toString()
|
|
346
|
+
}
|
|
221
347
|
|
|
222
348
|
</script>
|
|
223
349
|
|
|
@@ -237,7 +363,7 @@ const toKanji = (n: number) => {
|
|
|
237
363
|
font-weight: bold;
|
|
238
364
|
color: var(--slidev-theme-primary);
|
|
239
365
|
margin-right: 0.75em;
|
|
240
|
-
min-width: 2em;
|
|
366
|
+
min-width: 2em;
|
|
241
367
|
text-align: right;
|
|
242
368
|
}
|
|
243
369
|
.item-text {
|
package/components/TextBox.vue
CHANGED
|
@@ -88,22 +88,22 @@ const computedClasses = computed(() => {
|
|
|
88
88
|
|
|
89
89
|
/* Text color variants */
|
|
90
90
|
.text-blue {
|
|
91
|
-
color: #3b82f6
|
|
91
|
+
color: #3b82f6;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
.text-green {
|
|
95
|
-
color: #10b981
|
|
95
|
+
color: #10b981;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
.text-red {
|
|
99
|
-
color: #ef4444
|
|
99
|
+
color: #ef4444;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
.text-purple {
|
|
103
|
-
color: #8b5cf6
|
|
103
|
+
color: #8b5cf6;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
.text-orange {
|
|
107
|
-
color: #f97316
|
|
107
|
+
color: #f97316;
|
|
108
108
|
}
|
|
109
109
|
</style>
|
package/global-bottom.vue
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { watchEffect } from 'vue'
|
|
3
|
-
import { useSlideContext } from '@slidev/client'
|
|
2
|
+
import { computed, watchEffect } from 'vue'
|
|
3
|
+
import { useNav, useSlideContext } from '@slidev/client'
|
|
4
4
|
|
|
5
5
|
import { useActiveThemePalette } from './utils/useThemePalette'
|
|
6
6
|
import getLink from './utils/link'
|
|
7
7
|
|
|
8
|
-
const { $
|
|
8
|
+
const { $nav, $slidev } = useSlideContext()
|
|
9
|
+
const { currentSlideRoute } = useNav()
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
frontmatter
|
|
11
|
+
const currentFrontmatter = computed<Record<string, unknown>>(
|
|
12
|
+
() => currentSlideRoute.value.meta?.slide?.frontmatter ?? {},
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const link = computed(() => getLink({
|
|
16
|
+
frontmatter: currentFrontmatter.value,
|
|
12
17
|
slidevConfigs: $slidev?.configs as Record<string, unknown> | undefined,
|
|
13
|
-
})
|
|
18
|
+
}))
|
|
14
19
|
|
|
15
|
-
const { palette: currentPalette } = useActiveThemePalette(
|
|
20
|
+
const { palette: currentPalette } = useActiveThemePalette({
|
|
21
|
+
slidevConfigs: $slidev?.configs as Record<string, unknown> | undefined,
|
|
22
|
+
slideValue: () => currentFrontmatter.value.color,
|
|
23
|
+
})
|
|
16
24
|
|
|
17
25
|
watchEffect(() => {
|
|
18
26
|
if (typeof document === 'undefined')
|
package/layouts/cover.vue
CHANGED
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
{{ $frontmatter.author }}
|
|
12
12
|
</div>
|
|
13
13
|
<div class="wave">
|
|
14
|
-
<svg
|
|
14
|
+
<svg
|
|
15
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
16
|
+
viewBox="0 0 1280 700"
|
|
17
|
+
width="100%"
|
|
18
|
+
height="100%"
|
|
19
|
+
preserveAspectRatio="none"
|
|
20
|
+
aria-hidden="true"
|
|
21
|
+
focusable="false"
|
|
22
|
+
>
|
|
15
23
|
<defs>
|
|
16
24
|
<linearGradient id="wave-1754178473929" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="700" y2="700" gradientTransform="scale(1.83, 1)">
|
|
17
25
|
<stop offset="0%" :stop-color="currentPalette.gradientStart"/>
|
|
@@ -30,9 +38,12 @@ import { useSlideContext } from '@slidev/client'
|
|
|
30
38
|
|
|
31
39
|
import { useActiveThemePalette } from '../utils/useThemePalette'
|
|
32
40
|
|
|
33
|
-
const { $frontmatter } = useSlideContext()
|
|
41
|
+
const { $frontmatter, $slidev } = useSlideContext()
|
|
34
42
|
|
|
35
|
-
const { palette: currentPalette } = useActiveThemePalette(
|
|
43
|
+
const { palette: currentPalette } = useActiveThemePalette({
|
|
44
|
+
slidevConfigs: $slidev?.configs as Record<string, unknown> | undefined,
|
|
45
|
+
slideValue: () => $frontmatter.color,
|
|
46
|
+
})
|
|
36
47
|
|
|
37
48
|
const cssVars = computed(() => ({
|
|
38
49
|
'--slidev-theme-primary': currentPalette.value.primary,
|
package/layouts/image-scroll.vue
CHANGED
|
@@ -32,7 +32,9 @@ import { useSlideContext } from '@slidev/client'
|
|
|
32
32
|
|
|
33
33
|
const { $frontmatter } = useSlideContext()
|
|
34
34
|
|
|
35
|
-
const imageSrc = computed(() => $frontmatter.image
|
|
35
|
+
const imageSrc = computed(() => typeof $frontmatter.image === 'string'
|
|
36
|
+
? $frontmatter.image
|
|
37
|
+
: '')
|
|
36
38
|
|
|
37
39
|
const wrapperRef = ref<HTMLElement | null>(null)
|
|
38
40
|
const scrollerRef = ref<HTMLElement | null>(null)
|
package/layouts/image.vue
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="slidev-layout image">
|
|
3
|
-
<img
|
|
4
|
-
v-if="
|
|
5
|
-
:src="
|
|
3
|
+
<img
|
|
4
|
+
v-if="imageSrc"
|
|
5
|
+
:src="imageSrc"
|
|
6
6
|
class="background-image"
|
|
7
7
|
alt="Background"
|
|
8
8
|
/>
|
|
@@ -13,8 +13,14 @@
|
|
|
13
13
|
</template>
|
|
14
14
|
|
|
15
15
|
<script setup lang="ts">
|
|
16
|
+
import { computed } from 'vue'
|
|
16
17
|
import { useSlideContext } from '@slidev/client'
|
|
18
|
+
|
|
17
19
|
const { $frontmatter } = useSlideContext()
|
|
20
|
+
|
|
21
|
+
const imageSrc = computed(() => typeof $frontmatter.image === 'string'
|
|
22
|
+
? $frontmatter.image
|
|
23
|
+
: '')
|
|
18
24
|
</script>
|
|
19
25
|
|
|
20
26
|
<style scoped>
|
package/package.json
CHANGED
|
@@ -1,38 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slidev-theme-watabegg",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"packageManager": "pnpm@11.1.3",
|
|
5
6
|
"author": "watabegg",
|
|
6
|
-
"description": "Slidev theme for
|
|
7
|
+
"description": "A personal Slidev theme for watabegg.",
|
|
7
8
|
"license": "MIT",
|
|
9
|
+
"homepage": "https://github.com/watabegg/slidev-theme-watabegg#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/watabegg/slidev-theme-watabegg/issues"
|
|
12
|
+
},
|
|
8
13
|
"keywords": [
|
|
9
14
|
"slidev-theme",
|
|
10
15
|
"slidev",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"teaching"
|
|
16
|
+
"watabegg",
|
|
17
|
+
"personal-theme"
|
|
14
18
|
],
|
|
15
19
|
"engines": {
|
|
16
|
-
"node": ">=
|
|
20
|
+
"node": ">=20.12.0",
|
|
21
|
+
"slidev": ">=52.15.2"
|
|
17
22
|
},
|
|
18
23
|
"repository": {
|
|
19
24
|
"type": "git",
|
|
20
25
|
"url": "https://github.com/watabegg/slidev-theme-watabegg"
|
|
21
26
|
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"components",
|
|
32
|
+
"layouts",
|
|
33
|
+
"setup",
|
|
34
|
+
"styles",
|
|
35
|
+
"utils",
|
|
36
|
+
"global-bottom.vue"
|
|
37
|
+
],
|
|
22
38
|
"scripts": {
|
|
23
39
|
"build": "slidev build example.md",
|
|
24
40
|
"dev": "slidev example.md",
|
|
41
|
+
"dev:polling": "CHOKIDAR_USEPOLLING=1 slidev example.md",
|
|
42
|
+
"typecheck": "vue-tsc --noEmit -p tsconfig.json",
|
|
43
|
+
"lint": "biome lint .",
|
|
44
|
+
"format": "biome format --write .",
|
|
45
|
+
"format:check": "biome format .",
|
|
46
|
+
"check": "pnpm typecheck && pnpm lint && pnpm format:check && pnpm build && pnpm pack --dry-run",
|
|
47
|
+
"audit:prod": "pnpm audit --prod --audit-level=high",
|
|
25
48
|
"export": "slidev export example.md",
|
|
26
49
|
"screenshot": "slidev export example.md --format png"
|
|
27
50
|
},
|
|
28
51
|
"dependencies": {
|
|
29
|
-
"@slidev/types": "^52.
|
|
30
|
-
"katex": "^0.16.
|
|
52
|
+
"@slidev/types": "^52.15.2",
|
|
53
|
+
"katex": "^0.16.47",
|
|
54
|
+
"marked": "^18.0.4"
|
|
31
55
|
},
|
|
32
56
|
"devDependencies": {
|
|
33
|
-
"@
|
|
34
|
-
"
|
|
35
|
-
"
|
|
57
|
+
"@biomejs/biome": "^2.4.15",
|
|
58
|
+
"@slidev/cli": "^52.15.2",
|
|
59
|
+
"@slidev/client": "^52.15.2",
|
|
60
|
+
"bumpp": "^11.1.0",
|
|
61
|
+
"playwright-chromium": "^1.60.0",
|
|
62
|
+
"typescript": "^6.0.3",
|
|
63
|
+
"vue": "^3.5.34",
|
|
64
|
+
"vue-tsc": "^3.3.1"
|
|
36
65
|
},
|
|
37
66
|
"//": "Learn more: https://sli.dev/guide/write-theme.html",
|
|
38
67
|
"slidev": {
|
|
@@ -46,4 +75,4 @@
|
|
|
46
75
|
"lineNumbers": false
|
|
47
76
|
}
|
|
48
77
|
}
|
|
49
|
-
}
|
|
78
|
+
}
|
package/utils/colors.ts
CHANGED
|
@@ -43,8 +43,7 @@ export const themePalettes: Record<ThemeColorKey, ThemePalette> = {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function resolveThemeColor(rawValue: unknown): ThemeColorKey {
|
|
46
|
-
if (typeof rawValue !== 'string')
|
|
47
|
-
return defaultThemeColor
|
|
46
|
+
if (typeof rawValue !== 'string') return defaultThemeColor
|
|
48
47
|
|
|
49
48
|
const normalized = rawValue.trim().toLowerCase() as ThemeColorKey
|
|
50
49
|
return normalized in themePalettes ? normalized : defaultThemeColor
|
|
@@ -54,4 +53,3 @@ export function getThemePalette(rawValue: unknown): ThemePalette {
|
|
|
54
53
|
const key = resolveThemeColor(rawValue)
|
|
55
54
|
return themePalettes[key]
|
|
56
55
|
}
|
|
57
|
-
|
package/utils/link.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { toSafeUrl } from './safeUrl'
|
|
2
|
+
import { getWatabeggThemeConfig } from './themeConfig'
|
|
3
|
+
|
|
1
4
|
const getLink = ({
|
|
2
5
|
frontmatter,
|
|
3
6
|
slidevConfigs,
|
|
@@ -5,13 +8,13 @@ const getLink = ({
|
|
|
5
8
|
frontmatter: Record<string, unknown>
|
|
6
9
|
slidevConfigs: Record<string, unknown> | undefined
|
|
7
10
|
}) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
const frontmatterLink = toSafeUrl(frontmatter.link)
|
|
12
|
+
if (frontmatterLink) return frontmatterLink
|
|
13
|
+
|
|
14
|
+
const themeConfigLink = toSafeUrl(getWatabeggThemeConfig(slidevConfigs).link)
|
|
15
|
+
if (themeConfigLink) return themeConfigLink
|
|
16
|
+
|
|
17
|
+
return toSafeUrl(slidevConfigs?.link)
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
export default getLink
|
|
20
|
+
export default getLink
|
package/utils/render.ts
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Marked } from 'marked'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { toSafeUrl } from './safeUrl'
|
|
4
|
+
|
|
5
|
+
function escapeHtml(value: string) {
|
|
6
|
+
return value
|
|
7
|
+
.replaceAll('&', '&')
|
|
8
|
+
.replaceAll('<', '<')
|
|
9
|
+
.replaceAll('>', '>')
|
|
10
|
+
.replaceAll('"', '"')
|
|
11
|
+
.replaceAll("'", ''')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const inlineMarkdown = new Marked({
|
|
15
|
+
async: false,
|
|
16
|
+
gfm: true,
|
|
17
|
+
renderer: {
|
|
18
|
+
image(token) {
|
|
19
|
+
const src = toSafeUrl(token.href)
|
|
20
|
+
const alt = escapeHtml(token.text)
|
|
21
|
+
const title =
|
|
22
|
+
typeof token.title === 'string' && token.title ? ` title="${escapeHtml(token.title)}"` : ''
|
|
23
|
+
|
|
24
|
+
if (!src) return alt
|
|
25
|
+
|
|
26
|
+
return `<img src="${escapeHtml(src)}" alt="${alt}"${title}>`
|
|
27
|
+
},
|
|
28
|
+
link(token) {
|
|
29
|
+
const href = toSafeUrl(token.href)
|
|
30
|
+
const text = this.parser.parseInline(token.tokens)
|
|
31
|
+
const title =
|
|
32
|
+
typeof token.title === 'string' && token.title ? ` title="${escapeHtml(token.title)}"` : ''
|
|
33
|
+
|
|
34
|
+
if (!href) return text
|
|
35
|
+
|
|
36
|
+
return `<a href="${escapeHtml(href)}"${title} rel="noopener noreferrer">${text}</a>`
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export function renderInlineMarkdown(text: string) {
|
|
42
|
+
return inlineMarkdown.parseInline(escapeHtml(text))
|
|
43
|
+
}
|
package/utils/safeUrl.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const SAFE_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:'])
|
|
2
|
+
|
|
3
|
+
function isRelativeUrl(value: string) {
|
|
4
|
+
return (
|
|
5
|
+
value.startsWith('#') ||
|
|
6
|
+
value.startsWith('/') ||
|
|
7
|
+
value.startsWith('./') ||
|
|
8
|
+
value.startsWith('../') ||
|
|
9
|
+
value.startsWith('?')
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function toSafeUrl(value: unknown): string | undefined {
|
|
14
|
+
if (typeof value !== 'string') return undefined
|
|
15
|
+
|
|
16
|
+
const trimmed = value.trim()
|
|
17
|
+
if (!trimmed) return undefined
|
|
18
|
+
|
|
19
|
+
if (isRelativeUrl(trimmed)) return trimmed
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(trimmed)
|
|
23
|
+
return SAFE_PROTOCOLS.has(url.protocol) ? url.toString() : undefined
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface WatabeggThemeConfig {
|
|
2
|
+
color?: unknown
|
|
3
|
+
link?: unknown
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
7
|
+
return typeof value === 'object' && value !== null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getWatabeggThemeConfig(
|
|
11
|
+
slidevConfigs: Record<string, unknown> | undefined,
|
|
12
|
+
): WatabeggThemeConfig {
|
|
13
|
+
const themeConfig = slidevConfigs?.themeConfig
|
|
14
|
+
if (!isRecord(themeConfig)) return {}
|
|
15
|
+
|
|
16
|
+
const watabegg = themeConfig.watabegg
|
|
17
|
+
return isRecord(watabegg) ? watabegg : {}
|
|
18
|
+
}
|
package/utils/useThemePalette.ts
CHANGED
|
@@ -1,26 +1,22 @@
|
|
|
1
|
-
import { computed
|
|
1
|
+
import { computed } from 'vue'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
defaultThemeColor,
|
|
5
|
+
getThemePalette,
|
|
5
6
|
resolveThemeColor,
|
|
6
|
-
themePalettes,
|
|
7
7
|
type ThemeColorKey,
|
|
8
8
|
type ThemePalette,
|
|
9
9
|
} from './colors'
|
|
10
|
-
|
|
11
|
-
const activeColorKey = ref<ThemeColorKey>(defaultThemeColor)
|
|
10
|
+
import { getWatabeggThemeConfig } from './themeConfig'
|
|
12
11
|
|
|
13
12
|
function normalizeColor(value: unknown): ThemeColorKey | undefined {
|
|
14
|
-
if (value === undefined)
|
|
15
|
-
return undefined
|
|
13
|
+
if (value === undefined) return undefined
|
|
16
14
|
|
|
17
|
-
if (value === null)
|
|
18
|
-
return defaultThemeColor
|
|
15
|
+
if (value === null) return defaultThemeColor
|
|
19
16
|
|
|
20
17
|
if (typeof value === 'string') {
|
|
21
18
|
const trimmed = value.trim()
|
|
22
|
-
if (!trimmed)
|
|
23
|
-
return defaultThemeColor
|
|
19
|
+
if (!trimmed) return defaultThemeColor
|
|
24
20
|
|
|
25
21
|
return resolveThemeColor(trimmed)
|
|
26
22
|
}
|
|
@@ -28,22 +24,22 @@ function normalizeColor(value: unknown): ThemeColorKey | undefined {
|
|
|
28
24
|
return defaultThemeColor
|
|
29
25
|
}
|
|
30
26
|
|
|
31
|
-
export function useActiveThemePalette(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
export function useActiveThemePalette({
|
|
28
|
+
slidevConfigs,
|
|
29
|
+
slideValue,
|
|
30
|
+
}: {
|
|
31
|
+
slidevConfigs?: Record<string, unknown>
|
|
32
|
+
slideValue: () => unknown
|
|
33
|
+
}) {
|
|
34
|
+
const colorKey = computed<ThemeColorKey>(() => {
|
|
35
|
+
const frontmatterColor = normalizeColor(slideValue())
|
|
36
|
+
if (frontmatterColor !== undefined) return frontmatterColor
|
|
37
|
+
|
|
38
|
+
const themeConfigColor = normalizeColor(getWatabeggThemeConfig(slidevConfigs).color)
|
|
39
|
+
return themeConfigColor ?? defaultThemeColor
|
|
36
40
|
})
|
|
37
41
|
|
|
38
|
-
const palette = computed<ThemePalette>(() =>
|
|
42
|
+
const palette = computed<ThemePalette>(() => getThemePalette(colorKey.value))
|
|
39
43
|
|
|
40
|
-
return {
|
|
41
|
-
palette,
|
|
42
|
-
colorKey: computed(() => activeColorKey.value),
|
|
43
|
-
}
|
|
44
|
+
return { palette, colorKey }
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
export function getActiveThemePalette(): ThemePalette {
|
|
47
|
-
return themePalettes[activeColorKey.value]
|
|
48
|
-
}
|
|
49
|
-
|
package/example.md
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
theme: ./
|
|
3
|
-
title: Theme Demo
|
|
4
|
-
subtitle: slidev-theme-watabeggの紹介
|
|
5
|
-
author: watabegg
|
|
6
|
-
date: '2025/08/03'
|
|
7
|
-
link: 'https://example.com'
|
|
8
|
-
color: blue
|
|
9
|
-
transition: fade
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# 基本機能とスタイル
|
|
13
|
-
|
|
14
|
-
このスライドではテーマの基本スタイルをテストします。
|
|
15
|
-
|
|
16
|
-
- **太字** / *斜体* / `code`
|
|
17
|
-
- 入れ子リスト
|
|
18
|
-
- 第二階層
|
|
19
|
-
- もう一つ
|
|
20
|
-
- テーマカラーは frontmatter の `color` で `red | yellow | green | blue | purple` から選択
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
function hello(name: string) {
|
|
24
|
-
console.log(`Hello, ${name}`)
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
> ヒント: 最初の h1 は`image`, `image-scroll`Layout以外で固定ヘッダになります。
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
layout: two-cols
|
|
32
|
-
title: 2カラムレイアウト
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
::left::
|
|
36
|
-
### 左側
|
|
37
|
-
- ポイント1
|
|
38
|
-
- ポイント2
|
|
39
|
-
- ポイント3
|
|
40
|
-
|
|
41
|
-
固定ヘッダとフッターの余白を意識した構造。
|
|
42
|
-
|
|
43
|
-
::right::
|
|
44
|
-
### 右側
|
|
45
|
-
```js
|
|
46
|
-
const nums = [1,2,3]
|
|
47
|
-
const doubled = nums.map(n=>n*2)
|
|
48
|
-
console.log(doubled)
|
|
49
|
-
```
|
|
50
|
-
図表 / コード / 説明などを分割表示。
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
# QuestionList 基本
|
|
55
|
-
|
|
56
|
-
<QuestionList
|
|
57
|
-
:items="[
|
|
58
|
-
'正答のためのリストコンポーネント **Markdown OK**',
|
|
59
|
-
{
|
|
60
|
-
text: '2番目 (子を含む)',
|
|
61
|
-
items: [
|
|
62
|
-
{ label: 'A', text: 'A の内容'},
|
|
63
|
-
{ label: 'B', text: 'B の内容'},
|
|
64
|
-
{ text: 'さらにネスト', items: ['深い1', '深い2'] }
|
|
65
|
-
]
|
|
66
|
-
},
|
|
67
|
-
{ label: '★', text: 'labelスタイルも複数用意しカスタムも可能。' }
|
|
68
|
-
]"
|
|
69
|
-
:styles="['decimal-circle','katakana-paren','loweralpha-dot']"
|
|
70
|
-
/>
|
|
71
|
-
|
|
72
|
-
---
|
|
73
|
-
|
|
74
|
-
# QuestionList start 指定
|
|
75
|
-
|
|
76
|
-
配列で各階層の開始番号/文字を指定可能。
|
|
77
|
-
|
|
78
|
-
<QuestionList
|
|
79
|
-
:items="[
|
|
80
|
-
{ text: '大問1: サブ', items: ['1つ目','2つ目'] },
|
|
81
|
-
{ text: '大問2: サブ', items: ['A','B','C'] }
|
|
82
|
-
]"
|
|
83
|
-
:styles="['decimal-q','hiragana-paren']"
|
|
84
|
-
:start="[1,1]"
|
|
85
|
-
/>
|
|
86
|
-
|
|
87
|
-
---
|
|
88
|
-
|
|
89
|
-
# KaTeX と QuestionList
|
|
90
|
-
|
|
91
|
-
KaTeX コンポーネント `<KaTexReveal>` を QuestionList 内で使用可能。
|
|
92
|
-
|
|
93
|
-
<KaTexReveal formula="\\int_0^{2\\pi} \\sin x\\,dx = 0" block class="text-2xl" />
|
|
94
|
-
|
|
95
|
-
<KaTexReveal
|
|
96
|
-
formula="E = mc^2"
|
|
97
|
-
:block="false"
|
|
98
|
-
class="text-primary font-bold"
|
|
99
|
-
v-click="1"
|
|
100
|
-
/>
|
|
101
|
-
|
|
102
|
-
---
|
|
103
|
-
layout: center
|
|
104
|
-
---
|
|
105
|
-
|
|
106
|
-
# KaTeX と Markdown の混在
|
|
107
|
-
|
|
108
|
-
<QuestionList
|
|
109
|
-
:items="[
|
|
110
|
-
{
|
|
111
|
-
label: '①',
|
|
112
|
-
text: 'Markdownと **KaTeX** を混在',
|
|
113
|
-
items: [
|
|
114
|
-
{ formula: 'a^2 + b^2 = c^2', block: true, class: 'text-lg text-center', tex: true },
|
|
115
|
-
{ text: '$$\\frac{d}{dx} \\sin x = \\cos x$$' }
|
|
116
|
-
]
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
label: '②',
|
|
120
|
-
formula: '\\sum_{k=1}^n k = \\frac{n(n+1)}{2}',
|
|
121
|
-
block: false,
|
|
122
|
-
class: 'text-xl'
|
|
123
|
-
}
|
|
124
|
-
]"
|
|
125
|
-
:styles="['decimal-circle','loweralpha-paren']"
|
|
126
|
-
:start="[1]"
|
|
127
|
-
/>
|
|
128
|
-
|
|
129
|
-
---
|
|
130
|
-
layout: image
|
|
131
|
-
image: 'https://images.unsplash.com/photo-1501594907352-04cda38ebc29?auto=format&fit=crop&w=2370&q=80'
|
|
132
|
-
---
|
|
133
|
-
|
|
134
|
-
<TextBox :x="80" :y="140" :width="360" v-click="1">1番目に表示される注釈。</TextBox>
|
|
135
|
-
<TextBox :x="200" :y="380" :width="340" textBg="green" v-click="2">背景色付き 2番目。</TextBox>
|
|
136
|
-
<TextBox :x="500" :y="120" :width="300" color="red">常時表示 (赤文字)。</TextBox>
|
|
137
|
-
<TextBox :x="40" :y="20" :width="420" textBg="yellow" v-click="3">最後に表示される黄色背景。</TextBox>
|
|
138
|
-
|
|
139
|
-
---
|
|
140
|
-
layout: image-scroll
|
|
141
|
-
image: 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&w=2400&q=80'
|
|
142
|
-
imageScroll:
|
|
143
|
-
offsetY: -120
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
## image-scroll: 縦長画像を「横幅フィット+縦スクロール」で表示
|
|
147
|
-
|
|
148
|
-
- 画像はスライド幅に合わせて表示
|
|
149
|
-
- 縦方向はホイール / タッチでスクロール
|
|
150
|
-
- `offsetY` で初期位置を調整(中心からのpx)
|
|
151
|
-
|
|
152
|
-
ここでは `offsetY: -120` で少し上寄せしています。
|
|
153
|
-
|
|
154
|
-
---
|
|
155
|
-
layout: image-scroll
|
|
156
|
-
image: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=2400&q=80'
|
|
157
|
-
---
|
|
158
|
-
|
|
159
|
-
## offsetY の例(中心から上へ 300px)
|
|
160
|
-
|
|
161
|
-
背景の見せたい位置が決まっているときに便利です。
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
# ユーティリティ
|
|
166
|
-
|
|
167
|
-
普通のテキストと <span class="text-highlight">ハイライト</span>。
|
|
168
|
-
|
|
169
|
-
<div class="card mt-8" v-click="1">
|
|
170
|
-
<h3>カードスタイル</h3>
|
|
171
|
-
<p>重要事項をまとめるのに便利です。</p>
|
|
172
|
-
</div>
|
|
173
|
-
|
|
174
|
-
---
|
|
175
|
-
|
|
176
|
-
# フッターとショートカット
|
|
177
|
-
|
|
178
|
-
このテーマでは Enter / Backspace キーで進行を制御できます。
|
|
179
|
-
|
|
180
|
-
- Enter: 次のスライド / v-click
|
|
181
|
-
- Backspace: 前へ戻る
|
|
182
|
-
- フッター: (cover / image / image-scroll 以外) 日付 + ページ番号
|
|
183
|
-
|
|
184
|
-
---
|
|
185
|
-
|
|
186
|
-
# まとめ
|
|
187
|
-
|
|
188
|
-
- レイアウト: cover / two-cols / image / image-scroll / end
|
|
189
|
-
- コンポーネント: QuestionList / TextBox / KaTexReveal
|
|
190
|
-
- 自動フッター & 固定ヘッダ
|
|
191
|
-
- ラベルスタイル多彩 & Markdown 埋め込み
|
|
192
|
-
|
|
193
|
-
ご利用ありがとうございます
|
|
194
|
-
|
|
195
|
-
---
|
|
196
|
-
layout: end
|
|
197
|
-
---
|