mdsquare 0.0.0-stage → 0.2.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 +218 -2
- package/dist/callout.d.ts +17 -0
- package/dist/callout.js +541 -0
- package/dist/calloutHeader.d.ts +47 -0
- package/dist/calloutHeader.js +75 -0
- package/dist/codeBlock.d.ts +78 -0
- package/dist/codeBlock.js +686 -0
- package/dist/codeTheme.d.ts +47 -0
- package/dist/codeTheme.js +394 -0
- package/dist/delimiter.d.ts +20 -0
- package/dist/delimiter.js +73 -0
- package/dist/editor.d.ts +132 -0
- package/dist/editor.js +225 -0
- package/dist/emphasis.d.ts +6 -0
- package/dist/emphasis.js +101 -0
- package/dist/escape.d.ts +4 -0
- package/dist/escape.js +39 -0
- package/dist/extensions.d.ts +74 -0
- package/dist/extensions.js +81 -0
- package/dist/footnote.d.ts +24 -0
- package/dist/footnote.js +456 -0
- package/dist/footnoteSyntax.d.ts +23 -0
- package/dist/footnoteSyntax.js +107 -0
- package/dist/hashtag.d.ts +4 -0
- package/dist/hashtag.js +64 -0
- package/dist/hashtagSyntax.d.ts +36 -0
- package/dist/hashtagSyntax.js +99 -0
- package/dist/heading.d.ts +7 -0
- package/dist/heading.js +159 -0
- package/dist/highlight.d.ts +5 -0
- package/dist/highlight.js +6 -0
- package/dist/horizontalRule.d.ts +21 -0
- package/dist/horizontalRule.js +142 -0
- package/dist/image.d.ts +58 -0
- package/dist/image.js +344 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/inlineCode.d.ts +4 -0
- package/dist/inlineCode.js +68 -0
- package/dist/link.d.ts +47 -0
- package/dist/link.js +217 -0
- package/dist/list.d.ts +14 -0
- package/dist/list.js +136 -0
- package/dist/listEnter.d.ts +20 -0
- package/dist/listEnter.js +72 -0
- package/dist/listIndent.d.ts +23 -0
- package/dist/listIndent.js +335 -0
- package/dist/markVision.d.ts +17 -0
- package/dist/markVision.js +56 -0
- package/dist/math.d.ts +45 -0
- package/dist/math.js +233 -0
- package/dist/mathSyntax.d.ts +24 -0
- package/dist/mathSyntax.js +179 -0
- package/dist/mention.d.ts +4 -0
- package/dist/mention.js +58 -0
- package/dist/mentionSyntax.d.ts +14 -0
- package/dist/mentionSyntax.js +71 -0
- package/dist/quote.d.ts +9 -0
- package/dist/quote.js +122 -0
- package/dist/setup.d.ts +13 -0
- package/dist/setup.js +40 -0
- package/dist/style.css +6 -0
- package/dist/style.css.d.ts +2 -0
- package/dist/table.d.ts +38 -0
- package/dist/table.js +960 -0
- package/dist/tableEdit.d.ts +30 -0
- package/dist/tableEdit.js +305 -0
- package/dist/tableMenu.d.ts +65 -0
- package/dist/tableMenu.js +1125 -0
- package/dist/tableModel.d.ts +168 -0
- package/dist/tableModel.js +433 -0
- package/dist/tableState.d.ts +36 -0
- package/dist/tableState.js +96 -0
- package/dist/task.d.ts +13 -0
- package/dist/task.js +179 -0
- package/dist/textMate.d.ts +49 -0
- package/dist/textMate.js +284 -0
- package/dist/tikz.d.ts +56 -0
- package/dist/tikz.js +343 -0
- package/dist/underline.d.ts +5 -0
- package/dist/underline.js +6 -0
- package/dist/utils/blockMargin.d.ts +6 -0
- package/dist/utils/blockMargin.js +6 -0
- package/dist/utils/codePoint.d.ts +13 -0
- package/dist/utils/codePoint.js +43 -0
- package/dist/utils/container.d.ts +10 -0
- package/dist/utils/container.js +10 -0
- package/dist/utils/reveal.d.ts +99 -0
- package/dist/utils/reveal.js +161 -0
- package/dist/utils/selection.d.ts +33 -0
- package/dist/utils/selection.js +45 -0
- package/dist/utils/syntaxTree.d.ts +18 -0
- package/dist/utils/syntaxTree.js +23 -0
- package/package.json +79 -4
package/dist/callout.js
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { codeFolding, ensureSyntaxTree, foldEffect, foldedRanges, foldService, syntaxTree, unfoldEffect, } from "@codemirror/language";
|
|
2
|
+
import { Decoration, EditorView, ViewPlugin, WidgetType } from "@codemirror/view";
|
|
3
|
+
import { calloutsHandled, defaultCalloutTitle, readCalloutHeader, } from "./calloutHeader.js";
|
|
4
|
+
import { quote } from "./quote.js";
|
|
5
|
+
import { createRevealPlugin } from "./utils/reveal.js";
|
|
6
|
+
import { BLOCK_MARGIN_X } from "./utils/blockMargin.js";
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// 아이콘 (Lucide 아이콘과 같은 모양의 선 아이콘, 24x24, stroke=currentColor)
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const ICONS = {
|
|
11
|
+
note: '<path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/>',
|
|
12
|
+
abstract: '<rect x="8" y="2" width="8" height="4" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M12 11h4"/><path d="M12 16h4"/><path d="M8 11h.01"/><path d="M8 16h.01"/>',
|
|
13
|
+
info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
|
|
14
|
+
todo: '<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
|
|
15
|
+
tip: '<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.07-2.14-.22-4.05 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.15.43-2.29 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>',
|
|
16
|
+
success: '<path d="M20 6 9 17l-5-5"/>',
|
|
17
|
+
question: '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>',
|
|
18
|
+
warning: '<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
|
|
19
|
+
failure: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
|
20
|
+
danger: '<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/>',
|
|
21
|
+
bug: '<rect x="8" y="6" width="8" height="14" rx="4"/><path d="m19 7-3 2"/><path d="m5 7 3 2"/><path d="m19 19-3-2"/><path d="m5 19 3-2"/><path d="M20 13h-4"/><path d="M4 13h4"/><path d="m10 4 1 2"/><path d="m14 4-1 2"/>',
|
|
22
|
+
example: '<path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/>',
|
|
23
|
+
quote: '<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.76-2.02-2-2H4c-1.25 0-2 .75-2 1.97V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .01-1 1.03V20c0 1 0 1 1 1z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.76-2.02-2-2h-4c-1.25 0-2 .75-2 1.97V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/>',
|
|
24
|
+
};
|
|
25
|
+
const CHEVRON = '<path d="m6 9 6 6 6-6"/>';
|
|
26
|
+
function svg(paths) {
|
|
27
|
+
const el = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
28
|
+
for (const [k, v] of [
|
|
29
|
+
["viewBox", "0 0 24 24"],
|
|
30
|
+
["fill", "none"],
|
|
31
|
+
["stroke", "currentColor"],
|
|
32
|
+
["stroke-width", "2"],
|
|
33
|
+
["stroke-linecap", "round"],
|
|
34
|
+
["stroke-linejoin", "round"],
|
|
35
|
+
["aria-hidden", "true"],
|
|
36
|
+
]) {
|
|
37
|
+
el.setAttribute(k, v);
|
|
38
|
+
}
|
|
39
|
+
el.innerHTML = paths;
|
|
40
|
+
return el;
|
|
41
|
+
}
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// 위젯
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
/** 머리줄의 `[!type]` 자리에 그리는 아이콘 (+ 제목이 없으면 기본 제목, + 접기 버튼) */
|
|
46
|
+
class CalloutHeaderWidget extends WidgetType {
|
|
47
|
+
type;
|
|
48
|
+
defaultTitle;
|
|
49
|
+
foldable;
|
|
50
|
+
folded;
|
|
51
|
+
constructor(type, defaultTitle, foldable, folded) {
|
|
52
|
+
super();
|
|
53
|
+
this.type = type;
|
|
54
|
+
this.defaultTitle = defaultTitle;
|
|
55
|
+
this.foldable = foldable;
|
|
56
|
+
this.folded = folded;
|
|
57
|
+
}
|
|
58
|
+
eq(other) {
|
|
59
|
+
return (other.type === this.type &&
|
|
60
|
+
other.defaultTitle === this.defaultTitle &&
|
|
61
|
+
other.foldable === this.foldable &&
|
|
62
|
+
other.folded === this.folded);
|
|
63
|
+
}
|
|
64
|
+
toDOM() {
|
|
65
|
+
const wrap = document.createElement("span");
|
|
66
|
+
wrap.className = "cm-callout-header";
|
|
67
|
+
const icon = wrap.appendChild(document.createElement("span"));
|
|
68
|
+
icon.className = "cm-callout-icon";
|
|
69
|
+
icon.appendChild(svg(ICONS[this.type]));
|
|
70
|
+
if (this.defaultTitle !== null) {
|
|
71
|
+
const title = wrap.appendChild(document.createElement("span"));
|
|
72
|
+
title.className = "cm-callout-title-text";
|
|
73
|
+
title.textContent = this.defaultTitle;
|
|
74
|
+
}
|
|
75
|
+
if (this.foldable)
|
|
76
|
+
wrap.appendChild(foldButton(this.folded));
|
|
77
|
+
return wrap;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 접기 상태만 바뀐 경우 DOM을 새로 만들지 않고 버튼 상태만 바꿉니다.
|
|
81
|
+
* (새로 만들면 처음부터 회전된 상태로 그려져 회전 애니메이션이 보이지 않습니다)
|
|
82
|
+
*/
|
|
83
|
+
updateDOM(dom, _view, from) {
|
|
84
|
+
if (from.type !== this.type || from.defaultTitle !== this.defaultTitle || from.foldable !== this.foldable) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const button = dom.querySelector(".cm-callout-fold");
|
|
88
|
+
if (button)
|
|
89
|
+
setFoldState(button, this.folded);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
// 클릭을 에디터가 받도록 합니다. (접기 버튼은 mousedown 핸들러가 처리)
|
|
93
|
+
ignoreEvent() {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** 제목이 있는 콜아웃의 접기 버튼 (제목 뒤) */
|
|
98
|
+
class FoldButtonWidget extends WidgetType {
|
|
99
|
+
folded;
|
|
100
|
+
constructor(folded) {
|
|
101
|
+
super();
|
|
102
|
+
this.folded = folded;
|
|
103
|
+
}
|
|
104
|
+
eq(other) {
|
|
105
|
+
return other.folded === this.folded;
|
|
106
|
+
}
|
|
107
|
+
toDOM() {
|
|
108
|
+
return foldButton(this.folded);
|
|
109
|
+
}
|
|
110
|
+
/** 기존 버튼의 상태만 바꿔 회전 애니메이션이 보이도록 합니다. */
|
|
111
|
+
updateDOM(dom) {
|
|
112
|
+
setFoldState(dom, this.folded);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
ignoreEvent() {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function foldButton(folded) {
|
|
120
|
+
const button = document.createElement("span");
|
|
121
|
+
button.className = "cm-callout-fold";
|
|
122
|
+
button.setAttribute("role", "button");
|
|
123
|
+
button.appendChild(svg(CHEVRON));
|
|
124
|
+
setFoldState(button, folded);
|
|
125
|
+
return button;
|
|
126
|
+
}
|
|
127
|
+
function setFoldState(button, folded) {
|
|
128
|
+
button.classList.toggle("cm-callout-fold-collapsed", folded);
|
|
129
|
+
button.setAttribute("aria-label", folded ? "펼치기" : "접기");
|
|
130
|
+
button.setAttribute("aria-expanded", String(!folded));
|
|
131
|
+
}
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// 접기
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
/** 줄 [lineFrom, lineTo]에 머리줄이 있는 콜아웃 (가장 안쪽) */
|
|
136
|
+
function calloutAtLine(state, lineFrom, lineTo) {
|
|
137
|
+
let found = null;
|
|
138
|
+
syntaxTree(state).iterate({
|
|
139
|
+
from: lineFrom,
|
|
140
|
+
to: lineTo,
|
|
141
|
+
enter(node) {
|
|
142
|
+
if (node.name === "Blockquote" && node.from >= lineFrom && node.from <= lineTo) {
|
|
143
|
+
const header = readCalloutHeader(state, node.node);
|
|
144
|
+
if (header && header.lineFrom === lineFrom)
|
|
145
|
+
found = header;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
return found;
|
|
150
|
+
}
|
|
151
|
+
/** 접을 수 있는 콜아웃의 접기 범위: 머리줄 끝 ~ 콜아웃 끝 (내용이 없으면 null) */
|
|
152
|
+
function foldRangeOf(header) {
|
|
153
|
+
return header.fold && header.to > header.lineTo ? { from: header.lineTo, to: header.to } : null;
|
|
154
|
+
}
|
|
155
|
+
function isFolded(state, range) {
|
|
156
|
+
let folded = false;
|
|
157
|
+
foldedRanges(state).between(range.from, range.from, (from, to) => {
|
|
158
|
+
if (from === range.from && to === range.to)
|
|
159
|
+
folded = true;
|
|
160
|
+
});
|
|
161
|
+
return folded;
|
|
162
|
+
}
|
|
163
|
+
/** 접을 수 있는 콜아웃은 CodeMirror 접기 기능(접기 단축키, 접기 여백)으로도 접을 수 있습니다. */
|
|
164
|
+
const calloutFoldService = foldService.of((state, lineStart, lineEnd) => {
|
|
165
|
+
const header = calloutAtLine(state, lineStart, lineEnd);
|
|
166
|
+
return header ? foldRangeOf(header) : null;
|
|
167
|
+
});
|
|
168
|
+
/** 문서를 열 때 `[!type]-` 콜아웃을 접어 둡니다. (Obsidian과 같이 기본 접힘) */
|
|
169
|
+
const initialFold = ViewPlugin.define((view) => {
|
|
170
|
+
let destroyed = false;
|
|
171
|
+
queueMicrotask(() => {
|
|
172
|
+
if (destroyed)
|
|
173
|
+
return;
|
|
174
|
+
const { state } = view;
|
|
175
|
+
const tree = ensureSyntaxTree(state, state.doc.length, 200) ?? syntaxTree(state);
|
|
176
|
+
const effects = [];
|
|
177
|
+
tree.iterate({
|
|
178
|
+
enter(node) {
|
|
179
|
+
if (node.name === "Blockquote") {
|
|
180
|
+
const header = readCalloutHeader(state, node.node);
|
|
181
|
+
const range = header?.fold === "-" ? foldRangeOf(header) : null;
|
|
182
|
+
if (range && !isFolded(state, range))
|
|
183
|
+
effects.push(foldEffect.of(range));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (!CONTAINERS.has(node.name))
|
|
187
|
+
return false;
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
if (effects.length)
|
|
191
|
+
view.dispatch({ effects });
|
|
192
|
+
});
|
|
193
|
+
return { destroy: () => void (destroyed = true) };
|
|
194
|
+
});
|
|
195
|
+
/** 접기 버튼 클릭 → 접기/펼치기 */
|
|
196
|
+
const calloutEvents = EditorView.domEventHandlers({
|
|
197
|
+
mousedown(event, view) {
|
|
198
|
+
if (event.button !== 0)
|
|
199
|
+
return false;
|
|
200
|
+
const button = event.target?.closest?.(".cm-callout-fold");
|
|
201
|
+
if (!button || !view.contentDOM.contains(button))
|
|
202
|
+
return false;
|
|
203
|
+
const line = view.state.doc.lineAt(view.posAtDOM(button));
|
|
204
|
+
const header = calloutAtLine(view.state, line.from, line.to);
|
|
205
|
+
const range = header && foldRangeOf(header);
|
|
206
|
+
if (!range)
|
|
207
|
+
return false;
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
view.dispatch({
|
|
210
|
+
effects: (isFolded(view.state, range) ? unfoldEffect : foldEffect).of(range),
|
|
211
|
+
});
|
|
212
|
+
return true;
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// 데코레이션
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
/** 콜아웃을 담을 수 있는 컨테이너 블록 */
|
|
219
|
+
const CONTAINERS = new Set([
|
|
220
|
+
"Document",
|
|
221
|
+
"Blockquote",
|
|
222
|
+
"BulletList",
|
|
223
|
+
"OrderedList",
|
|
224
|
+
"ListItem",
|
|
225
|
+
]);
|
|
226
|
+
const markerSource = Decoration.mark({ class: "cm-formatting cm-formatting-callout" });
|
|
227
|
+
const titleText = Decoration.mark({ class: "cm-callout-title-text" });
|
|
228
|
+
/**
|
|
229
|
+
* 줄 데코레이션 (클래스 조합별로 재사용).
|
|
230
|
+
* style 속성은 quote()의 줄 데코레이션과 충돌하므로(같은 줄에 하나만 남음) 클래스만 씁니다.
|
|
231
|
+
*/
|
|
232
|
+
const lineCache = new Map();
|
|
233
|
+
function calloutLine(className) {
|
|
234
|
+
let deco = lineCache.get(className);
|
|
235
|
+
if (!deco)
|
|
236
|
+
lineCache.set(className, (deco = Decoration.line({ class: className })));
|
|
237
|
+
return deco;
|
|
238
|
+
}
|
|
239
|
+
/** 정확한 상자로 그리는 최대 중첩 단계 (줄 배경 + ::before + ::after) */
|
|
240
|
+
const MAX_LEVELS = 3;
|
|
241
|
+
/**
|
|
242
|
+
* 줄의 클래스 목록.
|
|
243
|
+
* - 1단계 상자는 줄 배경, 2·3단계 상자는 ::before/::after로 그립니다.
|
|
244
|
+
* - 한 줄에서 끝나는 상자들은 항상 안쪽부터 이어진 단계들(m..d)이므로, 그 개수로 위아래 여백과
|
|
245
|
+
* 안쪽 상자의 아래쪽 위치를 정합니다. (시작은 한 줄에 한 단계뿐: 안쪽 콜아웃은 다음 줄부터 시작)
|
|
246
|
+
*/
|
|
247
|
+
function lineClasses(allLevels, titleType, blank) {
|
|
248
|
+
const levels = allLevels.slice(0, MAX_LEVELS);
|
|
249
|
+
const depth = levels.length;
|
|
250
|
+
const cls = ["cm-callout", `cm-callout-depth-${depth}`];
|
|
251
|
+
levels.forEach((level, i) => cls.push(`cm-callout-l${i + 1}`, `cm-callout-l${i + 1}-${level.type}`));
|
|
252
|
+
const startAt = levels.findIndex((level) => level.start);
|
|
253
|
+
if (startAt >= 0)
|
|
254
|
+
cls.push(`cm-callout-l${startAt + 1}-start`);
|
|
255
|
+
const endAt = levels.findIndex((level) => level.end);
|
|
256
|
+
if (endAt >= 0) {
|
|
257
|
+
// 바깥 상자가 이어지면(endAt > 0) 안쪽 상자 아래에 간격을 둡니다.
|
|
258
|
+
const gap = endAt > 0 ? 1 : 0;
|
|
259
|
+
cls.push(`cm-callout-pb-${depth - endAt}-${gap}`);
|
|
260
|
+
for (let i = endAt; i < depth; i++) {
|
|
261
|
+
cls.push(`cm-callout-l${i + 1}-end`);
|
|
262
|
+
// 1단계 상자는 줄 배경이므로 위치 조정이 필요 없습니다.
|
|
263
|
+
if (i > 0)
|
|
264
|
+
cls.push(`cm-callout-l${i + 1}-bottom-${i - endAt}-${gap}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (titleType) {
|
|
268
|
+
cls.push("cm-callout-title-line", `cm-callout-title-${titleType}`);
|
|
269
|
+
if (endAt < 0)
|
|
270
|
+
cls.push("cm-callout-title-gap"); // 제목과 내용 사이 간격
|
|
271
|
+
}
|
|
272
|
+
// 빈 인용 줄(">"만 있는 줄): 문단 구분에 필요하지만 한 줄 높이를 다 쓰면 간격이 너무 넓어 낮게 그립니다.
|
|
273
|
+
if (blank)
|
|
274
|
+
cls.push("cm-callout-blank");
|
|
275
|
+
return cls.join(" ");
|
|
276
|
+
}
|
|
277
|
+
export const calloutPlugin = createRevealPlugin({
|
|
278
|
+
// 줄 데코레이션을 바깥 콜아웃이 끝날 때 한꺼번에 추가하므로 정렬합니다.
|
|
279
|
+
sort: true,
|
|
280
|
+
// 접기/펼치기로 접기 버튼 모양과 모서리가 바뀝니다.
|
|
281
|
+
rebuildOn: (update) => update.transactions.some((tr) => tr.effects.some((e) => e.is(foldEffect) || e.is(unfoldEffect))),
|
|
282
|
+
visit({ state, decorations, isRevealed }) {
|
|
283
|
+
const { doc } = state;
|
|
284
|
+
const stack = [];
|
|
285
|
+
let calloutDepth = 0;
|
|
286
|
+
/** 가장 바깥 콜아웃을 원문으로 보여주는지 (커서가 콜아웃 안에 있음) */
|
|
287
|
+
let source = false;
|
|
288
|
+
/** 줄 번호 → 그 줄을 덮는 콜아웃 단계들 (바깥부터) */
|
|
289
|
+
const lineLevels = new Map();
|
|
290
|
+
/** 머리줄 번호 → 그 콜아웃 유형 */
|
|
291
|
+
const titleLines = new Map();
|
|
292
|
+
const decorateHeader = (header, folded) => {
|
|
293
|
+
const foldable = header.fold !== null && header.to > header.lineTo;
|
|
294
|
+
if (header.titleFrom === null) {
|
|
295
|
+
// 제목 없음: 기호 자리에 아이콘 + 기본 제목(+ 접기 버튼)
|
|
296
|
+
const widget = new CalloutHeaderWidget(header.type, defaultCalloutTitle(header.rawType), foldable, folded);
|
|
297
|
+
decorations.push(Decoration.replace({ widget }).range(header.markerFrom, header.lineTo));
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
// 제목 있음: 기호 자리에 아이콘, 제목 스타일, 제목 뒤 접기 버튼
|
|
301
|
+
const widget = new CalloutHeaderWidget(header.type, null, false, folded);
|
|
302
|
+
decorations.push(Decoration.replace({ widget }).range(header.markerFrom, header.titleFrom));
|
|
303
|
+
decorations.push(titleText.range(header.titleFrom, header.titleTo));
|
|
304
|
+
if (foldable) {
|
|
305
|
+
decorations.push(Decoration.widget({ widget: new FoldButtonWidget(folded), side: 1 }).range(header.titleTo));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const flush = () => {
|
|
310
|
+
for (const [n, levels] of lineLevels) {
|
|
311
|
+
const line = doc.line(n);
|
|
312
|
+
const blank = !titleLines.has(n) && !/[^\s>]/.test(line.text);
|
|
313
|
+
decorations.push(calloutLine(lineClasses(levels, titleLines.get(n) ?? null, blank)).range(line.from));
|
|
314
|
+
}
|
|
315
|
+
lineLevels.clear();
|
|
316
|
+
titleLines.clear();
|
|
317
|
+
};
|
|
318
|
+
return {
|
|
319
|
+
enter(node) {
|
|
320
|
+
if (node.name === "Blockquote") {
|
|
321
|
+
const header = readCalloutHeader(state, node.node);
|
|
322
|
+
stack.push(header);
|
|
323
|
+
if (!header)
|
|
324
|
+
return; // 일반 인용 안의 콜아웃을 찾기 위해 내려갑니다.
|
|
325
|
+
calloutDepth++;
|
|
326
|
+
// 커서가 가장 바깥 콜아웃 안에 있으면, 안쪽 콜아웃까지 전체를 원문으로 보여줍니다.
|
|
327
|
+
if (calloutDepth === 1)
|
|
328
|
+
source = isRevealed(header.from, header.to);
|
|
329
|
+
if (source) {
|
|
330
|
+
decorations.push(markerSource.range(header.markerFrom, header.markerTo));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const range = foldRangeOf(header);
|
|
334
|
+
const folded = range !== null && isFolded(state, range);
|
|
335
|
+
decorateHeader(header, folded);
|
|
336
|
+
const first = doc.lineAt(header.from).number;
|
|
337
|
+
const last = folded ? first : doc.lineAt(header.to).number; // 접히면 머리줄이 마지막 줄
|
|
338
|
+
const level = calloutDepth - 1;
|
|
339
|
+
for (let n = first; n <= doc.lineAt(header.to).number; n++) {
|
|
340
|
+
const levels = lineLevels.get(n) ?? [];
|
|
341
|
+
levels.length = level; // 이 콜아웃보다 깊은 단계(이전 형제 콜아웃의 것)는 버림
|
|
342
|
+
levels[level] = { type: header.type, start: n === first, end: n === last };
|
|
343
|
+
lineLevels.set(n, levels);
|
|
344
|
+
}
|
|
345
|
+
titleLines.set(first, header.type);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (!CONTAINERS.has(node.name))
|
|
349
|
+
return false;
|
|
350
|
+
},
|
|
351
|
+
leave(node) {
|
|
352
|
+
if (node.name !== "Blockquote")
|
|
353
|
+
return;
|
|
354
|
+
if (!stack.pop())
|
|
355
|
+
return;
|
|
356
|
+
calloutDepth--;
|
|
357
|
+
if (calloutDepth === 0) {
|
|
358
|
+
if (!source)
|
|
359
|
+
flush();
|
|
360
|
+
source = false;
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// 테마 (Obsidian 기본 콜아웃 디자인 참고)
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
/** 유형별 색 (r, g, b) */
|
|
370
|
+
const COLORS = {
|
|
371
|
+
note: "8, 109, 221",
|
|
372
|
+
abstract: "0, 191, 188",
|
|
373
|
+
info: "8, 109, 221",
|
|
374
|
+
todo: "8, 109, 221",
|
|
375
|
+
tip: "0, 191, 188",
|
|
376
|
+
success: "8, 185, 78",
|
|
377
|
+
question: "236, 117, 0",
|
|
378
|
+
warning: "236, 117, 0",
|
|
379
|
+
failure: "233, 49, 71",
|
|
380
|
+
danger: "233, 49, 71",
|
|
381
|
+
bug: "233, 49, 71",
|
|
382
|
+
example: "120, 82, 238",
|
|
383
|
+
quote: "158, 158, 158",
|
|
384
|
+
};
|
|
385
|
+
const TYPES = Object.keys(COLORS);
|
|
386
|
+
const PAD = 12; // 상자 안쪽 위아래 여백
|
|
387
|
+
const GAP = 8; // 안쪽 상자와 바깥 내용 사이 간격
|
|
388
|
+
const TITLE_GAP = 8; // 제목과 내용 사이 간격
|
|
389
|
+
const INSET = 14; // 중첩 한 단계마다 안쪽 상자를 들이는 폭
|
|
390
|
+
const TEXT = 24; // 상자 왼쪽에서 글자까지
|
|
391
|
+
const RADIUS = "4px";
|
|
392
|
+
const px = (n) => `${n}px`;
|
|
393
|
+
/** 2·3단계 상자를 그리는 가상 요소 */
|
|
394
|
+
const PSEUDO = { 2: "::before", 3: "::after" };
|
|
395
|
+
function themeSpec() {
|
|
396
|
+
const spec = {
|
|
397
|
+
".cm-callout": {
|
|
398
|
+
// 가상 요소(안쪽 상자)를 글자 뒤, 줄 배경 위에 그리기 위한 쌓임 맥락
|
|
399
|
+
position: "relative",
|
|
400
|
+
zIndex: "0",
|
|
401
|
+
// 양옆을 살짝 띄웁니다. (코드 블록과 같은 변수)
|
|
402
|
+
marginLeft: BLOCK_MARGIN_X,
|
|
403
|
+
marginRight: BLOCK_MARGIN_X,
|
|
404
|
+
},
|
|
405
|
+
".cm-callout-l1": {
|
|
406
|
+
backgroundColor: "rgba(var(--cm-callout-c1), 0.1)",
|
|
407
|
+
},
|
|
408
|
+
".cm-callout-l1-start": {
|
|
409
|
+
borderTopLeftRadius: RADIUS,
|
|
410
|
+
borderTopRightRadius: RADIUS,
|
|
411
|
+
paddingTop: px(PAD),
|
|
412
|
+
},
|
|
413
|
+
".cm-callout-l1-end": {
|
|
414
|
+
borderBottomLeftRadius: RADIUS,
|
|
415
|
+
borderBottomRightRadius: RADIUS,
|
|
416
|
+
},
|
|
417
|
+
".cm-callout-title-line": { fontWeight: "600" },
|
|
418
|
+
".cm-callout-title-gap": { paddingBottom: px(TITLE_GAP) },
|
|
419
|
+
// 머리줄의 제목에는 구문 강조 색이 끼어들지 않도록 콜아웃 색을 상속합니다.
|
|
420
|
+
".cm-line.cm-callout-title-line .cm-callout-title-text, .cm-line.cm-callout-title-line .cm-callout-title-text *": {
|
|
421
|
+
color: "inherit",
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
for (const type of TYPES) {
|
|
425
|
+
spec[`.cm-callout-title-${type}`] = { color: `rgb(${COLORS[type]})` };
|
|
426
|
+
for (let k = 1; k <= MAX_LEVELS; k++)
|
|
427
|
+
spec[`.cm-callout-l${k}-${type}`] = { [`--cm-callout-c${k}`]: COLORS[type] };
|
|
428
|
+
}
|
|
429
|
+
for (let depth = 1; depth <= MAX_LEVELS; depth++) {
|
|
430
|
+
const inset = (depth - 1) * INSET;
|
|
431
|
+
spec[`.cm-callout-depth-${depth}`] = {
|
|
432
|
+
paddingLeft: px(inset + TEXT),
|
|
433
|
+
paddingRight: px(inset + PAD),
|
|
434
|
+
// 내용 영역을 다른 블록(코드 블록 등)에도 알려줍니다. (utils/container.ts)
|
|
435
|
+
"--cm-container-indent": px(inset + TEXT),
|
|
436
|
+
"--cm-container-right": px(inset + PAD),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
// 2·3단계 상자: 바깥 상자 안쪽으로 들인 가상 요소
|
|
440
|
+
for (let k = 2; k <= MAX_LEVELS; k++) {
|
|
441
|
+
const pseudo = PSEUDO[k];
|
|
442
|
+
const inset = px((k - 1) * INSET);
|
|
443
|
+
spec[`.cm-callout-l${k}${pseudo}`] = {
|
|
444
|
+
content: '""',
|
|
445
|
+
position: "absolute",
|
|
446
|
+
left: inset,
|
|
447
|
+
right: inset,
|
|
448
|
+
top: "0",
|
|
449
|
+
bottom: "0",
|
|
450
|
+
zIndex: "-1",
|
|
451
|
+
backgroundColor: `rgba(var(--cm-callout-c${k}), 0.1)`,
|
|
452
|
+
};
|
|
453
|
+
// 시작: 바깥 상자 안에서 위쪽 간격을 두고 둥근 모서리
|
|
454
|
+
spec[`.cm-callout-l${k}-start`] = { paddingTop: px(PAD + GAP) };
|
|
455
|
+
spec[`.cm-callout-l${k}-start${pseudo}`] = {
|
|
456
|
+
top: px(GAP),
|
|
457
|
+
borderTopLeftRadius: RADIUS,
|
|
458
|
+
borderTopRightRadius: RADIUS,
|
|
459
|
+
};
|
|
460
|
+
spec[`.cm-callout-l${k}-end${pseudo}`] = {
|
|
461
|
+
borderBottomLeftRadius: RADIUS,
|
|
462
|
+
borderBottomRightRadius: RADIUS,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
// 끝: 이 줄에서 끝나는 상자 수(count)만큼 아래 여백, 바깥 상자가 이어지면(gap) 간격 추가
|
|
466
|
+
for (let count = 1; count <= MAX_LEVELS; count++) {
|
|
467
|
+
for (const gap of [0, 1]) {
|
|
468
|
+
const bottom = px(count * PAD + gap * GAP);
|
|
469
|
+
spec[`.cm-callout-pb-${count}-${gap}`] = { paddingBottom: bottom, "--cm-container-bottom": bottom };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// 안쪽 상자의 아래쪽 위치: 그 바깥에서 함께 끝나는 상자 수(n)만큼 위로
|
|
473
|
+
for (let k = 2; k <= MAX_LEVELS; k++) {
|
|
474
|
+
for (let n = 0; n < k; n++) {
|
|
475
|
+
for (const gap of [0, 1]) {
|
|
476
|
+
spec[`.cm-callout-l${k}-bottom-${n}-${gap}${PSEUDO[k]}`] = { bottom: px(n * PAD + gap * GAP) };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
Object.assign(spec, {
|
|
481
|
+
".cm-callout-header": {
|
|
482
|
+
display: "inline-flex",
|
|
483
|
+
alignItems: "center",
|
|
484
|
+
gap: "6px",
|
|
485
|
+
// 제목이 문서 글자(위젯 밖)일 때도 아이콘과 제목 사이가 벌어지도록 뒤에 여백을 줍니다.
|
|
486
|
+
marginRight: "6px",
|
|
487
|
+
},
|
|
488
|
+
".cm-callout-icon, .cm-callout-fold": {
|
|
489
|
+
display: "inline-flex",
|
|
490
|
+
width: "1em",
|
|
491
|
+
height: "1em",
|
|
492
|
+
verticalAlign: "-0.125em",
|
|
493
|
+
},
|
|
494
|
+
".cm-callout-icon svg, .cm-callout-fold svg": { width: "100%", height: "100%" },
|
|
495
|
+
".cm-callout-fold": {
|
|
496
|
+
marginLeft: "6px",
|
|
497
|
+
cursor: "pointer",
|
|
498
|
+
opacity: "0.7",
|
|
499
|
+
transition: "transform 150ms ease, opacity 150ms ease",
|
|
500
|
+
},
|
|
501
|
+
".cm-callout-fold:hover": { opacity: "1" },
|
|
502
|
+
".cm-callout-fold-collapsed": { transform: "rotate(-90deg)" },
|
|
503
|
+
// 동작 줄이기 설정을 켠 사용자에게는 애니메이션 없이 바로 바꿉니다.
|
|
504
|
+
"@media (prefers-reduced-motion: reduce)": {
|
|
505
|
+
".cm-callout-fold": { transition: "none" },
|
|
506
|
+
},
|
|
507
|
+
// 접기 버튼이 상태를 보여주므로 CodeMirror 기본 접기 표시(…)는 숨깁니다.
|
|
508
|
+
".cm-callout-title-line .cm-foldPlaceholder": { display: "none" },
|
|
509
|
+
".cm-callout-blank": { fontSize: "0.5em" },
|
|
510
|
+
// 원문 보기: 기호를 흐리게. ([!type]은 링크로 파싱되어 밑줄이 생기므로 없앱니다)
|
|
511
|
+
".cm-formatting-callout": { opacity: "0.6" },
|
|
512
|
+
".cm-formatting-callout, .cm-formatting-callout *": { textDecoration: "none" },
|
|
513
|
+
});
|
|
514
|
+
return spec;
|
|
515
|
+
}
|
|
516
|
+
const calloutTheme = EditorView.baseTheme(themeSpec());
|
|
517
|
+
/**
|
|
518
|
+
* Obsidian 콜아웃 / GFM 알림을 보여주는 확장입니다.
|
|
519
|
+
*
|
|
520
|
+
* ```
|
|
521
|
+
* > [!note] 제목 유형: note, abstract, info, todo, tip, success, question,
|
|
522
|
+
* > 내용 warning, failure, danger, bug, example, quote (+ 별칭)
|
|
523
|
+
* > [!faq]- 접힌 콜아웃 +: 접을 수 있음(펼침), -: 접을 수 있음(기본 접힘)
|
|
524
|
+
* ```
|
|
525
|
+
* - 유형별 색 배경 + 아이콘 + 제목. 제목이 없으면 유형 이름을 제목으로 보여줍니다.
|
|
526
|
+
* - 커서가 콜아웃 안에 들어오면 콜아웃 전체(안쪽 콜아웃 포함)를 원문으로 보여줍니다. (Obsidian과 같은 편집 방식)
|
|
527
|
+
* - 중첩 콜아웃은 바깥 상자 안의 상자로 그립니다. (3단계까지 정확히, 더 깊으면 3단계로)
|
|
528
|
+
* - quote()를 포함합니다. (`>` 숨김, 콜아웃이 아닌 인용의 세로줄)
|
|
529
|
+
*/
|
|
530
|
+
export function callout() {
|
|
531
|
+
return [
|
|
532
|
+
quote(),
|
|
533
|
+
calloutsHandled.of(true),
|
|
534
|
+
calloutPlugin,
|
|
535
|
+
codeFolding(),
|
|
536
|
+
calloutFoldService,
|
|
537
|
+
initialFold,
|
|
538
|
+
calloutEvents,
|
|
539
|
+
calloutTheme,
|
|
540
|
+
];
|
|
541
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Facet, type EditorState } from "@codemirror/state";
|
|
2
|
+
import type { SyntaxNode } from "@lezer/common";
|
|
3
|
+
/**
|
|
4
|
+
* 콜아웃(Obsidian callout / GFM alert) 머리줄 해석.
|
|
5
|
+
*
|
|
6
|
+
* 콜아웃은 첫 줄이 `[!type]`으로 시작하는 인용(Blockquote)입니다.
|
|
7
|
+
* 파서를 바꾸지 않고 기존 Blockquote 구조 위에서 판별하므로, 내용 안의 목록/서식/게으른 연속 줄이
|
|
8
|
+
* 그대로 동작하고 다른 마크다운 뷰어에서도 인용으로 해석됩니다.
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* > [!type]+/- 제목(선택)
|
|
12
|
+
* > 내용
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
/** 표준 유형 */
|
|
16
|
+
export type CalloutType = "note" | "abstract" | "info" | "todo" | "tip" | "success" | "question" | "warning" | "failure" | "danger" | "bug" | "example" | "quote";
|
|
17
|
+
export interface CalloutHeader {
|
|
18
|
+
/** 표준 유형 (알 수 없는 유형은 "note") */
|
|
19
|
+
readonly type: CalloutType;
|
|
20
|
+
/** 원문에 쓰인 유형 이름 */
|
|
21
|
+
readonly rawType: string;
|
|
22
|
+
/** 접기 표시: "+" 펼친 상태, "-" 접힌 상태(기본), null 접을 수 없음 */
|
|
23
|
+
readonly fold: "+" | "-" | null;
|
|
24
|
+
/** 제목. 없으면 null (화면에는 유형 이름을 보여줍니다) */
|
|
25
|
+
readonly title: string | null;
|
|
26
|
+
/** 인용 전체 범위 */
|
|
27
|
+
readonly from: number;
|
|
28
|
+
readonly to: number;
|
|
29
|
+
/** 머리줄 범위 */
|
|
30
|
+
readonly lineFrom: number;
|
|
31
|
+
readonly lineTo: number;
|
|
32
|
+
/** `[!type]+` 기호 범위 */
|
|
33
|
+
readonly markerFrom: number;
|
|
34
|
+
readonly markerTo: number;
|
|
35
|
+
/** 제목 범위 (없으면 null) */
|
|
36
|
+
readonly titleFrom: number | null;
|
|
37
|
+
readonly titleTo: number | null;
|
|
38
|
+
}
|
|
39
|
+
/** 화면에 보여줄 기본 제목: 유형 이름의 첫 글자만 대문자 ("faq" → "Faq", "NOTE" → "Note") */
|
|
40
|
+
export declare const defaultCalloutTitle: (rawType: string) => string;
|
|
41
|
+
/** Blockquote 노드가 콜아웃이면 머리줄 정보를, 아니면 null을 반환합니다. */
|
|
42
|
+
export declare function readCalloutHeader(state: EditorState, quote: SyntaxNode): CalloutHeader | null;
|
|
43
|
+
/**
|
|
44
|
+
* 콜아웃을 callout()이 그리는지 여부.
|
|
45
|
+
* true이면 quote()는 콜아웃 줄에 왼쪽 세로줄을 그리지 않습니다. (`>` 숨김은 계속 담당)
|
|
46
|
+
*/
|
|
47
|
+
export declare const calloutsHandled: Facet<boolean, boolean>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Facet } from "@codemirror/state";
|
|
2
|
+
/** 유형 이름(별칭 포함, 소문자) → 표준 유형. (Obsidian 기준. GFM의 IMPORTANT/CAUTION도 별칭) */
|
|
3
|
+
const ALIASES = {
|
|
4
|
+
note: "note",
|
|
5
|
+
abstract: "abstract",
|
|
6
|
+
summary: "abstract",
|
|
7
|
+
tldr: "abstract",
|
|
8
|
+
info: "info",
|
|
9
|
+
todo: "todo",
|
|
10
|
+
tip: "tip",
|
|
11
|
+
hint: "tip",
|
|
12
|
+
important: "tip",
|
|
13
|
+
success: "success",
|
|
14
|
+
check: "success",
|
|
15
|
+
done: "success",
|
|
16
|
+
question: "question",
|
|
17
|
+
help: "question",
|
|
18
|
+
faq: "question",
|
|
19
|
+
warning: "warning",
|
|
20
|
+
caution: "warning",
|
|
21
|
+
attention: "warning",
|
|
22
|
+
failure: "failure",
|
|
23
|
+
fail: "failure",
|
|
24
|
+
missing: "failure",
|
|
25
|
+
danger: "danger",
|
|
26
|
+
error: "danger",
|
|
27
|
+
bug: "bug",
|
|
28
|
+
example: "example",
|
|
29
|
+
quote: "quote",
|
|
30
|
+
cite: "quote",
|
|
31
|
+
};
|
|
32
|
+
const HEADER_RE = /^\[!([\w-]+)\]([+-]?)/;
|
|
33
|
+
/** 화면에 보여줄 기본 제목: 유형 이름의 첫 글자만 대문자 ("faq" → "Faq", "NOTE" → "Note") */
|
|
34
|
+
export const defaultCalloutTitle = (rawType) => rawType.charAt(0).toUpperCase() + rawType.slice(1).toLowerCase();
|
|
35
|
+
/** Blockquote 노드가 콜아웃이면 머리줄 정보를, 아니면 null을 반환합니다. */
|
|
36
|
+
export function readCalloutHeader(state, quote) {
|
|
37
|
+
if (quote.name !== "Blockquote")
|
|
38
|
+
return null;
|
|
39
|
+
const mark = quote.firstChild;
|
|
40
|
+
if (!mark || mark.name !== "QuoteMark")
|
|
41
|
+
return null;
|
|
42
|
+
const line = state.doc.lineAt(mark.from);
|
|
43
|
+
const afterMark = mark.to - line.from;
|
|
44
|
+
const rest = line.text.slice(afterMark);
|
|
45
|
+
const indent = rest.length - rest.trimStart().length;
|
|
46
|
+
const match = HEADER_RE.exec(rest.slice(indent));
|
|
47
|
+
if (!match)
|
|
48
|
+
return null;
|
|
49
|
+
const markerFrom = line.from + afterMark + indent;
|
|
50
|
+
const markerTo = markerFrom + match[0].length;
|
|
51
|
+
const titleRaw = line.text.slice(markerTo - line.from);
|
|
52
|
+
const title = titleRaw.trim();
|
|
53
|
+
const titleFrom = title ? markerTo + (titleRaw.length - titleRaw.trimStart().length) : null;
|
|
54
|
+
return {
|
|
55
|
+
type: ALIASES[match[1].toLowerCase()] ?? "note",
|
|
56
|
+
rawType: match[1],
|
|
57
|
+
fold: match[2] === "+" || match[2] === "-" ? match[2] : null,
|
|
58
|
+
title: title || null,
|
|
59
|
+
from: quote.from,
|
|
60
|
+
to: quote.to,
|
|
61
|
+
lineFrom: line.from,
|
|
62
|
+
lineTo: line.to,
|
|
63
|
+
markerFrom,
|
|
64
|
+
markerTo,
|
|
65
|
+
titleFrom,
|
|
66
|
+
titleTo: titleFrom === null ? null : titleFrom + title.length,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 콜아웃을 callout()이 그리는지 여부.
|
|
71
|
+
* true이면 quote()는 콜아웃 줄에 왼쪽 세로줄을 그리지 않습니다. (`>` 숨김은 계속 담당)
|
|
72
|
+
*/
|
|
73
|
+
export const calloutsHandled = Facet.define({
|
|
74
|
+
combine: (values) => values.some(Boolean),
|
|
75
|
+
});
|