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.
- package/LICENSE +21 -0
- package/README.md +298 -0
- package/components/Citation.vue +253 -0
- package/components/CitationListPage.vue +249 -0
- package/components/Header.vue +354 -0
- package/components/MathText.vue +557 -0
- package/components/SectionDivider.vue +24 -0
- package/components/SectionTitle.vue +70 -0
- package/components/SubSectionTitle.vue +67 -0
- package/components/TableOfContents.vue +349 -0
- package/components/TextColorBox.vue +97 -0
- package/layouts/cover.vue +69 -0
- package/layouts/default.vue +12 -0
- package/layouts/intro.vue +7 -0
- package/package.json +65 -0
- package/setup/shiki.ts +11 -0
- package/styles/index.ts +3 -0
- package/styles/layout.css +24 -0
- package/uno.config.ts +13 -0
|
@@ -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, '&')
|
|
138
|
+
.replace(/</g, '<')
|
|
139
|
+
.replace(/>/g, '>')
|
|
140
|
+
.replace(/"/g, '"')
|
|
141
|
+
.replace(/'/g, ''')
|
|
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>
|