@teriusu/rich-editor 0.9.1 → 0.9.2
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/dist/components/BubbleToolbar.svelte +23 -0
- package/dist/components/BubbleToolbar.svelte.d.ts.map +1 -1
- package/dist/components/FixedToolbar.svelte +44 -1
- package/dist/components/FixedToolbar.svelte.d.ts.map +1 -1
- package/dist/components/MathModal.svelte +165 -0
- package/dist/components/MathModal.svelte.d.ts +11 -0
- package/dist/components/MathModal.svelte.d.ts.map +1 -0
- package/dist/components/SlashCommandMenu.svelte +11 -0
- package/dist/components/SlashCommandMenu.svelte.d.ts.map +1 -1
- package/dist/components/TipTapEditor.svelte +51 -3
- package/dist/components/TipTapEditor.svelte.d.ts +3 -0
- package/dist/components/TipTapEditor.svelte.d.ts.map +1 -1
- package/dist/extensions/Math.d.ts +32 -2
- package/dist/extensions/Math.d.ts.map +1 -1
- package/dist/extensions/Math.js +455 -10
- package/dist/extensions/Math.test.d.ts +2 -0
- package/dist/extensions/Math.test.d.ts.map +1 -0
- package/dist/extensions/Math.test.js +263 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/styles/editor.css +1 -1
- package/dist/types.d.ts +8 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -3
- package/package.json +1 -1
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
ListChecks,
|
|
23
23
|
Quote,
|
|
24
24
|
ChevronDown,
|
|
25
|
+
Sigma,
|
|
25
26
|
} from "lucide-svelte";
|
|
26
27
|
import { cn } from "../utils/cn";
|
|
27
28
|
import type { ToolbarFeature, PromptHandler } from "../types";
|
|
@@ -380,6 +381,28 @@
|
|
|
380
381
|
</button>
|
|
381
382
|
{/if}
|
|
382
383
|
|
|
384
|
+
{#if has('math')}
|
|
385
|
+
<!--
|
|
386
|
+
버블 툴바는 **뭔가 선택돼 있을 때만** 뜨므로 여기서는 감싸기/풀기만 하면 된다
|
|
387
|
+
(고정 툴바처럼 "빈 선택이면 새로 넣기" 로 떨어질 일이 없다).
|
|
388
|
+
`minimal` 프리셋에는 고정 툴바가 없어서, 댓글에서 수식을 만드는 유일한 버튼이 이것이다.
|
|
389
|
+
-->
|
|
390
|
+
<button
|
|
391
|
+
type="button"
|
|
392
|
+
onclick={() => editor.chain().focus().toggleMathInline().run()}
|
|
393
|
+
title="인라인 수식"
|
|
394
|
+
aria-label="인라인 수식"
|
|
395
|
+
class={cn(
|
|
396
|
+
"p-1.5 rounded-full transition-colors",
|
|
397
|
+
isActive("math_inline")
|
|
398
|
+
? "bg-white/20 text-white"
|
|
399
|
+
: "text-white/70 hover:text-white hover:bg-white/10",
|
|
400
|
+
)}
|
|
401
|
+
>
|
|
402
|
+
<Sigma size={iconSize} />
|
|
403
|
+
</button>
|
|
404
|
+
{/if}
|
|
405
|
+
|
|
383
406
|
{#if has('highlight') || has('text-color')}
|
|
384
407
|
<div class="w-px h-5 bg-white/20 mx-0.5"></div>
|
|
385
408
|
{/if}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BubbleToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/BubbleToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"BubbleToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/BubbleToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAwB3C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE7D,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B,CAAC;AAgYJ,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
Plus,
|
|
35
35
|
Pilcrow,
|
|
36
36
|
Palette,
|
|
37
|
+
Sigma,
|
|
37
38
|
} from "lucide-svelte";
|
|
38
39
|
import { cn } from "../utils/cn";
|
|
39
40
|
import InputModal from "./InputModal.svelte";
|
|
@@ -249,7 +250,8 @@
|
|
|
249
250
|
has("columns-2") ||
|
|
250
251
|
has("columns-3") ||
|
|
251
252
|
has("horizontal-rule") ||
|
|
252
|
-
has("code-block")
|
|
253
|
+
has("code-block") ||
|
|
254
|
+
has("math"),
|
|
253
255
|
);
|
|
254
256
|
|
|
255
257
|
const hasBlockItems = $derived(
|
|
@@ -533,6 +535,37 @@
|
|
|
533
535
|
<Strikethrough size={iconSize} />
|
|
534
536
|
</button>
|
|
535
537
|
{/if}
|
|
538
|
+
{#if has('math')}
|
|
539
|
+
<!--
|
|
540
|
+
선택한 텍스트를 인라인 수식으로 감싸고, 이미 수식인 자리에서 누르면 원문으로 되돌린다
|
|
541
|
+
(구세대 `setMath.js` 와 같은 토글). 선택이 없으면 감쌀 게 없으니 프롬프트로 새로 넣는다.
|
|
542
|
+
|
|
543
|
+
⚠️ **`run()` 의 반환값으로 갈라지면 안 된다.** `chain().focus().toggleMathInline().run()`
|
|
544
|
+
은 감싸기가 실제로 성공해도 false 를 돌려주는 경우가 있어(체인의 다른 단계가 false 를
|
|
545
|
+
섞는다), 감싸 놓고 프롬프트까지 같이 여는 걸 브라우저에서 확인했다.
|
|
546
|
+
**무엇을 할지는 실행 전에 상태로 정한다.**
|
|
547
|
+
-->
|
|
548
|
+
<button
|
|
549
|
+
type="button"
|
|
550
|
+
onclick={() => {
|
|
551
|
+
if (editor.state.selection.empty && !editor.isActive('math_inline')) {
|
|
552
|
+
editor.chain().focus().promptMathInline().run();
|
|
553
|
+
} else {
|
|
554
|
+
editor.chain().focus().toggleMathInline().run();
|
|
555
|
+
}
|
|
556
|
+
}}
|
|
557
|
+
data-tooltip="인라인 수식"
|
|
558
|
+
aria-label="인라인 수식"
|
|
559
|
+
class={cn(
|
|
560
|
+
"p-1.5 rounded-md transition-colors",
|
|
561
|
+
isActive("math_inline")
|
|
562
|
+
? "hce-active"
|
|
563
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
564
|
+
)}
|
|
565
|
+
>
|
|
566
|
+
<Sigma size={iconSize} />
|
|
567
|
+
</button>
|
|
568
|
+
{/if}
|
|
536
569
|
{#if has('text-color')}
|
|
537
570
|
<div bind:this={colorMenuEl} class="relative">
|
|
538
571
|
<button
|
|
@@ -687,6 +720,16 @@
|
|
|
687
720
|
<Code2 size={14} /> 코드 블록
|
|
688
721
|
</button>
|
|
689
722
|
{/if}
|
|
723
|
+
{#if has('math')}
|
|
724
|
+
<button
|
|
725
|
+
type="button"
|
|
726
|
+
class="w-full text-left px-2.5 py-1.5 text-xs transition-colors flex items-center gap-2 hover:bg-muted"
|
|
727
|
+
onclick={() =>
|
|
728
|
+
runInsert(() => editor.chain().focus().promptMathDisplay().run())}
|
|
729
|
+
>
|
|
730
|
+
<Sigma size={14} /> 수식 블록
|
|
731
|
+
</button>
|
|
732
|
+
{/if}
|
|
690
733
|
{#if has('pdf')}
|
|
691
734
|
<button
|
|
692
735
|
type="button"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FixedToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/FixedToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"FixedToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/FixedToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAsCtC,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE7D,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAqnBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* LaTeX 수식 편집 모달.
|
|
4
|
+
*
|
|
5
|
+
* `InputModal` 을 재사용하지 않고 따로 둔 이유는 둘이다:
|
|
6
|
+
* 1. 수식은 한 줄로 안 끝난다(행렬·정렬 환경) → `textarea`
|
|
7
|
+
* 2. LaTeX 는 눈으로 확인하지 않으면 맞게 썼는지 알 수 없다 → **실시간 미리보기**
|
|
8
|
+
*
|
|
9
|
+
* 렌더는 편집기 NodeView 와 같은 KaTeX 옵션(`throwOnError:false`)을 쓴다. 그래서
|
|
10
|
+
* 여기서 보이는 모양이 곧 본문에 들어갈 모양이다.
|
|
11
|
+
*/
|
|
12
|
+
import { X } from "lucide-svelte";
|
|
13
|
+
import katex from "katex";
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
latex = "",
|
|
17
|
+
displayMode = false,
|
|
18
|
+
onConfirm,
|
|
19
|
+
onCancel,
|
|
20
|
+
}: {
|
|
21
|
+
latex?: string;
|
|
22
|
+
displayMode?: boolean;
|
|
23
|
+
/** 빈 문자열로 확인하면 호출부가 노드를 지운다(편집 중일 때). */
|
|
24
|
+
onConfirm: (value: string) => void;
|
|
25
|
+
onCancel: () => void;
|
|
26
|
+
} = $props();
|
|
27
|
+
|
|
28
|
+
let value = $state(latex);
|
|
29
|
+
let inputEl: HTMLTextAreaElement | undefined = $state();
|
|
30
|
+
|
|
31
|
+
const preview = $derived.by(() => {
|
|
32
|
+
const source = value.trim();
|
|
33
|
+
if (!source) return { html: "", error: "" };
|
|
34
|
+
try {
|
|
35
|
+
return {
|
|
36
|
+
html: katex.renderToString(source, {
|
|
37
|
+
displayMode,
|
|
38
|
+
throwOnError: true,
|
|
39
|
+
strict: false,
|
|
40
|
+
output: "html",
|
|
41
|
+
}),
|
|
42
|
+
error: "",
|
|
43
|
+
};
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return { html: "", error: err instanceof Error ? err.message : String(err) };
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
$effect(() => {
|
|
50
|
+
if (inputEl) {
|
|
51
|
+
inputEl.focus();
|
|
52
|
+
inputEl.select();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
$effect(() => {
|
|
57
|
+
function onKeydown(e: KeyboardEvent) {
|
|
58
|
+
if (e.key === "Escape") {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
onCancel();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
window.addEventListener("keydown", onKeydown);
|
|
64
|
+
return () => window.removeEventListener("keydown", onKeydown);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function handleSubmit() {
|
|
68
|
+
onConfirm(value.trim());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
72
|
+
// 줄바꿈이 필요한 문법(행렬 등)이 있으므로 Enter 는 그대로 두고,
|
|
73
|
+
// 확인은 Ctrl/⌘+Enter 로 받는다.
|
|
74
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
handleSubmit();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
82
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
83
|
+
<!--
|
|
84
|
+
⚠️ z-index 를 Tailwind `z-50` 으로 두면 **호스트 앱 밑에 깔린다.** 정올은 헤더가 98,
|
|
85
|
+
레이아웃 오버레이가 1000, 다이얼로그 스크림이 99999999 다(`apps/jungol/src/routes/layout.css`).
|
|
86
|
+
이 모달은 본문 클릭으로도 열리므로 다이얼로그 안의 에디터에서 열릴 수 있다 — 스크림보다 위여야 한다.
|
|
87
|
+
Tailwind 임의값 클래스 대신 인라인 style 로 준다(`.claude/rules/rich-editor.md`: 동적 클래스 금지).
|
|
88
|
+
-->
|
|
89
|
+
<div
|
|
90
|
+
class="fixed inset-0 flex items-center justify-center bg-black/40"
|
|
91
|
+
style="z-index: 100000001"
|
|
92
|
+
onclick={onCancel}
|
|
93
|
+
>
|
|
94
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
95
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
96
|
+
<div
|
|
97
|
+
class="bg-background border border-border rounded-xl shadow-xl p-5 w-[440px] max-w-[90vw]"
|
|
98
|
+
onclick={(e) => e.stopPropagation()}
|
|
99
|
+
>
|
|
100
|
+
<div class="flex items-center justify-between mb-3">
|
|
101
|
+
<span class="text-sm font-semibold">
|
|
102
|
+
{displayMode ? "수식 블록" : "인라인 수식"}
|
|
103
|
+
</span>
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
class="p-1 rounded-md hover:bg-muted text-muted-foreground"
|
|
107
|
+
onclick={onCancel}
|
|
108
|
+
aria-label="닫기"
|
|
109
|
+
>
|
|
110
|
+
<X size={14} />
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<textarea
|
|
115
|
+
bind:this={inputEl}
|
|
116
|
+
rows="3"
|
|
117
|
+
spellcheck="false"
|
|
118
|
+
class="w-full border border-border rounded-md px-3 py-2 text-sm font-mono bg-background resize-y focus:outline-none focus:ring-2 focus:ring-primary/40 focus:border-primary"
|
|
119
|
+
placeholder={"\\frac{a}{b} \\le c"}
|
|
120
|
+
bind:value
|
|
121
|
+
onkeydown={handleKeydown}
|
|
122
|
+
></textarea>
|
|
123
|
+
|
|
124
|
+
<!--
|
|
125
|
+
⚠️ 바탕을 `bg-muted/40` 으로 주지 않는다. 호스트가 `--muted` 를 **hex** 로 주는데
|
|
126
|
+
Tailwind 3 의 투명도 변형은 채널 표기를 요구해 그 클래스를 **조용히 만들지 않는다**
|
|
127
|
+
(빌드된 CSS 에 규칙이 아예 없다 — 실제로 확인했다). `.claude/rules/rich-editor.md` 대로
|
|
128
|
+
`color-mix` 를 인라인으로 준다. `editor.css` 의 `.hce-active` 가 같은 이유로 같은 처방을 쓴다.
|
|
129
|
+
-->
|
|
130
|
+
<div
|
|
131
|
+
class="mt-3 min-h-[52px] flex items-center justify-center rounded-md border border-border px-3 py-2 overflow-x-auto"
|
|
132
|
+
style="background-color: color-mix(in srgb, var(--muted) 40%, transparent)"
|
|
133
|
+
>
|
|
134
|
+
{#if preview.error}
|
|
135
|
+
<span class="text-xs text-destructive break-all">{preview.error}</span>
|
|
136
|
+
{:else if preview.html}
|
|
137
|
+
<!-- katex.renderToString 산출물. 사용자 입력은 KaTeX 가 이스케이프한다. -->
|
|
138
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
139
|
+
{@html preview.html}
|
|
140
|
+
{:else}
|
|
141
|
+
<span class="text-xs text-muted-foreground">미리보기</span>
|
|
142
|
+
{/if}
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div class="flex items-center justify-between gap-2 mt-4">
|
|
146
|
+
<span class="text-[11px] text-muted-foreground">⌘/Ctrl + Enter 로 확인</span>
|
|
147
|
+
<div class="flex gap-2">
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
class="px-3.5 py-1.5 text-sm rounded-md border border-border hover:bg-muted transition-colors"
|
|
151
|
+
onclick={onCancel}
|
|
152
|
+
>
|
|
153
|
+
취소
|
|
154
|
+
</button>
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
class="px-3.5 py-1.5 text-sm rounded-md bg-primary text-primary-foreground hover:opacity-90 transition-opacity"
|
|
158
|
+
onclick={handleSubmit}
|
|
159
|
+
>
|
|
160
|
+
확인
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
latex?: string;
|
|
3
|
+
displayMode?: boolean;
|
|
4
|
+
/** 빈 문자열로 확인하면 호출부가 노드를 지운다(편집 중일 때). */
|
|
5
|
+
onConfirm: (value: string) => void;
|
|
6
|
+
onCancel: () => void;
|
|
7
|
+
};
|
|
8
|
+
declare const MathModal: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type MathModal = ReturnType<typeof MathModal>;
|
|
10
|
+
export default MathModal;
|
|
11
|
+
//# sourceMappingURL=MathModal.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MathModal.svelte.d.ts","sourceRoot":"","sources":["../../src/components/MathModal.svelte.ts"],"names":[],"mappings":"AAgBC,KAAK,gBAAgB,GAAI;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yCAAyC;IACzC,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB,CAAC;AAiHJ,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
Type,
|
|
22
22
|
Tv,
|
|
23
23
|
SquareDashed,
|
|
24
|
+
Sigma,
|
|
24
25
|
} from "lucide-svelte";
|
|
25
26
|
import type { SlashMenuItem, ToolbarFeature, PromptHandler } from "../types";
|
|
26
27
|
import type { Component } from "svelte";
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
/** 각 feature가 속하는 섹션 라벨 */
|
|
31
32
|
const SECTION_MAP: Record<string, string> = {
|
|
32
33
|
"code-block": "자주 쓰는",
|
|
34
|
+
math: "자주 쓰는",
|
|
33
35
|
file: "미디어",
|
|
34
36
|
pdf: "미디어",
|
|
35
37
|
paragraph: "기본",
|
|
@@ -133,6 +135,15 @@
|
|
|
133
135
|
command: (editor) =>
|
|
134
136
|
editor.chain().focus().setCodeBlock().run(),
|
|
135
137
|
},
|
|
138
|
+
{
|
|
139
|
+
feature: "math",
|
|
140
|
+
label: "수식",
|
|
141
|
+
keywords: "math latex tex 수식 공식 수학 katex 시그마 분수",
|
|
142
|
+
icon: Sigma,
|
|
143
|
+
// 슬래시 메뉴는 `/수식` 을 지우면서 커맨드를 부른다. 프롬프트는 그 뒤에 열려야
|
|
144
|
+
// 지운 자리에 결과가 들어간다 — `promptMathDisplay` 가 비동기라 순서가 맞는다.
|
|
145
|
+
command: (editor) => editor.chain().focus().promptMathDisplay().run(),
|
|
146
|
+
},
|
|
136
147
|
{
|
|
137
148
|
feature: "toggle",
|
|
138
149
|
label: "토글",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SlashCommandMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/components/SlashCommandMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"SlashCommandMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/components/SlashCommandMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAwB3C,OAAO,KAAK,EAAiB,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEvC,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B,CAAC;AAqXJ,QAAA,MAAM,gBAAgB,qCAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|
|
@@ -38,11 +38,12 @@
|
|
|
38
38
|
import { FileAttachment } from "../extensions/FileAttachment";
|
|
39
39
|
import { MbusVideo } from "../extensions/MbusVideo";
|
|
40
40
|
import { CardBlock } from "../extensions/CardBlock";
|
|
41
|
-
import { MathInline, MathDisplay } from "../extensions/Math";
|
|
41
|
+
import { MathInline, MathDisplay, type MathPrompt } from "../extensions/Math";
|
|
42
42
|
import FixedToolbar from "./FixedToolbar.svelte";
|
|
43
43
|
import BubbleToolbar from "./BubbleToolbar.svelte";
|
|
44
44
|
import SlashCommandMenu from "./SlashCommandMenu.svelte";
|
|
45
45
|
import TableBubbleMenu from "./TableBubbleMenu.svelte";
|
|
46
|
+
import MathModal from "./MathModal.svelte";
|
|
46
47
|
import type { UploadHandler, PromptHandler, ToolbarMode, ToolbarFeature } from "../types";
|
|
47
48
|
import { resolveFeatures } from "../types";
|
|
48
49
|
import type { FileResolver } from "../extensions/FileAttachment";
|
|
@@ -120,6 +121,7 @@
|
|
|
120
121
|
onPromptImage,
|
|
121
122
|
onPromptMbus,
|
|
122
123
|
onPromptCardBackground,
|
|
124
|
+
onPromptMath,
|
|
123
125
|
toolbarEnd,
|
|
124
126
|
extensions: extraExtensions = [],
|
|
125
127
|
editable = true,
|
|
@@ -137,6 +139,8 @@
|
|
|
137
139
|
onPromptMbus?: PromptHandler;
|
|
138
140
|
/** 카드 배경 고르기. 미제공 시 window.prompt 폴백 */
|
|
139
141
|
onPromptCardBackground?: PromptHandler;
|
|
142
|
+
/** LaTeX 수식 편집. 미제공 시 내장 MathModal(실시간 미리보기) 폴백 */
|
|
143
|
+
onPromptMath?: MathPrompt;
|
|
140
144
|
/** 고정 툴바 오른쪽 끝에 끼워 넣을 조각 */
|
|
141
145
|
toolbarEnd?: Snippet;
|
|
142
146
|
extensions?: AnyExtension[];
|
|
@@ -161,6 +165,36 @@
|
|
|
161
165
|
const cc = editor?.storage?.characterCount;
|
|
162
166
|
return { chars: cc?.characters?.() ?? 0, words: cc?.words?.() ?? 0 };
|
|
163
167
|
});
|
|
168
|
+
/*
|
|
169
|
+
* 수식 프롬프트 다리.
|
|
170
|
+
*
|
|
171
|
+
* 확장(vanilla NodeView·입력 규칙)은 Svelte 를 모르므로 **약속된 함수 하나**만 받는다.
|
|
172
|
+
* 그 함수가 여기서 `MathModal` 을 띄우고 사용자가 확인/취소할 때 resolve 한다.
|
|
173
|
+
* 링크·이미지가 `InputModal` 로 하는 것과 같은 구조인데, 저쪽은 툴바가 열고 이쪽은
|
|
174
|
+
* 본문 클릭·입력 규칙·슬래시가 열기 때문에 상태를 에디터 최상단에 둬야 한다.
|
|
175
|
+
*
|
|
176
|
+
* ⚠️ 이 함수는 확장을 만들 때 한 번만 캡처된다. 그래서 `onPromptMath` 를 직접 넘기지 않고
|
|
177
|
+
* 호출 시점에 읽는다 — 호스트가 나중에 핸들러를 붙여도 따라간다.
|
|
178
|
+
*/
|
|
179
|
+
let mathPrompt = $state<{
|
|
180
|
+
latex: string;
|
|
181
|
+
displayMode: boolean;
|
|
182
|
+
resolve: (value: string | null) => void;
|
|
183
|
+
} | null>(null);
|
|
184
|
+
|
|
185
|
+
const promptMath: MathPrompt = (latex, displayMode) => {
|
|
186
|
+
if (onPromptMath) return onPromptMath(latex, displayMode);
|
|
187
|
+
return new Promise<string | null>((resolve) => {
|
|
188
|
+
mathPrompt = { latex, displayMode, resolve };
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
function closeMathPrompt(value: string | null) {
|
|
193
|
+
const pending = mathPrompt;
|
|
194
|
+
mathPrompt = null;
|
|
195
|
+
pending?.resolve(value);
|
|
196
|
+
}
|
|
197
|
+
|
|
164
198
|
let uploading = $state(false);
|
|
165
199
|
let pdfInputEl: HTMLInputElement | undefined = $state();
|
|
166
200
|
let fileInputEl: HTMLInputElement | undefined = $state();
|
|
@@ -413,10 +447,10 @@
|
|
|
413
447
|
*/
|
|
414
448
|
...(extraExtensions.some((ext) => (ext as any).name === "math_inline")
|
|
415
449
|
? []
|
|
416
|
-
: [MathInline]),
|
|
450
|
+
: [MathInline.configure({ promptMath })]),
|
|
417
451
|
...(extraExtensions.some((ext) => (ext as any).name === "math_display")
|
|
418
452
|
? []
|
|
419
|
-
: [MathDisplay]),
|
|
453
|
+
: [MathDisplay.configure({ promptMath })]),
|
|
420
454
|
Columns,
|
|
421
455
|
Column,
|
|
422
456
|
CodeBlockTopEscape,
|
|
@@ -660,6 +694,20 @@
|
|
|
660
694
|
</div>
|
|
661
695
|
{/if}
|
|
662
696
|
|
|
697
|
+
<!--
|
|
698
|
+
수식 프롬프트. `onPromptMath` 를 준 호스트에겐 열리지 않는다(그쪽이 직접 띄운다).
|
|
699
|
+
`features` 로 막지 않는다 — `$…$` 입력 규칙과 붙여넣기 변환은 feature 와 무관하게
|
|
700
|
+
항상 살아 있어서, 그렇게 만든 수식을 고치려면 이 모달이 필요하다.
|
|
701
|
+
-->
|
|
702
|
+
{#if mathPrompt}
|
|
703
|
+
<MathModal
|
|
704
|
+
latex={mathPrompt.latex}
|
|
705
|
+
displayMode={mathPrompt.displayMode}
|
|
706
|
+
onConfirm={(value) => closeMathPrompt(value)}
|
|
707
|
+
onCancel={() => closeMathPrompt(null)}
|
|
708
|
+
/>
|
|
709
|
+
{/if}
|
|
710
|
+
|
|
663
711
|
{#if features.has('character-count')}
|
|
664
712
|
<div
|
|
665
713
|
class="flex justify-end px-4 py-2 text-xs text-muted-foreground border-t border-border"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from "svelte";
|
|
2
2
|
import { type AnyExtension } from "@tiptap/core";
|
|
3
|
+
import { type MathPrompt } from "../extensions/Math";
|
|
3
4
|
import type { UploadHandler, PromptHandler, ToolbarMode, ToolbarFeature } from "../types";
|
|
4
5
|
import type { FileResolver } from "../extensions/FileAttachment";
|
|
5
6
|
type $$ComponentProps = {
|
|
@@ -14,6 +15,8 @@ type $$ComponentProps = {
|
|
|
14
15
|
onPromptMbus?: PromptHandler;
|
|
15
16
|
/** 카드 배경 고르기. 미제공 시 window.prompt 폴백 */
|
|
16
17
|
onPromptCardBackground?: PromptHandler;
|
|
18
|
+
/** LaTeX 수식 편집. 미제공 시 내장 MathModal(실시간 미리보기) 폴백 */
|
|
19
|
+
onPromptMath?: MathPrompt;
|
|
17
20
|
/** 고정 툴바 오른쪽 끝에 끼워 넣을 조각 */
|
|
18
21
|
toolbarEnd?: Snippet;
|
|
19
22
|
extensions?: AnyExtension[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AA6BtC,OAAO,EAAa,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"TipTapEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TipTapEditor.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AA6BtC,OAAO,EAAa,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAU5D,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAM9E,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEhE,KAAK,gBAAgB,GAAI;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,wCAAwC;IACxC,sBAAsB,CAAC,EAAE,aAAa,CAAC;IACvC,mDAAmD;IACnD,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B,4BAA4B;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;CAC7B,CAAC;AAmqBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -7,22 +7,52 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 노드는 atom이며 텍스트 콘텐츠(LaTeX 원문)를 자식 텍스트 노드로 보관한다.
|
|
9
9
|
* NodeView가 KaTeX로 렌더링한다.
|
|
10
|
+
*
|
|
11
|
+
* ## 입력 경로 — 구세대(@seorii/tiptap)와 맞춘 것
|
|
12
|
+
*
|
|
13
|
+
* 이전할 때 **렌더만 옮기고 넣는 길이 통째로 빠져 있었다.** 문서는 열리는데 새 수식을
|
|
14
|
+
* 만들 방법이 없었다는 뜻이다. 구세대가 갖고 있던 네 경로를 여기로 되살렸다:
|
|
15
|
+
*
|
|
16
|
+
* | 경로 | 구세대 | 여기 |
|
|
17
|
+
* |---|---|---|
|
|
18
|
+
* | `$…$` | `makeInlineMathInputRule(/\$(.+)\$/)` | `addInputRules` (MathInline) |
|
|
19
|
+
* | `$$…$$` / `$$`+스페이스 | `makeBlockMathInputRule(/\$\$\s+$/)` | `addInputRules` (MathDisplay) |
|
|
20
|
+
* | 붙여넣기 `$…$` 자동 변환 | `plugin/mathPaste.js` | `mathPastePlugin` (아래) |
|
|
21
|
+
* | 선택 영역 감싸기/풀기 | `tiptap/setMath.js` | `toggleMathInline` 커맨드 |
|
|
22
|
+
*
|
|
23
|
+
* ## 편집은 모달로 — 구세대와 다른 유일한 지점
|
|
24
|
+
*
|
|
25
|
+
* 구세대는 노드 안에 **중첩 EditorView**를 띄워 LaTeX 원문을 그 자리에서 고치게 했다
|
|
26
|
+
* (`MathView` + `mathPlugin`/`mathSelectPlugin`, vanilla ProseMirror 400줄 남짓).
|
|
27
|
+
* 여기서는 그 대신 **호스트가 주는 프롬프트 훅**을 부른다 — 링크·이미지·카드 배경이
|
|
28
|
+
* 이미 쓰는 규약(`onPromptLink`, `promptBackground`)과 같은 모양이고, ProseMirror
|
|
29
|
+
* 내부(중첩 뷰·커서 브리지·backspace 체인)에 손대지 않아 유지비가 훨씬 싸다.
|
|
30
|
+
*
|
|
31
|
+
* 호스트가 `promptMath`를 안 주면 `window.prompt`로 떨어진다.
|
|
10
32
|
*/
|
|
11
33
|
import { Node } from "@tiptap/core";
|
|
12
|
-
|
|
34
|
+
/** 현재 LaTeX 원문을 받아 새 값(취소면 null)을 돌려주는 호스트 훅. */
|
|
35
|
+
export type MathPrompt = (latex: string, displayMode: boolean) => Promise<string | null>;
|
|
36
|
+
export interface MathOptions {
|
|
13
37
|
HTMLAttributes: Record<string, unknown>;
|
|
38
|
+
promptMath: MathPrompt | null;
|
|
14
39
|
}
|
|
15
40
|
declare module "@tiptap/core" {
|
|
16
41
|
interface Commands<ReturnType> {
|
|
17
42
|
mathInline: {
|
|
18
43
|
insertMathInline: (latex: string) => ReturnType;
|
|
44
|
+
/** 선택 영역을 인라인 수식으로 감싼다. 이미 수식이면 원문 텍스트로 되돌린다. */
|
|
45
|
+
toggleMathInline: () => ReturnType;
|
|
46
|
+
/** 프롬프트를 띄워 인라인 수식을 새로 넣는다. */
|
|
47
|
+
promptMathInline: () => ReturnType;
|
|
19
48
|
};
|
|
20
49
|
mathDisplay: {
|
|
21
50
|
insertMathDisplay: (latex: string) => ReturnType;
|
|
51
|
+
/** 프롬프트를 띄워 수식 블록을 새로 넣는다. */
|
|
52
|
+
promptMathDisplay: () => ReturnType;
|
|
22
53
|
};
|
|
23
54
|
}
|
|
24
55
|
}
|
|
25
56
|
export declare const MathInline: Node<MathOptions, any>;
|
|
26
57
|
export declare const MathDisplay: Node<MathOptions, any>;
|
|
27
|
-
export {};
|
|
28
58
|
//# sourceMappingURL=Math.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Math.d.ts","sourceRoot":"","sources":["../../src/extensions/Math.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"Math.d.ts","sourceRoot":"","sources":["../../src/extensions/Math.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAAa,IAAI,EAAgC,MAAM,cAAc,CAAC;AAK7E,iDAAiD;AACjD,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAEzF,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;CAC/B;AAED,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,UAAU,EAAE;YACV,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,UAAU,CAAC;YAChD,iDAAiD;YACjD,gBAAgB,EAAE,MAAM,UAAU,CAAC;YACnC,+BAA+B;YAC/B,gBAAgB,EAAE,MAAM,UAAU,CAAC;SACpC,CAAC;QACF,WAAW,EAAE;YACX,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,UAAU,CAAC;YACjD,8BAA8B;YAC9B,iBAAiB,EAAE,MAAM,UAAU,CAAC;SACrC,CAAC;KACH;CACF;AAkZD,eAAO,MAAM,UAAU,wBA4GrB,CAAC;AAEH,eAAO,MAAM,WAAW,wBA8EtB,CAAC"}
|