slidev-theme-gtlabo 1.0.0

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.
@@ -0,0 +1,557 @@
1
+ <template>
2
+ <component
3
+ :is="containerTag"
4
+ :class="['math-text-container', containerClass]"
5
+ >
6
+ <!-- スロットが使われている場合 -->
7
+ <template v-if="hasSlotContent">
8
+ <template
9
+ v-for="(segment, index) in processedSlotSegments"
10
+ :key="`slot-${index}`"
11
+ >
12
+ <span
13
+ v-if="segment.type === 'text'"
14
+ :class="textClass"
15
+ v-html="processTextContent(segment.content)"
16
+ />
17
+ <span
18
+ v-else-if="segment.type === 'inline-math'"
19
+ :ref="el => setMathElement(`slot-inline-${index}`, el)"
20
+ :class="['inline-math-formula', inlineMathClass]"
21
+ :data-formula="segment.content"
22
+ :data-display-mode="false"
23
+ >
24
+ {{ segment.content }}
25
+ </span>
26
+ <div
27
+ v-else-if="segment.type === 'block-math'"
28
+ :ref="el => setMathElement(`slot-block-${index}`, el)"
29
+ :class="['block-math-formula', blockMathClass]"
30
+ :data-formula="segment.content"
31
+ :data-display-mode="true"
32
+ >
33
+ {{ segment.content }}
34
+ </div>
35
+ <component
36
+ :is="segment.component"
37
+ v-else-if="segment.type === 'component'"
38
+ v-bind="segment.props"
39
+ v-html="segment.content"
40
+ />
41
+ </template>
42
+ </template>
43
+
44
+ <!-- textプロパティが使われている場合 -->
45
+ <template v-else-if="text">
46
+ <template
47
+ v-for="(segment, index) in processedTextSegments"
48
+ :key="`text-${index}`"
49
+ >
50
+ <span
51
+ v-if="segment.type === 'text'"
52
+ :class="textClass"
53
+ v-html="processTextContent(segment.content)"
54
+ />
55
+ <span
56
+ v-else-if="segment.type === 'inline-math'"
57
+ :ref="el => setMathElement(`text-inline-${index}`, el)"
58
+ :class="['inline-math-formula', inlineMathClass]"
59
+ :data-formula="segment.content"
60
+ :data-display-mode="false"
61
+ >
62
+ {{ segment.content }}
63
+ </span>
64
+ <div
65
+ v-else-if="segment.type === 'block-math'"
66
+ :ref="el => setMathElement(`text-block-${index}`, el)"
67
+ :class="['block-math-formula', blockMathClass]"
68
+ :data-formula="segment.content"
69
+ :data-display-mode="true"
70
+ >
71
+ {{ segment.content }}
72
+ </div>
73
+ </template>
74
+ </template>
75
+ </component>
76
+ </template>
77
+
78
+ <script setup>
79
+ import { computed, ref, onMounted, nextTick, watch, useSlots } from 'vue'
80
+
81
+ const props = defineProps({
82
+ text: {
83
+ type: String,
84
+ default: ''
85
+ },
86
+ containerTag: {
87
+ type: String,
88
+ default: 'span'
89
+ },
90
+ containerClass: {
91
+ type: String,
92
+ default: ''
93
+ },
94
+ inlineMathClass: {
95
+ type: String,
96
+ default: ''
97
+ },
98
+ blockMathClass: {
99
+ type: String,
100
+ default: ''
101
+ },
102
+ textClass: {
103
+ type: String,
104
+ default: ''
105
+ },
106
+ // シンプルモード(SimpleMathText相当)
107
+ simple: {
108
+ type: Boolean,
109
+ default: false
110
+ },
111
+ // Markdownを無効にする
112
+ disableMarkdown: {
113
+ type: Boolean,
114
+ default: false
115
+ },
116
+ // カスタム区切り文字パターン
117
+ customDelimiters: {
118
+ type: Array,
119
+ default: null
120
+ }
121
+ })
122
+
123
+ const slots = useSlots()
124
+ const mathElements = ref({})
125
+
126
+ // スロットにコンテンツがあるかチェック
127
+ const hasSlotContent = computed(() => {
128
+ return slots.default && slots.default().length > 0
129
+ })
130
+
131
+ // テキストコンテンツを処理(Markdown + 改行 + HTMLエスケープ)
132
+ const processTextContent = (content) => {
133
+ if (!content) return ''
134
+
135
+ // まずHTMLエスケープ(数式とMarkdownは後で処理)
136
+ let processed = content
137
+ .replace(/&/g, '&amp;')
138
+ .replace(/</g, '&lt;')
139
+ .replace(/>/g, '&gt;')
140
+ .replace(/"/g, '&quot;')
141
+ .replace(/'/g, '&#x27;')
142
+
143
+ // Markdownが有効な場合の処理
144
+ if (!props.disableMarkdown) {
145
+ // リストアイテム(- で始まる行)
146
+ processed = processed.replace(/^- (.+)$/gm, '<li>$1</li>')
147
+
148
+ // 連続するliタグをulで囲む
149
+ processed = processed.replace(/(<li>.*<\/li>(?:\n<li>.*<\/li>)*)/g, '<ul>$1</ul>')
150
+
151
+ // 番号付きリスト(1. で始まる行)
152
+ processed = processed.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
153
+
154
+ // 連続する番号付きliタグをolで囲む(ulの後に処理)
155
+ processed = processed.replace(/(<li>.*<\/li>(?:\n<li>.*<\/li>)*)/g, (match) => {
156
+ // 既にulで囲まれていない場合のみolで囲む
157
+ if (!match.includes('<ul>')) {
158
+ return '<ol>' + match + '</ol>'
159
+ }
160
+ return match
161
+ })
162
+
163
+ // 太字 **text** または __text__
164
+ processed = processed.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
165
+ processed = processed.replace(/__(.*?)__/g, '<strong>$1</strong>')
166
+
167
+ // イタリック *text* または _text_(ただし数式の*は除外)
168
+ processed = processed.replace(/(?<!\$[^$]*)\*([^*\n]+?)\*(?![^$]*\$)/g, '<em>$1</em>')
169
+ processed = processed.replace(/(?<!\$[^$]*)_([^_\n]+?)_(?![^$]*\$)/g, '<em>$1</em>')
170
+
171
+ // コードスパン `code`
172
+ processed = processed.replace(/`([^`]+)`/g, '<code>$1</code>')
173
+
174
+ // リンク [text](url)
175
+ processed = processed.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
176
+
177
+ // 見出し ### text
178
+ processed = processed.replace(/^### (.+)$/gm, '<h3>$1</h3>')
179
+ processed = processed.replace(/^## (.+)$/gm, '<h2>$1</h2>')
180
+ processed = processed.replace(/^# (.+)$/gm, '<h1>$1</h1>')
181
+
182
+ // 水平線 ---
183
+ processed = processed.replace(/^---$/gm, '<hr>')
184
+
185
+ // 引用 > text
186
+ processed = processed.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
187
+ }
188
+
189
+ // 改行を<br>タグに変換(ただし、HTMLタグの直後は除く)
190
+ processed = processed.replace(/\n(?!<)/g, '<br>')
191
+
192
+ // HTMLタグ間の不要な<br>を除去
193
+ processed = processed.replace(/<\/([^>]+)><br><([^>\/][^>]*)>/g, '</$1><$2>')
194
+ processed = processed.replace(/<\/li><br>/g, '</li>')
195
+ processed = processed.replace(/<br><li>/g, '<li>')
196
+ processed = processed.replace(/<\/ul><br>/g, '</ul>')
197
+ processed = processed.replace(/<\/ol><br>/g, '</ol>')
198
+ processed = processed.replace(/<br><ul>/g, '<ul>')
199
+ processed = processed.replace(/<br><ol>/g, '<ol>')
200
+
201
+ return processed
202
+ }
203
+
204
+ // スロットの内容をテキストに変換(改良版)
205
+ const slotTextContent = computed(() => {
206
+ if (!hasSlotContent.value) return ''
207
+
208
+ const extractTextFromVNode = (vnode) => {
209
+ if (typeof vnode === 'string') return vnode
210
+ if (typeof vnode === 'number') return String(vnode)
211
+ if (!vnode) return ''
212
+
213
+ if (Array.isArray(vnode)) {
214
+ return vnode.map(extractTextFromVNode).join('')
215
+ }
216
+
217
+ // テキストノードの処理
218
+ if (vnode.type === 'text' || vnode.type === Text || typeof vnode.children === 'string') {
219
+ return vnode.children || vnode.text || ''
220
+ }
221
+
222
+ // 改行要素の処理
223
+ if (vnode.type === 'br') {
224
+ return '\n'
225
+ }
226
+
227
+ // 子要素がある場合の再帰処理
228
+ if (Array.isArray(vnode.children)) {
229
+ return vnode.children.map(extractTextFromVNode).join('')
230
+ }
231
+
232
+ // コンポーネントの場合はプレースホルダーを返す
233
+ if (typeof vnode.type === 'object' || typeof vnode.type === 'function') {
234
+ return `<COMPONENT:${vnode.type.name || 'Unknown'}>`
235
+ }
236
+
237
+ return vnode.children || ''
238
+ }
239
+
240
+ const result = slots.default().map(extractTextFromVNode).join('')
241
+ return result
242
+ })
243
+
244
+ // デフォルトの区切り文字パターン
245
+ const getDelimiters = () => {
246
+ if (props.simple) {
247
+ // シンプルモード:$...$のみ
248
+ return [
249
+ {
250
+ pattern: /\$([^$\n]+)\$/g,
251
+ type: 'inline-math',
252
+ process: (match) => match[1].trim()
253
+ }
254
+ ]
255
+ }
256
+
257
+ return props.customDelimiters || [
258
+ // LaTeX環境(最優先)
259
+ {
260
+ pattern: /\\begin\{(align\*?|equation\*?|gather\*?|multline\*?|split|eqnarray\*?|alignat\*?|flalign\*?)\}([\s\S]*?)\\end\{\1\}/g,
261
+ type: 'block-math',
262
+ process: (match) => match[0],
263
+ priority: 0
264
+ },
265
+ // $$...$$(ブロック数式)
266
+ {
267
+ pattern: /\$\$([^$]*(?:\$(?!\$)[^$]*)*)\$\$/g,
268
+ type: 'block-math',
269
+ process: (match) => match[1].trim(),
270
+ priority: 1
271
+ },
272
+ // $...$(インライン数式)
273
+ {
274
+ pattern: /\$([^$\n]+)\$/g,
275
+ type: 'inline-math',
276
+ process: (match) => match[1].trim(),
277
+ priority: 2
278
+ },
279
+ // \(...\)(インライン数式)
280
+ {
281
+ pattern: /\\\(([^)]+)\\\)/g,
282
+ type: 'inline-math',
283
+ process: (match) => match[1].trim(),
284
+ priority: 3
285
+ },
286
+ // \[...\](ブロック数式)
287
+ {
288
+ pattern: /\\\[([^\]]+)\\\]/g,
289
+ type: 'block-math',
290
+ process: (match) => match[1].trim(),
291
+ priority: 4
292
+ }
293
+ ]
294
+ }
295
+
296
+ // テキストを解析してセグメントに分割
297
+ const parseTextToSegments = (inputText) => {
298
+ if (!inputText) return []
299
+
300
+ const delimiters = getDelimiters()
301
+ const segments = []
302
+ let currentText = inputText
303
+ const mathBlocks = []
304
+
305
+ // 全ての数式パターンを検出
306
+ delimiters.forEach((delimiter, delimiterIndex) => {
307
+ let match
308
+ const regex = new RegExp(delimiter.pattern.source, delimiter.pattern.flags)
309
+
310
+ while ((match = regex.exec(currentText)) !== null) {
311
+ mathBlocks.push({
312
+ start: match.index,
313
+ end: match.index + match[0].length,
314
+ content: delimiter.process ? delimiter.process(match) : match[1].trim(),
315
+ type: delimiter.type,
316
+ original: match[0],
317
+ priority: delimiter.priority || delimiterIndex
318
+ })
319
+ }
320
+ })
321
+
322
+ // 開始位置でソート、重複する場合は優先度で決定
323
+ mathBlocks.sort((a, b) => {
324
+ if (a.start !== b.start) return a.start - b.start
325
+ return a.priority - b.priority
326
+ })
327
+
328
+ // 重複する範囲を除去
329
+ const filteredBlocks = []
330
+ for (let i = 0; i < mathBlocks.length; i++) {
331
+ const current = mathBlocks[i]
332
+ let shouldAdd = true
333
+
334
+ for (let j = filteredBlocks.length - 1; j >= 0; j--) {
335
+ const existing = filteredBlocks[j]
336
+
337
+ if (!(current.end <= existing.start || current.start >= existing.end)) {
338
+ if (current.priority < existing.priority ||
339
+ (current.priority === existing.priority && (current.end - current.start) > (existing.end - existing.start))) {
340
+ filteredBlocks.splice(j, 1)
341
+ } else {
342
+ shouldAdd = false
343
+ break
344
+ }
345
+ }
346
+ }
347
+
348
+ if (shouldAdd) {
349
+ filteredBlocks.push(current)
350
+ }
351
+ }
352
+
353
+ filteredBlocks.sort((a, b) => a.start - b.start)
354
+
355
+ // セグメントを構築
356
+ let lastIndex = 0
357
+
358
+ filteredBlocks.forEach(block => {
359
+ // 数式の前のテキスト部分
360
+ if (block.start > lastIndex) {
361
+ const textContent = currentText.slice(lastIndex, block.start)
362
+ if (textContent) {
363
+ // 改行で分割してセグメントを作成
364
+ const lines = textContent.split('\n')
365
+ lines.forEach((line, lineIndex) => {
366
+ if (line || lineIndex === 0) { // 空行も最初の行なら保持
367
+ segments.push({
368
+ type: 'text',
369
+ content: line
370
+ })
371
+ }
372
+ // 改行を追加(最後の行以外)
373
+ if (lineIndex < lines.length - 1) {
374
+ segments.push({
375
+ type: 'text',
376
+ content: '\n'
377
+ })
378
+ }
379
+ })
380
+ }
381
+ }
382
+
383
+ // 数式部分
384
+ segments.push({
385
+ type: block.type,
386
+ content: block.content
387
+ })
388
+
389
+ lastIndex = block.end
390
+ })
391
+
392
+ // 残りのテキスト
393
+ if (lastIndex < currentText.length) {
394
+ const textContent = currentText.slice(lastIndex)
395
+ if (textContent) {
396
+ const lines = textContent.split('\n')
397
+ lines.forEach((line, lineIndex) => {
398
+ if (line || lineIndex === 0) {
399
+ segments.push({
400
+ type: 'text',
401
+ content: line
402
+ })
403
+ }
404
+ if (lineIndex < lines.length - 1) {
405
+ segments.push({
406
+ type: 'text',
407
+ content: '\n'
408
+ })
409
+ }
410
+ })
411
+ }
412
+ }
413
+
414
+ return segments
415
+ }
416
+
417
+ // textプロパティから処理されたセグメント
418
+ const processedTextSegments = computed(() => {
419
+ if (!props.text) return []
420
+ return parseTextToSegments(props.text)
421
+ })
422
+
423
+ // スロットから処理されたセグメント
424
+ const processedSlotSegments = computed(() => {
425
+ if (!hasSlotContent.value) return []
426
+ return parseTextToSegments(slotTextContent.value)
427
+ })
428
+
429
+ // 数式要素の参照を設定
430
+ const setMathElement = (key, el) => {
431
+ if (el) {
432
+ mathElements.value[key] = el
433
+ }
434
+ }
435
+
436
+ // 数式レンダリング
437
+ const renderMathElements = async () => {
438
+ await nextTick()
439
+
440
+ for (const [key, element] of Object.entries(mathElements.value)) {
441
+ if (element && element.dataset.formula) {
442
+ const formula = element.dataset.formula
443
+ const displayMode = element.dataset.displayMode === 'true'
444
+
445
+ try {
446
+ let katex = null
447
+
448
+ if (typeof window !== 'undefined') {
449
+ katex = window.katex || window.KaTeX
450
+
451
+ if (!katex) {
452
+ try {
453
+ const katexModule = await import('katex')
454
+ katex = katexModule.default || katexModule
455
+ window.katex = katex
456
+ } catch (e) {
457
+ // KaTeXが利用できない場合のフォールバック
458
+ if (props.simple) {
459
+ element.innerHTML = `<span style="font-style: italic; color: #0066cc; background: #f0f8ff; padding: 1px 3px; border-radius: 3px;">${formula}</span>`
460
+ } else {
461
+ element.textContent = formula
462
+ }
463
+ continue
464
+ }
465
+ }
466
+ }
467
+
468
+ if (katex && katex.render) {
469
+ katex.render(formula, element, {
470
+ displayMode: displayMode,
471
+ throwOnError: false,
472
+ errorColor: '#cc0000',
473
+ strict: false,
474
+ fleqn: false,
475
+ macros: {
476
+ "\\R": "\\mathbb{R}",
477
+ "\\N": "\\mathbb{N}",
478
+ "\\Z": "\\mathbb{Z}",
479
+ "\\Q": "\\mathbb{Q}",
480
+ "\\C": "\\mathbb{C}",
481
+ }
482
+ })
483
+ }
484
+ } catch (error) {
485
+ console.warn(`Math rendering failed for formula: ${formula}`, error)
486
+ element.textContent = formula
487
+ }
488
+ }
489
+ }
490
+ }
491
+
492
+ onMounted(() => {
493
+ renderMathElements()
494
+ })
495
+
496
+ watch([() => props.text, hasSlotContent], () => {
497
+ mathElements.value = {}
498
+ nextTick(() => {
499
+ renderMathElements()
500
+ })
501
+ }, { flush: 'post' })
502
+
503
+ watch([processedTextSegments, processedSlotSegments], () => {
504
+ nextTick(() => {
505
+ renderMathElements()
506
+ })
507
+ }, { flush: 'post' })
508
+ </script>
509
+
510
+ <style scoped>
511
+ .math-text-container {
512
+ line-height: 1.6;
513
+ }
514
+
515
+ .math-text-container.inline-container {
516
+ display: inline;
517
+ }
518
+
519
+ .math-text-container.block-container {
520
+ display: block;
521
+ }
522
+
523
+ .inline-math-formula {
524
+ display: inline;
525
+ margin: 0 2px;
526
+ }
527
+
528
+ .block-math-formula {
529
+ display: block;
530
+ margin: 1em 0;
531
+ text-align: center;
532
+ }
533
+
534
+ /* KaTeX用の基本スタイリング */
535
+ .inline-math-formula :deep(.katex),
536
+ .block-math-formula :deep(.katex) {
537
+ font-size: inherit !important;
538
+ }
539
+
540
+ .inline-math-formula :deep(.katex-html) {
541
+ vertical-align: baseline;
542
+ }
543
+
544
+ .block-math-formula :deep(.katex-display) {
545
+ margin: 0;
546
+ text-align: center;
547
+ }
548
+
549
+ /* シンプルモード用のフォールバックスタイル */
550
+ .math-text-container.simple-mode .inline-math-formula {
551
+ font-style: italic;
552
+ color: #0066cc;
553
+ background: #f0f8ff;
554
+ padding: 1px 3px;
555
+ border-radius: 3px;
556
+ }
557
+ </style>
@@ -0,0 +1,24 @@
1
+ <template>
2
+ <!-- セクション区切り用の目次ページ -->
3
+ <TableOfContents
4
+ :next-chapter="nextChapter"
5
+ :current-chapter="currentChapter"
6
+ />
7
+ </template>
8
+
9
+ <script setup>
10
+ import TableOfContents from './TableOfContents.vue'
11
+
12
+ const props = defineProps({
13
+ nextChapter: {
14
+ type: String,
15
+ required: true,
16
+ description: '次に始まる章のキー(例: "c2", "c3", "ap1")'
17
+ },
18
+ currentChapter: {
19
+ type: String,
20
+ required: false,
21
+ description: '現在の章のキー(進捗表示用)'
22
+ }
23
+ })
24
+ </script>
@@ -0,0 +1,70 @@
1
+ <template>
2
+ <div class="flex items-center gap-4 my-2">
3
+ <!-- 左側のバー -->
4
+ <div
5
+ class="w-3 h-10 rounded-sm flex-shrink-0"
6
+ :class="barColorClass"
7
+ :style="customBarStyle"
8
+ />
9
+
10
+ <!-- タイトルテキスト -->
11
+ <h2
12
+ class="text-5xl font-extrabold m-0 leading-tight tracking-wide"
13
+ :class="textColorClass"
14
+ :style="customTextStyle"
15
+ >
16
+ <!-- スロットがある場合はスロットを使用、ない場合はtitleプロパティを使用 -->
17
+ <slot v-if="$slots.default" />
18
+ <span v-else>{{ title }}</span>
19
+ </h2>
20
+ </div>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import { computed } from 'vue'
25
+
26
+ const props = defineProps<{
27
+ title?: string
28
+ color?: string
29
+ }>()
30
+
31
+ // UnoCSS用のカラークラス
32
+ const barColorClass = computed(() => {
33
+ if (!props.color) return 'bg-sky-800'
34
+
35
+ // UnoCSS/Tailwindのカラー名かチェック
36
+ const colorPattern = /^(red|blue|green|yellow|purple|pink|indigo|cyan|teal|orange|amber|lime|emerald|sky|violet|fuchsia|rose|slate|gray|zinc|neutral|stone)(-\d{1,3})?$/
37
+
38
+ if (colorPattern.test(props.color)) {
39
+ return `bg-${props.color}`
40
+ }
41
+
42
+ // カスタムカラーの場合は空文字を返してstyleで適用
43
+ return ''
44
+ })
45
+
46
+ const textColorClass = computed(() => {
47
+ if (!props.color) return 'text-gray-900'
48
+
49
+ // UnoCSS/Tailwindのカラー名かチェック
50
+ const colorPattern = /^(red|blue|green|yellow|purple|pink|indigo|cyan|teal|orange|amber|lime|emerald|sky|violet|fuchsia|rose|slate|gray|zinc|neutral|stone)(-\d{1,3})?$/
51
+
52
+ if (colorPattern.test(props.color)) {
53
+ return `text-${props.color}`
54
+ }
55
+
56
+ // カスタムカラーの場合は空文字を返してstyleで適用
57
+ return ''
58
+ })
59
+
60
+ // カスタムカラー用のスタイル
61
+ const customBarStyle = computed(() => {
62
+ if (!props.color || barColorClass.value) return {}
63
+ return { backgroundColor: props.color }
64
+ })
65
+
66
+ const customTextStyle = computed(() => {
67
+ if (!props.color || textColorClass.value) return {}
68
+ return { color: props.color }
69
+ })
70
+ </script>