slidev-theme-gtlabo 2.1.7 → 2.1.9
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/components/AlertBox.vue +58 -0
- package/components/Citation.vue +51 -104
- package/components/EqRef.vue +33 -0
- package/components/HighlightText.vue +40 -0
- package/components/MathText.vue +52 -117
- package/components/SubSectionTitle.vue +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<!-- AlertBox.vue -->
|
|
2
|
+
<template>
|
|
3
|
+
<div :class="[
|
|
4
|
+
'border-l-8 p-3 shadow-md rounded-r-lg gap-4 flex items-center',
|
|
5
|
+
variants[variant].bg,
|
|
6
|
+
variants[variant].border,
|
|
7
|
+
]">
|
|
8
|
+
<div :class="['w-10 h-10 flex-shrink-0 ml-2', variants[variant].iconColor]">
|
|
9
|
+
<slot name="icon">
|
|
10
|
+
<lucide-frown v-if="variant === 'warning'" class="w-10 h-10" />
|
|
11
|
+
<lucide-lightbulb v-else-if="variant === 'info'" class="w-10 h-10" />
|
|
12
|
+
<lucide-circle-check v-else-if="variant === 'success'" class="w-10 h-10" />
|
|
13
|
+
<lucide-search-check v-else-if="variant === 'result'" class="w-10 h-10" />
|
|
14
|
+
<lucide-circle-x v-else class="w-10 h-10" />
|
|
15
|
+
</slot>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="text-2xl font-bold text-slate-800 leading-relaxed">
|
|
18
|
+
<slot />
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup>
|
|
24
|
+
const props = defineProps({
|
|
25
|
+
variant: {
|
|
26
|
+
type: String,
|
|
27
|
+
default: 'warning',
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const variants = {
|
|
32
|
+
warning: {
|
|
33
|
+
bg: 'bg-slate-50',
|
|
34
|
+
border: 'border-slate-600',
|
|
35
|
+
iconColor: 'text-red-500',
|
|
36
|
+
},
|
|
37
|
+
info: {
|
|
38
|
+
bg: 'bg-gradient-to-r from-slate-50 to-white',
|
|
39
|
+
border: 'border-indigo-600',
|
|
40
|
+
iconColor: 'text-yellow-500',
|
|
41
|
+
},
|
|
42
|
+
success: {
|
|
43
|
+
bg: 'bg-green-50',
|
|
44
|
+
border: 'border-green-600',
|
|
45
|
+
iconColor: 'text-green-500',
|
|
46
|
+
},
|
|
47
|
+
error: {
|
|
48
|
+
bg: 'bg-red-50',
|
|
49
|
+
border: 'border-red-600',
|
|
50
|
+
iconColor: 'text-red-500',
|
|
51
|
+
},
|
|
52
|
+
result: {
|
|
53
|
+
bg: 'bg-gradient-to-r from-slate-50 to-white',
|
|
54
|
+
border: 'border-indigo-600',
|
|
55
|
+
iconColor: 'text-green-500',
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
</script>
|
package/components/Citation.vue
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<span>
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
<sup v-if="citationStyle === 'numbered'"
|
|
4
|
+
class="text-blue-600 font-semibold cursor-help"
|
|
5
|
+
:title="formattedCitation">
|
|
6
|
+
{{ displayText }}
|
|
6
7
|
</sup>
|
|
8
|
+
<span v-else
|
|
9
|
+
class="text-blue-600 font-semibold cursor-help"
|
|
10
|
+
:title="formattedCitation">
|
|
11
|
+
{{ displayText }}
|
|
12
|
+
</span>
|
|
7
13
|
</span>
|
|
8
14
|
</template>
|
|
9
15
|
|
|
@@ -13,141 +19,91 @@ import { useSlideContext } from '@slidev/client'
|
|
|
13
19
|
|
|
14
20
|
const { $slidev, $page } = useSlideContext()
|
|
15
21
|
|
|
16
|
-
// $page を数値として取得するヘルパー
|
|
17
22
|
const getPageNumber = () => {
|
|
18
|
-
if (isRef($page))
|
|
19
|
-
return $page.value
|
|
20
|
-
}
|
|
23
|
+
if (isRef($page)) return $page.value
|
|
21
24
|
return $page
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
// 方法1: frontmatterから取得(従来通り)
|
|
25
27
|
const frontmatterCitations = $slidev.configs.citations || {}
|
|
26
|
-
|
|
27
|
-
// 方法2: injectから取得(外部ファイル対応)
|
|
28
28
|
const injectedCitations = inject('citations', {})
|
|
29
|
-
|
|
30
|
-
// 両方をマージ(frontmatter優先)
|
|
31
29
|
const citations = computed(() => ({
|
|
32
30
|
...injectedCitations,
|
|
33
31
|
...frontmatterCitations
|
|
34
32
|
}))
|
|
35
33
|
|
|
34
|
+
// slides.md の frontmatter から citationStyle を取得
|
|
35
|
+
const citationStyle = $slidev.configs.citationStyle || 'numbered'
|
|
36
|
+
|
|
36
37
|
const props = defineProps({
|
|
37
|
-
id: {
|
|
38
|
-
type: String,
|
|
39
|
-
required: true
|
|
40
|
-
}
|
|
38
|
+
id: { type: String, required: true }
|
|
41
39
|
})
|
|
42
40
|
|
|
43
|
-
// ページごとの引用管理(slide-bottom.vue用)
|
|
44
41
|
if (!window.pageCitations) {
|
|
45
42
|
window.pageCitations = {
|
|
46
|
-
data: new Map(),
|
|
43
|
+
data: new Map(),
|
|
47
44
|
listeners: new Set()
|
|
48
45
|
}
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
const notifyListeners = () => {
|
|
52
49
|
window.pageCitations.listeners.forEach(listener => {
|
|
53
|
-
try { listener() } catch (e) {
|
|
50
|
+
try { listener() } catch (e) {}
|
|
54
51
|
})
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
const citationData = computed(() => {
|
|
59
|
-
return citations.value[props.id] || null
|
|
60
|
-
})
|
|
54
|
+
const citationData = computed(() => citations.value[props.id] || null)
|
|
61
55
|
|
|
62
|
-
//
|
|
56
|
+
// 番号形式: [1]
|
|
63
57
|
const citationNumber = computed(() => {
|
|
64
58
|
const citationsData = citations.value
|
|
65
|
-
if (!citationsData || !citationData.value)
|
|
66
|
-
return '?'
|
|
67
|
-
}
|
|
68
|
-
|
|
59
|
+
if (!citationsData || !citationData.value) return '?'
|
|
69
60
|
const keys = Object.keys(citationsData)
|
|
70
61
|
const index = keys.indexOf(props.id)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return '?'
|
|
62
|
+
return index >= 0 ? index + 1 : '?'
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// APA インライン形式: (first_author, year)
|
|
66
|
+
const apaInline = computed(() => {
|
|
67
|
+
if (!citationData.value) return '(?)'
|
|
68
|
+
const firstAuthor = citationData.value.first_author || '?'
|
|
69
|
+
const year = citationData.value.year || '?'
|
|
70
|
+
return `(${firstAuthor} ${year})`
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// スタイルに応じて表示テキストを切り替え
|
|
74
|
+
const displayText = computed(() => {
|
|
75
|
+
if (citationStyle === 'apa') return apaInline.value
|
|
76
|
+
return `[${citationNumber.value}]`
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
-
// フォーマットされた引用テキスト
|
|
80
79
|
const formattedCitation = computed(() => {
|
|
81
|
-
if (!citationData.value)
|
|
82
|
-
return '引用情報が見つかりません'
|
|
83
|
-
}
|
|
80
|
+
if (!citationData.value) return '引用情報が見つかりません'
|
|
84
81
|
return formatCitation(citationData.value)
|
|
85
82
|
})
|
|
86
83
|
|
|
87
|
-
// 引用をフォーマット
|
|
88
84
|
const formatCitation = (data) => {
|
|
89
|
-
if (!data)
|
|
90
|
-
return '引用情報が見つかりません'
|
|
91
|
-
}
|
|
92
|
-
|
|
85
|
+
if (!data) return '引用情報が見つかりません'
|
|
93
86
|
let citation = ''
|
|
94
|
-
|
|
95
|
-
if (data.
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (data.
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (data.
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (data.volume && data.number) {
|
|
108
|
-
citation += `, Vol. ${data.volume}, No. ${data.number}`
|
|
109
|
-
} else if (data.volume) {
|
|
110
|
-
citation += `, Vol. ${data.volume}`
|
|
111
|
-
} else if (data.number) {
|
|
112
|
-
citation += `, No. ${data.number}`
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (data.pages) {
|
|
116
|
-
citation += `, pp. ${data.pages}`
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (data.year) {
|
|
120
|
-
citation += citation ? ` (${data.year})` : data.year
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (data.publisher) {
|
|
124
|
-
citation += citation ? `, ${data.publisher}` : data.publisher
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (data.url) {
|
|
128
|
-
citation += citation ? `, ${data.url}` : data.url
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (data.issn) {
|
|
132
|
-
citation += `, ISSN: ${data.issn}`
|
|
133
|
-
}
|
|
134
|
-
|
|
87
|
+
if (data.author) citation += data.author
|
|
88
|
+
if (data.title) citation += citation ? `, "${data.title}"` : `"${data.title}"`
|
|
89
|
+
if (data.journal) citation += citation ? `, ${data.journal}` : data.journal
|
|
90
|
+
if (data.volume && data.number) citation += `, Vol. ${data.volume}, No. ${data.number}`
|
|
91
|
+
else if (data.volume) citation += `, Vol. ${data.volume}`
|
|
92
|
+
else if (data.number) citation += `, No. ${data.number}`
|
|
93
|
+
if (data.pages) citation += `, pp. ${data.pages}`
|
|
94
|
+
if (data.year) citation += citation ? ` (${data.year})` : data.year
|
|
95
|
+
if (data.publisher) citation += citation ? `, ${data.publisher}` : data.publisher
|
|
96
|
+
if (data.url) citation += citation ? `, ${data.url}` : data.url
|
|
97
|
+
if (data.issn) citation += `, ISSN: ${data.issn}`
|
|
135
98
|
return citation || '引用情報が不完全です'
|
|
136
99
|
}
|
|
137
100
|
|
|
138
|
-
// 現在のページに引用を登録(slide-bottom.vue用)
|
|
139
101
|
const registerCitation = () => {
|
|
140
102
|
const page = getPageNumber()
|
|
141
|
-
|
|
142
|
-
if (!page) {
|
|
143
|
-
console.warn('Citation: page is null or undefined')
|
|
144
|
-
return
|
|
145
|
-
}
|
|
146
|
-
|
|
103
|
+
if (!page) return
|
|
147
104
|
if (!window.pageCitations.data.has(page)) {
|
|
148
105
|
window.pageCitations.data.set(page, new Set())
|
|
149
106
|
}
|
|
150
|
-
|
|
151
107
|
const pageSet = window.pageCitations.data.get(page)
|
|
152
108
|
if (!pageSet.has(props.id)) {
|
|
153
109
|
pageSet.add(props.id)
|
|
@@ -155,28 +111,19 @@ const registerCitation = () => {
|
|
|
155
111
|
}
|
|
156
112
|
}
|
|
157
113
|
|
|
158
|
-
// 現在のページから引用を解除
|
|
159
114
|
const unregisterCitation = () => {
|
|
160
115
|
const page = getPageNumber()
|
|
161
116
|
if (!page) return
|
|
162
|
-
|
|
163
117
|
const pageSet = window.pageCitations.data.get(page)
|
|
164
118
|
if (pageSet) {
|
|
165
119
|
pageSet.delete(props.id)
|
|
166
|
-
if (pageSet.size === 0)
|
|
167
|
-
window.pageCitations.data.delete(page)
|
|
168
|
-
}
|
|
120
|
+
if (pageSet.size === 0) window.pageCitations.data.delete(page)
|
|
169
121
|
notifyListeners()
|
|
170
122
|
}
|
|
171
123
|
}
|
|
172
124
|
|
|
173
|
-
onMounted(() =>
|
|
174
|
-
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
onUnmounted(() => {
|
|
178
|
-
unregisterCitation()
|
|
179
|
-
})
|
|
125
|
+
onMounted(() => registerCitation())
|
|
126
|
+
onUnmounted(() => unregisterCitation())
|
|
180
127
|
</script>
|
|
181
128
|
|
|
182
129
|
<style scoped>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span class="eq-ref cursor-help" :title="tooltipText">({{ displayNumber }})</span>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
import { computed, inject } from 'vue'
|
|
7
|
+
|
|
8
|
+
const props = defineProps({
|
|
9
|
+
id: {
|
|
10
|
+
type: String,
|
|
11
|
+
required: true
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const equationRegistry = inject('equationRegistry', null)
|
|
16
|
+
|
|
17
|
+
const displayNumber = computed(() => {
|
|
18
|
+
if (!equationRegistry) return '?'
|
|
19
|
+
const num = equationRegistry.getNumber(props.id)
|
|
20
|
+
return num ?? '?'
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const tooltipText = computed(() => {
|
|
24
|
+
return `式 (${displayNumber.value})`
|
|
25
|
+
})
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<style scoped>
|
|
29
|
+
.eq-ref {
|
|
30
|
+
color: #2563eb;
|
|
31
|
+
font-weight: 500;
|
|
32
|
+
}
|
|
33
|
+
</style>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!-- HighlightText.vue として保存 -->
|
|
2
|
+
<template>
|
|
3
|
+
<span :class="[
|
|
4
|
+
'mx-1',
|
|
5
|
+
'font-bold',
|
|
6
|
+
variants[variant].text,
|
|
7
|
+
variants[variant].decoration,
|
|
8
|
+
]">
|
|
9
|
+
<slot />
|
|
10
|
+
</span>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup>
|
|
14
|
+
const props = defineProps({
|
|
15
|
+
variant: {
|
|
16
|
+
type: String,
|
|
17
|
+
default: 'orange',
|
|
18
|
+
validator: (v) => ['orange', 'indigo', 'red', 'green'].includes(v),
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const variants = {
|
|
23
|
+
orange: {
|
|
24
|
+
text: 'text-orange-700',
|
|
25
|
+
decoration: 'border-b-4 border-orange-300',
|
|
26
|
+
},
|
|
27
|
+
indigo: {
|
|
28
|
+
text: 'text-indigo-700',
|
|
29
|
+
decoration: 'underline decoration-indigo-300 decoration-4 underline-offset-8',
|
|
30
|
+
},
|
|
31
|
+
red: {
|
|
32
|
+
text: 'text-red-700',
|
|
33
|
+
decoration: 'border-b-4 border-red-300',
|
|
34
|
+
},
|
|
35
|
+
green: {
|
|
36
|
+
text: 'text-green-700',
|
|
37
|
+
decoration: 'border-b-4 border-green-300',
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
</script>
|
package/components/MathText.vue
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<component
|
|
3
3
|
:is="containerTag"
|
|
4
4
|
:class="['math-text-container', containerClass]"
|
|
5
|
+
:style="eq ? 'position: relative; display: block;' : ''"
|
|
5
6
|
>
|
|
6
7
|
<!-- スロットが使われている場合 -->
|
|
7
8
|
<template v-if="hasSlotContent">
|
|
@@ -72,11 +73,16 @@
|
|
|
72
73
|
</div>
|
|
73
74
|
</template>
|
|
74
75
|
</template>
|
|
76
|
+
|
|
77
|
+
<!-- 数式番号 -->
|
|
78
|
+
<span v-if="eq && eqNumber !== null" class="eq-number">
|
|
79
|
+
({{ eqNumber }})
|
|
80
|
+
</span>
|
|
75
81
|
</component>
|
|
76
82
|
</template>
|
|
77
83
|
|
|
78
84
|
<script setup>
|
|
79
|
-
import { computed, ref, onMounted, nextTick, watch, useSlots } from 'vue'
|
|
85
|
+
import { computed, ref, onMounted, onUnmounted, nextTick, watch, useSlots, inject } from 'vue'
|
|
80
86
|
|
|
81
87
|
const props = defineProps({
|
|
82
88
|
text: {
|
|
@@ -103,36 +109,42 @@ const props = defineProps({
|
|
|
103
109
|
type: String,
|
|
104
110
|
default: ''
|
|
105
111
|
},
|
|
106
|
-
// シンプルモード(SimpleMathText相当)
|
|
107
112
|
simple: {
|
|
108
113
|
type: Boolean,
|
|
109
114
|
default: false
|
|
110
115
|
},
|
|
111
|
-
// Markdownを無効にする
|
|
112
116
|
disableMarkdown: {
|
|
113
117
|
type: Boolean,
|
|
114
118
|
default: false
|
|
115
119
|
},
|
|
116
|
-
// カスタム区切り文字パターン
|
|
117
120
|
customDelimiters: {
|
|
118
121
|
type: Array,
|
|
119
122
|
default: null
|
|
123
|
+
},
|
|
124
|
+
eq: {
|
|
125
|
+
type: String,
|
|
126
|
+
default: null
|
|
120
127
|
}
|
|
121
128
|
})
|
|
122
129
|
|
|
130
|
+
// 数式レジストリ
|
|
131
|
+
const equationRegistry = inject('equationRegistry', null)
|
|
132
|
+
|
|
133
|
+
const eqNumber = computed(() => {
|
|
134
|
+
if (!props.eq || !equationRegistry) return null
|
|
135
|
+
return equationRegistry.getNumber(props.eq)
|
|
136
|
+
})
|
|
137
|
+
|
|
123
138
|
const slots = useSlots()
|
|
124
139
|
const mathElements = ref({})
|
|
125
140
|
|
|
126
|
-
// スロットにコンテンツがあるかチェック
|
|
127
141
|
const hasSlotContent = computed(() => {
|
|
128
142
|
return slots.default && slots.default().length > 0
|
|
129
143
|
})
|
|
130
144
|
|
|
131
|
-
// テキストコンテンツを処理(Markdown + 改行 + HTMLエスケープ)
|
|
132
145
|
const processTextContent = (content) => {
|
|
133
146
|
if (!content) return ''
|
|
134
147
|
|
|
135
|
-
// まずHTMLエスケープ(数式とMarkdownは後で処理)
|
|
136
148
|
let processed = content
|
|
137
149
|
.replace(/&/g, '&')
|
|
138
150
|
.replace(/</g, '<')
|
|
@@ -140,56 +152,28 @@ const processTextContent = (content) => {
|
|
|
140
152
|
.replace(/"/g, '"')
|
|
141
153
|
.replace(/'/g, ''')
|
|
142
154
|
|
|
143
|
-
// Markdownが有効な場合の処理
|
|
144
155
|
if (!props.disableMarkdown) {
|
|
145
|
-
// リストアイテム(- で始まる行)
|
|
146
156
|
processed = processed.replace(/^- (.+)$/gm, '<li>$1</li>')
|
|
147
|
-
|
|
148
|
-
// 連続するliタグをulで囲む
|
|
149
157
|
processed = processed.replace(/(<li>.*<\/li>(?:\n<li>.*<\/li>)*)/g, '<ul>$1</ul>')
|
|
150
|
-
|
|
151
|
-
// 番号付きリスト(1. で始まる行)
|
|
152
158
|
processed = processed.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
|
153
|
-
|
|
154
|
-
// 連続する番号付きliタグをolで囲む(ulの後に処理)
|
|
155
159
|
processed = processed.replace(/(<li>.*<\/li>(?:\n<li>.*<\/li>)*)/g, (match) => {
|
|
156
|
-
|
|
157
|
-
if (!match.includes('<ul>')) {
|
|
158
|
-
return '<ol>' + match + '</ol>'
|
|
159
|
-
}
|
|
160
|
+
if (!match.includes('<ul>')) return '<ol>' + match + '</ol>'
|
|
160
161
|
return match
|
|
161
162
|
})
|
|
162
|
-
|
|
163
|
-
// 太字 **text** または __text__
|
|
164
163
|
processed = processed.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
165
164
|
processed = processed.replace(/__(.*?)__/g, '<strong>$1</strong>')
|
|
166
|
-
|
|
167
|
-
// イタリック *text* または _text_(ただし数式の*は除外)
|
|
168
165
|
processed = processed.replace(/(?<!\$[^$]*)\*([^*\n]+?)\*(?![^$]*\$)/g, '<em>$1</em>')
|
|
169
166
|
processed = processed.replace(/(?<!\$[^$]*)_([^_\n]+?)_(?![^$]*\$)/g, '<em>$1</em>')
|
|
170
|
-
|
|
171
|
-
// コードスパン `code`
|
|
172
167
|
processed = processed.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
173
|
-
|
|
174
|
-
// リンク [text](url)
|
|
175
168
|
processed = processed.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
|
176
|
-
|
|
177
|
-
// 見出し ### text
|
|
178
169
|
processed = processed.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
|
179
170
|
processed = processed.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
180
171
|
processed = processed.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
|
181
|
-
|
|
182
|
-
// 水平線 ---
|
|
183
172
|
processed = processed.replace(/^---$/gm, '<hr>')
|
|
184
|
-
|
|
185
|
-
// 引用 > text
|
|
186
173
|
processed = processed.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
|
|
187
174
|
}
|
|
188
175
|
|
|
189
|
-
// 改行を<br>タグに変換(ただし、HTMLタグの直後は除く)
|
|
190
176
|
processed = processed.replace(/\n(?!<)/g, '<br>')
|
|
191
|
-
|
|
192
|
-
// HTMLタグ間の不要な<br>を除去
|
|
193
177
|
processed = processed.replace(/<\/([^>]+)><br><([^>\/][^>]*)>/g, '</$1><$2>')
|
|
194
178
|
processed = processed.replace(/<\/li><br>/g, '</li>')
|
|
195
179
|
processed = processed.replace(/<br><li>/g, '<li>')
|
|
@@ -201,7 +185,6 @@ const processTextContent = (content) => {
|
|
|
201
185
|
return processed
|
|
202
186
|
}
|
|
203
187
|
|
|
204
|
-
// スロットの内容をテキストに変換(改良版)
|
|
205
188
|
const slotTextContent = computed(() => {
|
|
206
189
|
if (!hasSlotContent.value) return ''
|
|
207
190
|
|
|
@@ -209,42 +192,23 @@ const slotTextContent = computed(() => {
|
|
|
209
192
|
if (typeof vnode === 'string') return vnode
|
|
210
193
|
if (typeof vnode === 'number') return String(vnode)
|
|
211
194
|
if (!vnode) return ''
|
|
212
|
-
|
|
213
|
-
if (Array.isArray(vnode)) {
|
|
214
|
-
return vnode.map(extractTextFromVNode).join('')
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// テキストノードの処理
|
|
195
|
+
if (Array.isArray(vnode)) return vnode.map(extractTextFromVNode).join('')
|
|
218
196
|
if (vnode.type === 'text' || vnode.type === Text || typeof vnode.children === 'string') {
|
|
219
197
|
return vnode.children || vnode.text || ''
|
|
220
198
|
}
|
|
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
|
-
// コンポーネントの場合はプレースホルダーを返す
|
|
199
|
+
if (vnode.type === 'br') return '\n'
|
|
200
|
+
if (Array.isArray(vnode.children)) return vnode.children.map(extractTextFromVNode).join('')
|
|
233
201
|
if (typeof vnode.type === 'object' || typeof vnode.type === 'function') {
|
|
234
202
|
return `<COMPONENT:${vnode.type.name || 'Unknown'}>`
|
|
235
203
|
}
|
|
236
|
-
|
|
237
204
|
return vnode.children || ''
|
|
238
205
|
}
|
|
239
206
|
|
|
240
|
-
|
|
241
|
-
return result
|
|
207
|
+
return slots.default().map(extractTextFromVNode).join('')
|
|
242
208
|
})
|
|
243
209
|
|
|
244
|
-
// デフォルトの区切り文字パターン
|
|
245
210
|
const getDelimiters = () => {
|
|
246
211
|
if (props.simple) {
|
|
247
|
-
// シンプルモード:$...$のみ
|
|
248
212
|
return [
|
|
249
213
|
{
|
|
250
214
|
pattern: /\$([^$\n]+)\$/g,
|
|
@@ -255,35 +219,30 @@ const getDelimiters = () => {
|
|
|
255
219
|
}
|
|
256
220
|
|
|
257
221
|
return props.customDelimiters || [
|
|
258
|
-
// LaTeX環境(最優先)
|
|
259
222
|
{
|
|
260
223
|
pattern: /\\begin\{(align\*?|equation\*?|gather\*?|multline\*?|split|eqnarray\*?|alignat\*?|flalign\*?)\}([\s\S]*?)\\end\{\1\}/g,
|
|
261
224
|
type: 'block-math',
|
|
262
225
|
process: (match) => match[0],
|
|
263
226
|
priority: 0
|
|
264
227
|
},
|
|
265
|
-
// $$...$$(ブロック数式)
|
|
266
228
|
{
|
|
267
229
|
pattern: /\$\$([^$]*(?:\$(?!\$)[^$]*)*)\$\$/g,
|
|
268
230
|
type: 'block-math',
|
|
269
231
|
process: (match) => match[1].trim(),
|
|
270
232
|
priority: 1
|
|
271
233
|
},
|
|
272
|
-
// $...$(インライン数式)
|
|
273
234
|
{
|
|
274
235
|
pattern: /\$([^$\n]+)\$/g,
|
|
275
236
|
type: 'inline-math',
|
|
276
237
|
process: (match) => match[1].trim(),
|
|
277
238
|
priority: 2
|
|
278
239
|
},
|
|
279
|
-
// \(...\)(インライン数式)
|
|
280
240
|
{
|
|
281
241
|
pattern: /\\\(([^)]+)\\\)/g,
|
|
282
242
|
type: 'inline-math',
|
|
283
243
|
process: (match) => match[1].trim(),
|
|
284
244
|
priority: 3
|
|
285
245
|
},
|
|
286
|
-
// \[...\](ブロック数式)
|
|
287
246
|
{
|
|
288
247
|
pattern: /\\\[([^\]]+)\\\]/g,
|
|
289
248
|
type: 'block-math',
|
|
@@ -293,7 +252,6 @@ const getDelimiters = () => {
|
|
|
293
252
|
]
|
|
294
253
|
}
|
|
295
254
|
|
|
296
|
-
// テキストを解析してセグメントに分割
|
|
297
255
|
const parseTextToSegments = (inputText) => {
|
|
298
256
|
if (!inputText) return []
|
|
299
257
|
|
|
@@ -302,7 +260,6 @@ const parseTextToSegments = (inputText) => {
|
|
|
302
260
|
let currentText = inputText
|
|
303
261
|
const mathBlocks = []
|
|
304
262
|
|
|
305
|
-
// 全ての数式パターンを検出
|
|
306
263
|
delimiters.forEach((delimiter, delimiterIndex) => {
|
|
307
264
|
let match
|
|
308
265
|
const regex = new RegExp(delimiter.pattern.source, delimiter.pattern.flags)
|
|
@@ -319,13 +276,11 @@ const parseTextToSegments = (inputText) => {
|
|
|
319
276
|
}
|
|
320
277
|
})
|
|
321
278
|
|
|
322
|
-
// 開始位置でソート、重複する場合は優先度で決定
|
|
323
279
|
mathBlocks.sort((a, b) => {
|
|
324
280
|
if (a.start !== b.start) return a.start - b.start
|
|
325
281
|
return a.priority - b.priority
|
|
326
282
|
})
|
|
327
283
|
|
|
328
|
-
// 重複する範囲を除去
|
|
329
284
|
const filteredBlocks = []
|
|
330
285
|
for (let i = 0; i < mathBlocks.length; i++) {
|
|
331
286
|
const current = mathBlocks[i]
|
|
@@ -333,7 +288,6 @@ const parseTextToSegments = (inputText) => {
|
|
|
333
288
|
|
|
334
289
|
for (let j = filteredBlocks.length - 1; j >= 0; j--) {
|
|
335
290
|
const existing = filteredBlocks[j]
|
|
336
|
-
|
|
337
291
|
if (!(current.end <= existing.start || current.start >= existing.end)) {
|
|
338
292
|
if (current.priority < existing.priority ||
|
|
339
293
|
(current.priority === existing.priority && (current.end - current.start) > (existing.end - existing.start))) {
|
|
@@ -345,67 +299,43 @@ const parseTextToSegments = (inputText) => {
|
|
|
345
299
|
}
|
|
346
300
|
}
|
|
347
301
|
|
|
348
|
-
if (shouldAdd)
|
|
349
|
-
filteredBlocks.push(current)
|
|
350
|
-
}
|
|
302
|
+
if (shouldAdd) filteredBlocks.push(current)
|
|
351
303
|
}
|
|
352
304
|
|
|
353
305
|
filteredBlocks.sort((a, b) => a.start - b.start)
|
|
354
306
|
|
|
355
|
-
// セグメントを構築
|
|
356
307
|
let lastIndex = 0
|
|
357
308
|
|
|
358
309
|
filteredBlocks.forEach(block => {
|
|
359
|
-
// 数式の前のテキスト部分
|
|
360
310
|
if (block.start > lastIndex) {
|
|
361
311
|
const textContent = currentText.slice(lastIndex, block.start)
|
|
362
312
|
if (textContent) {
|
|
363
|
-
// 改行で分割してセグメントを作成
|
|
364
313
|
const lines = textContent.split('\n')
|
|
365
314
|
lines.forEach((line, lineIndex) => {
|
|
366
|
-
if (line || lineIndex === 0) {
|
|
367
|
-
segments.push({
|
|
368
|
-
type: 'text',
|
|
369
|
-
content: line
|
|
370
|
-
})
|
|
315
|
+
if (line || lineIndex === 0) {
|
|
316
|
+
segments.push({ type: 'text', content: line })
|
|
371
317
|
}
|
|
372
|
-
// 改行を追加(最後の行以外)
|
|
373
318
|
if (lineIndex < lines.length - 1) {
|
|
374
|
-
segments.push({
|
|
375
|
-
type: 'text',
|
|
376
|
-
content: '\n'
|
|
377
|
-
})
|
|
319
|
+
segments.push({ type: 'text', content: '\n' })
|
|
378
320
|
}
|
|
379
321
|
})
|
|
380
322
|
}
|
|
381
323
|
}
|
|
382
324
|
|
|
383
|
-
|
|
384
|
-
segments.push({
|
|
385
|
-
type: block.type,
|
|
386
|
-
content: block.content
|
|
387
|
-
})
|
|
388
|
-
|
|
325
|
+
segments.push({ type: block.type, content: block.content })
|
|
389
326
|
lastIndex = block.end
|
|
390
327
|
})
|
|
391
328
|
|
|
392
|
-
// 残りのテキスト
|
|
393
329
|
if (lastIndex < currentText.length) {
|
|
394
330
|
const textContent = currentText.slice(lastIndex)
|
|
395
331
|
if (textContent) {
|
|
396
332
|
const lines = textContent.split('\n')
|
|
397
333
|
lines.forEach((line, lineIndex) => {
|
|
398
334
|
if (line || lineIndex === 0) {
|
|
399
|
-
segments.push({
|
|
400
|
-
type: 'text',
|
|
401
|
-
content: line
|
|
402
|
-
})
|
|
335
|
+
segments.push({ type: 'text', content: line })
|
|
403
336
|
}
|
|
404
337
|
if (lineIndex < lines.length - 1) {
|
|
405
|
-
segments.push({
|
|
406
|
-
type: 'text',
|
|
407
|
-
content: '\n'
|
|
408
|
-
})
|
|
338
|
+
segments.push({ type: 'text', content: '\n' })
|
|
409
339
|
}
|
|
410
340
|
})
|
|
411
341
|
}
|
|
@@ -414,26 +344,20 @@ const parseTextToSegments = (inputText) => {
|
|
|
414
344
|
return segments
|
|
415
345
|
}
|
|
416
346
|
|
|
417
|
-
// textプロパティから処理されたセグメント
|
|
418
347
|
const processedTextSegments = computed(() => {
|
|
419
348
|
if (!props.text) return []
|
|
420
349
|
return parseTextToSegments(props.text)
|
|
421
350
|
})
|
|
422
351
|
|
|
423
|
-
// スロットから処理されたセグメント
|
|
424
352
|
const processedSlotSegments = computed(() => {
|
|
425
353
|
if (!hasSlotContent.value) return []
|
|
426
354
|
return parseTextToSegments(slotTextContent.value)
|
|
427
355
|
})
|
|
428
356
|
|
|
429
|
-
// 数式要素の参照を設定
|
|
430
357
|
const setMathElement = (key, el) => {
|
|
431
|
-
if (el)
|
|
432
|
-
mathElements.value[key] = el
|
|
433
|
-
}
|
|
358
|
+
if (el) mathElements.value[key] = el
|
|
434
359
|
}
|
|
435
360
|
|
|
436
|
-
// 数式レンダリング
|
|
437
361
|
const renderMathElements = async () => {
|
|
438
362
|
await nextTick()
|
|
439
363
|
|
|
@@ -454,7 +378,6 @@ const renderMathElements = async () => {
|
|
|
454
378
|
katex = katexModule.default || katexModule
|
|
455
379
|
window.katex = katex
|
|
456
380
|
} catch (e) {
|
|
457
|
-
// KaTeXが利用できない場合のフォールバック
|
|
458
381
|
if (props.simple) {
|
|
459
382
|
element.innerHTML = `<span style="font-style: italic; color: #0066cc; background: #f0f8ff; padding: 1px 3px; border-radius: 3px;">${formula}</span>`
|
|
460
383
|
} else {
|
|
@@ -490,20 +413,25 @@ const renderMathElements = async () => {
|
|
|
490
413
|
}
|
|
491
414
|
|
|
492
415
|
onMounted(() => {
|
|
416
|
+
if (props.eq && equationRegistry) {
|
|
417
|
+
equationRegistry.register(props.eq)
|
|
418
|
+
}
|
|
493
419
|
renderMathElements()
|
|
494
420
|
})
|
|
495
421
|
|
|
422
|
+
onUnmounted(() => {
|
|
423
|
+
if (props.eq && equationRegistry) {
|
|
424
|
+
equationRegistry.unregister(props.eq)
|
|
425
|
+
}
|
|
426
|
+
})
|
|
427
|
+
|
|
496
428
|
watch([() => props.text, hasSlotContent], () => {
|
|
497
429
|
mathElements.value = {}
|
|
498
|
-
nextTick(() =>
|
|
499
|
-
renderMathElements()
|
|
500
|
-
})
|
|
430
|
+
nextTick(() => renderMathElements())
|
|
501
431
|
}, { flush: 'post' })
|
|
502
432
|
|
|
503
433
|
watch([processedTextSegments, processedSlotSegments], () => {
|
|
504
|
-
nextTick(() =>
|
|
505
|
-
renderMathElements()
|
|
506
|
-
})
|
|
434
|
+
nextTick(() => renderMathElements())
|
|
507
435
|
}, { flush: 'post' })
|
|
508
436
|
</script>
|
|
509
437
|
|
|
@@ -531,7 +459,6 @@ watch([processedTextSegments, processedSlotSegments], () => {
|
|
|
531
459
|
text-align: center;
|
|
532
460
|
}
|
|
533
461
|
|
|
534
|
-
/* KaTeX用の基本スタイリング */
|
|
535
462
|
.inline-math-formula :deep(.katex),
|
|
536
463
|
.block-math-formula :deep(.katex) {
|
|
537
464
|
font-size: inherit !important;
|
|
@@ -546,7 +473,6 @@ watch([processedTextSegments, processedSlotSegments], () => {
|
|
|
546
473
|
text-align: center;
|
|
547
474
|
}
|
|
548
475
|
|
|
549
|
-
/* シンプルモード用のフォールバックスタイル */
|
|
550
476
|
.math-text-container.simple-mode .inline-math-formula {
|
|
551
477
|
font-style: italic;
|
|
552
478
|
color: #0066cc;
|
|
@@ -554,4 +480,13 @@ watch([processedTextSegments, processedSlotSegments], () => {
|
|
|
554
480
|
padding: 1px 3px;
|
|
555
481
|
border-radius: 3px;
|
|
556
482
|
}
|
|
483
|
+
|
|
484
|
+
.eq-number {
|
|
485
|
+
position: absolute;
|
|
486
|
+
right: 0;
|
|
487
|
+
top: 50%;
|
|
488
|
+
transform: translateY(-50%);
|
|
489
|
+
color: #374151;
|
|
490
|
+
font-size: 0.9em;
|
|
491
|
+
}
|
|
557
492
|
</style>
|