mdsquare 0.1.0 → 0.3.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/README.md +115 -19
- package/dist/callout.js +3 -0
- package/dist/codeBlock.js +3 -1
- package/dist/editor.d.ts +19 -1
- package/dist/editor.js +48 -22
- package/dist/extensions.d.ts +11 -3
- package/dist/extensions.js +6 -4
- package/dist/footnote.d.ts +24 -3
- package/dist/footnote.js +458 -163
- package/dist/footnoteSyntax.d.ts +6 -3
- package/dist/footnoteSyntax.js +37 -39
- package/dist/hashtag.js +8 -3
- package/dist/heading.js +6 -0
- package/dist/horizontalRule.js +1 -0
- package/dist/link.d.ts +47 -1
- package/dist/link.js +347 -14
- package/dist/list.js +39 -0
- package/dist/listIndent.js +1 -0
- package/dist/mention.js +16 -4
- package/dist/quote.js +70 -16
- package/dist/tableModel.js +1 -0
- package/dist/task.js +1 -0
- package/dist/tikz.js +1 -1
- package/dist/utils/textWidth.d.ts +6 -0
- package/dist/utils/textWidth.js +11 -0
- package/package.json +1 -1
package/dist/footnote.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { syntaxTree } from "@codemirror/language";
|
|
2
|
-
import { EditorSelection, RangeSet, StateField } from "@codemirror/state";
|
|
3
|
-
import { Decoration, EditorView, GutterMarker, gutterLineClass, hoverTooltip } from "@codemirror/view";
|
|
4
|
-
import {
|
|
5
|
-
import { isSafeUrl, readInlineLink } from "./link.js";
|
|
2
|
+
import { EditorSelection, EditorState, Facet, RangeSet, StateEffect, StateField, Prec, Transaction, } from "@codemirror/state";
|
|
3
|
+
import { Decoration, EditorView, GutterMarker, gutterLineClass, hoverTooltip, keymap, } from "@codemirror/view";
|
|
4
|
+
import { FOOTNOTE_INDENT } from "./footnoteSyntax.js";
|
|
6
5
|
import { createRevealPlugin } from "./utils/reveal.js";
|
|
6
|
+
import { displayWidth } from "./utils/textWidth.js";
|
|
7
7
|
/** 라벨 비교는 대소문자를 구분하지 않습니다. */
|
|
8
8
|
const normalize = (label) => label.toLowerCase();
|
|
9
9
|
const TOOLTIP_MAX = 200;
|
|
@@ -52,9 +52,42 @@ export function footnoteDefinitions(state) {
|
|
|
52
52
|
const hidden = Decoration.replace({});
|
|
53
53
|
const formatting = Decoration.mark({ class: "cm-formatting cm-formatting-footnote" });
|
|
54
54
|
const revealedRefLabel = Decoration.mark({ class: "cm-footnote-label" });
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
/**
|
|
56
|
+
* 정의 줄 (클래스 조합별로 재사용). 정의의 모든 줄에 cm-footnote-def-line(왼쪽 레일)이 붙고,
|
|
57
|
+
* 첫 줄에는 cm-footnote-def-first(위 간격), 묶음 첫 줄에는 cm-footnote-defs-start(구분선),
|
|
58
|
+
* 배지를 따로 한 줄에 그리는 정의의 첫 줄에는 cm-footnote-def-head가 붙습니다.
|
|
59
|
+
*/
|
|
60
|
+
const defLineCache = new Map();
|
|
61
|
+
function defLine(extra) {
|
|
62
|
+
let deco = defLineCache.get(extra);
|
|
63
|
+
if (!deco)
|
|
64
|
+
defLineCache.set(extra, (deco = Decoration.line({ class: `cm-footnote-def-line${extra}` })));
|
|
65
|
+
return deco;
|
|
66
|
+
}
|
|
67
|
+
/** 배지를 첫 줄 안에 둘 수 있는 라벨 폭 (배지 3칸 + 간격 1칸 = 이어지는 줄의 들여쓰기 4칸) */
|
|
68
|
+
const INLINE_LABEL_WIDTH = 2;
|
|
69
|
+
// 마우스를 올린 정의(레일을 진하게)와 참조에서 이동해 온 정의(잠깐 반짝임)
|
|
70
|
+
const setHoveredFootnote = StateEffect.define();
|
|
71
|
+
const setFlashedFootnote = StateEffect.define();
|
|
72
|
+
const footnoteUi = StateField.define({
|
|
73
|
+
create: () => ({ hover: -1, flash: -1 }),
|
|
74
|
+
update(value, tr) {
|
|
75
|
+
let { hover, flash } = value;
|
|
76
|
+
if (tr.docChanged) {
|
|
77
|
+
if (hover >= 0)
|
|
78
|
+
hover = tr.changes.mapPos(hover);
|
|
79
|
+
if (flash >= 0)
|
|
80
|
+
flash = tr.changes.mapPos(flash);
|
|
81
|
+
}
|
|
82
|
+
for (const e of tr.effects) {
|
|
83
|
+
if (e.is(setHoveredFootnote))
|
|
84
|
+
hover = e.value;
|
|
85
|
+
else if (e.is(setFlashedFootnote))
|
|
86
|
+
flash = e.value;
|
|
87
|
+
}
|
|
88
|
+
return hover === value.hover && flash === value.flash ? value : { hover, flash };
|
|
89
|
+
},
|
|
90
|
+
});
|
|
58
91
|
const defLabel = Decoration.mark({ class: "cm-footnote-def-label" });
|
|
59
92
|
/** 참조 라벨 (툴팁은 hoverTooltip이 담당하므로 title 속성을 쓰지 않습니다) */
|
|
60
93
|
const refLabel = Decoration.mark({ class: "cm-footnote-ref" });
|
|
@@ -79,8 +112,12 @@ function partsOf(node) {
|
|
|
79
112
|
export const footnotePlugin = createRevealPlugin({
|
|
80
113
|
// 정의 줄 데코레이션과 기호/참조 데코레이션이 섞일 수 있으므로 정렬합니다.
|
|
81
114
|
sort: true,
|
|
115
|
+
// 마우스를 올린 정의, 반짝이는 정의가 바뀌면 다시 그립니다.
|
|
116
|
+
rebuildOn: (update) => update.startState.field(footnoteUi, false) !== update.state.field(footnoteUi, false),
|
|
82
117
|
visit({ state, decorations, isRevealed }) {
|
|
83
|
-
const
|
|
118
|
+
const ui = state.field(footnoteUi, false);
|
|
119
|
+
// 미리보기 에디터 안의 참조는 원래 문서의 정의를 봅니다.
|
|
120
|
+
const defs = footnoteDefinitions(sourceOf(state));
|
|
84
121
|
return {
|
|
85
122
|
enter(node) {
|
|
86
123
|
if (node.name === "FootnoteReference") {
|
|
@@ -102,22 +139,31 @@ export const footnotePlugin = createRevealPlugin({
|
|
|
102
139
|
if (!parts)
|
|
103
140
|
return false;
|
|
104
141
|
const [open, label, close] = parts;
|
|
105
|
-
// 정의의 모든 줄(이어지는 줄 포함)에 정의 스타일을 적용하고,
|
|
106
|
-
// 연속된 정의 묶음의 첫 줄에는 본문과 구분하는 구분선을 그립니다.
|
|
107
142
|
const { doc } = state;
|
|
143
|
+
// 내용을 입력하는 동안 기호가 펼쳐지지 않도록, 라벨 부분([^label]:)에 닿을 때만 원문을 보여줍니다.
|
|
144
|
+
const revealed = isRevealed(open.from, close.to);
|
|
145
|
+
// 정의의 모든 줄(이어지는 줄 포함)에 왼쪽 레일을 긋고, 정의마다 위에 간격을 둡니다.
|
|
146
|
+
// 연속된 정의 묶음의 첫 줄에는 본문과 구분하는 구분선을 그립니다.
|
|
108
147
|
const groupStart = node.node.prevSibling?.name !== "FootnoteDefinition";
|
|
109
148
|
const firstLine = doc.lineAt(node.from).number;
|
|
110
149
|
const lastLine = doc.lineAt(node.to).number;
|
|
150
|
+
// 라벨이 길어 배지가 들여쓰기(4칸)에 들어가지 않으면, 여러 줄 정의는 배지를 따로 한 줄에 그려
|
|
151
|
+
// 내용이 모두 같은 열에서 시작하게 합니다. (한 줄 정의는 배지 옆에 이어 씀)
|
|
152
|
+
const head = !revealed &&
|
|
153
|
+
lastLine > firstLine &&
|
|
154
|
+
displayWidth(doc.sliceString(label.from, label.to)) > INLINE_LABEL_WIDTH;
|
|
155
|
+
const state_ = `${ui?.hover === node.from ? " cm-footnote-def-hover" : ""}${ui?.flash === node.from ? " cm-footnote-def-flash" : ""}`;
|
|
156
|
+
const first = ` cm-footnote-def-first${groupStart ? " cm-footnote-defs-start" : ""}${head ? " cm-footnote-def-head" : ""}`;
|
|
111
157
|
for (let n = firstLine; n <= lastLine; n++) {
|
|
112
|
-
|
|
113
|
-
decorations.push(deco.range(doc.line(n).from));
|
|
158
|
+
decorations.push(defLine((n === firstLine ? first : "") + state_).range(doc.line(n).from));
|
|
114
159
|
}
|
|
115
|
-
|
|
116
|
-
if (isRevealed(open.from, close.to)) {
|
|
160
|
+
if (revealed) {
|
|
117
161
|
decorations.push(formatting.range(open.from, open.to), revealedRefLabel.range(label.from, label.to), formatting.range(close.from, close.to));
|
|
118
162
|
}
|
|
119
163
|
else {
|
|
120
|
-
|
|
164
|
+
// `]:` 뒤의 공백 하나도 숨깁니다. (배지와 내용 사이 간격은 CSS로 1칸)
|
|
165
|
+
const next = doc.sliceString(close.to, close.to + 1);
|
|
166
|
+
decorations.push(hidden.range(open.from, open.to), defLabel.range(label.from, label.to), hidden.range(close.from, next === " " || next === "\t" ? close.to + 1 : close.to));
|
|
121
167
|
}
|
|
122
168
|
return; // 정의 내용 안의 각주 참조도 처리합니다.
|
|
123
169
|
}
|
|
@@ -128,118 +174,51 @@ export const footnotePlugin = createRevealPlugin({
|
|
|
128
174
|
},
|
|
129
175
|
});
|
|
130
176
|
// ---------------------------------------------------------------------------
|
|
131
|
-
// 툴팁:
|
|
132
|
-
//
|
|
133
|
-
// (
|
|
177
|
+
// 툴팁: 각주 내용을 읽기 전용 작은 에디터로 보여줍니다.
|
|
178
|
+
// 본문 에디터의 상태(설정)를 그대로 이어받고 문서만 각주 내용으로 바꾸므로,
|
|
179
|
+
// 켜져 있는 기능(표, 수식, TikZ, 코드 강조, 이미지 주소 변환, 테마 등)이 본문과 똑같이 그려집니다.
|
|
134
180
|
// ---------------------------------------------------------------------------
|
|
135
|
-
/**
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
"StrikethroughMark",
|
|
143
|
-
"SuperscriptMark",
|
|
144
|
-
"SubscriptMark",
|
|
145
|
-
"FootnoteMark",
|
|
146
|
-
"URL",
|
|
147
|
-
"LinkTitle",
|
|
148
|
-
"LinkLabel",
|
|
149
|
-
]);
|
|
150
|
-
/** 노드 → HTML 태그 */
|
|
151
|
-
const TOOLTIP_TAGS = {
|
|
152
|
-
StrongEmphasis: "strong",
|
|
153
|
-
Emphasis: "em",
|
|
154
|
-
Strikethrough: "s",
|
|
155
|
-
Highlight: "mark",
|
|
156
|
-
Underline: "u",
|
|
157
|
-
InlineCode: "code",
|
|
158
|
-
Superscript: "sup",
|
|
159
|
-
Subscript: "sub",
|
|
160
|
-
};
|
|
161
|
-
/** 마크다운의 줄바꿈(소프트 줄바꿈)은 공백으로 보여줍니다. */
|
|
162
|
-
const appendText = (parent, text) => {
|
|
163
|
-
if (text)
|
|
164
|
-
parent.appendChild(document.createTextNode(text.replace(/[ \t]*\n[ \t]*/g, " ")));
|
|
165
|
-
};
|
|
166
|
-
/** node의 [from, to] 범위를 자식 노드와 그 사이 글자로 나눠 그립니다. */
|
|
167
|
-
function renderRange(state, node, from, to, parent) {
|
|
168
|
-
let pos = from;
|
|
169
|
-
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
170
|
-
if (child.to <= from || child.from >= to)
|
|
171
|
-
continue;
|
|
172
|
-
if (child.from > pos)
|
|
173
|
-
appendText(parent, state.doc.sliceString(pos, child.from));
|
|
174
|
-
renderNode(state, child, parent);
|
|
175
|
-
pos = Math.max(pos, child.to);
|
|
176
|
-
}
|
|
177
|
-
if (pos < to)
|
|
178
|
-
appendText(parent, state.doc.sliceString(pos, to));
|
|
181
|
+
/** 미리보기 에디터가 각주 정의를 찾을 원래 문서 (미리보기 안의 각주 참조도 원래 문서의 정의를 씀) */
|
|
182
|
+
const footnoteSource = Facet.define({ combine: (values) => values[0] ?? null });
|
|
183
|
+
/** 각주 정의를 찾을 상태 (미리보기 에디터면 원래 문서) */
|
|
184
|
+
const sourceOf = (state) => state.facet(footnoteSource) ?? state;
|
|
185
|
+
/** 각주 미리보기 에디터인지 */
|
|
186
|
+
export function isFootnotePreview(state) {
|
|
187
|
+
return state.facet(footnoteSource) !== null;
|
|
179
188
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return appendText(parent, state.doc.sliceString(node.from + 1, node.to));
|
|
186
|
-
if (name === "HardBreak")
|
|
187
|
-
return void parent.appendChild(document.createElement("br"));
|
|
188
|
-
if (name === "FootnoteReference") {
|
|
189
|
-
const label = node.getChild("FootnoteLabel");
|
|
190
|
-
const sup = document.createElement("sup");
|
|
191
|
-
sup.className = "cm-footnote-tooltip-ref";
|
|
192
|
-
if (label)
|
|
193
|
-
sup.textContent = state.doc.sliceString(label.from, label.to);
|
|
194
|
-
return void parent.appendChild(sup);
|
|
195
|
-
}
|
|
196
|
-
if (name === "Link") {
|
|
197
|
-
const link = readInlineLink(state, node);
|
|
198
|
-
const a = document.createElement("a");
|
|
199
|
-
a.className = "cm-footnote-tooltip-link";
|
|
200
|
-
// 안전한 주소만 링크로 만듭니다. (javascript: 등은 글자만 표시)
|
|
201
|
-
if (link && isSafeUrl(link.href)) {
|
|
202
|
-
a.href = link.href;
|
|
203
|
-
a.target = "_blank";
|
|
204
|
-
a.rel = "noopener noreferrer";
|
|
205
|
-
}
|
|
206
|
-
if (link)
|
|
207
|
-
renderRange(state, node, link.labelFrom, link.labelTo, a);
|
|
208
|
-
else
|
|
209
|
-
renderRange(state, node, node.from, node.to, a);
|
|
210
|
-
return void parent.appendChild(a);
|
|
211
|
-
}
|
|
212
|
-
if (name === "Image") {
|
|
213
|
-
// 툴팁에는 이미지 대신 캡션(대체 텍스트)을 보여줍니다.
|
|
214
|
-
const image = readImage(state, node);
|
|
215
|
-
const span = document.createElement("span");
|
|
216
|
-
span.className = "cm-footnote-tooltip-image";
|
|
217
|
-
span.textContent = image?.caption || "이미지";
|
|
218
|
-
return void parent.appendChild(span);
|
|
219
|
-
}
|
|
220
|
-
const el = document.createElement(TOOLTIP_TAGS[name] ?? "span");
|
|
221
|
-
if (!TOOLTIP_TAGS[name])
|
|
222
|
-
el.className = `cm-footnote-tooltip-${name.toLowerCase()}`;
|
|
223
|
-
renderRange(state, node, node.from, node.to, el);
|
|
224
|
-
parent.appendChild(el);
|
|
225
|
-
}
|
|
226
|
-
function definitionNode(state, def) {
|
|
227
|
-
for (let n = syntaxTree(state).resolveInner(def.from, 1); n; n = n.parent) {
|
|
228
|
-
if (n.name === "FootnoteDefinition")
|
|
229
|
-
return n;
|
|
230
|
-
}
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
/** 각주 정의 내용을 서식이 반영된 DOM으로 그립니다. 정의가 없으면 null */
|
|
234
|
-
export function renderFootnoteDefinition(state, label) {
|
|
189
|
+
/**
|
|
190
|
+
* 각주 정의 내용 (첫 줄은 `[^label]: ` 뒤부터, 이어지는 줄은 4칸 들여쓰기를 뺀 글).
|
|
191
|
+
* 정의가 없으면 null
|
|
192
|
+
*/
|
|
193
|
+
export function footnoteContent(state, label) {
|
|
235
194
|
const def = footnoteDefinitions(state).get(normalize(label));
|
|
236
|
-
|
|
237
|
-
if (!def || !node)
|
|
195
|
+
if (!def)
|
|
238
196
|
return null;
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
197
|
+
const lines = state.doc.sliceString(def.contentFrom, def.to).split("\n");
|
|
198
|
+
return lines.map((line, i) => (i === 0 ? line : line.replace(/^(?: {1,4}|\t)/, ""))).join("\n");
|
|
199
|
+
}
|
|
200
|
+
/** 미리보기의 마지막 빈 줄 (커서를 두는 곳, 화면에서는 숨김) */
|
|
201
|
+
const previewTailLine = Decoration.line({ class: "cm-footnote-preview-tail" });
|
|
202
|
+
const previewTail = EditorView.decorations.compute(["doc"], (state) => Decoration.set(previewTailLine.range(state.doc.line(state.doc.lines).from)));
|
|
203
|
+
/**
|
|
204
|
+
* 각주 미리보기용 상태: state의 설정을 그대로 쓰고 문서만 content로 바꿉니다. (읽기 전용)
|
|
205
|
+
* 커서는 끝에 덧붙인 빈 줄에 둬서 어떤 블록도 원문으로 펼쳐지지 않게 합니다.
|
|
206
|
+
*/
|
|
207
|
+
export function footnotePreviewState(state, content) {
|
|
208
|
+
const doc = `${content}\n`;
|
|
209
|
+
return state.update({
|
|
210
|
+
changes: { from: 0, to: state.doc.length, insert: doc },
|
|
211
|
+
selection: EditorSelection.cursor(doc.length),
|
|
212
|
+
effects: StateEffect.appendConfig.of([
|
|
213
|
+
EditorState.readOnly.of(true),
|
|
214
|
+
EditorView.editable.of(false),
|
|
215
|
+
footnoteSource.of(sourceOf(state)),
|
|
216
|
+
previewTail,
|
|
217
|
+
]),
|
|
218
|
+
// 본문용 입력 보정(표 가장자리 보호 등)이 문서 교체를 바꾸지 않도록 합니다.
|
|
219
|
+
filter: false,
|
|
220
|
+
annotations: Transaction.addToHistory.of(false),
|
|
221
|
+
}).state;
|
|
243
222
|
}
|
|
244
223
|
/** pos(마우스 위치)에 각주 참조가 있으면 그 정의를 보여주는 툴팁 */
|
|
245
224
|
export function footnoteTooltipAt(state, pos, side = 1) {
|
|
@@ -260,15 +239,41 @@ export function footnoteTooltipAt(state, pos, side = 1) {
|
|
|
260
239
|
create() {
|
|
261
240
|
const dom = document.createElement("div");
|
|
262
241
|
dom.className = "cm-footnote-tooltip";
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
242
|
+
// 머리: 각주 라벨 배지 + "각주"
|
|
243
|
+
const header = document.createElement("div");
|
|
244
|
+
header.className = "cm-footnote-tooltip-header";
|
|
245
|
+
const badge = document.createElement("span");
|
|
246
|
+
badge.className = "cm-footnote-tooltip-badge";
|
|
247
|
+
badge.textContent = label;
|
|
248
|
+
const title = document.createElement("span");
|
|
249
|
+
title.textContent = "각주";
|
|
250
|
+
header.append(badge, title);
|
|
251
|
+
dom.appendChild(header);
|
|
252
|
+
// 참조와 툴팁 사이 간격 (위로 뜨면 위로, 아래로 뜨면 아래로 밀림)
|
|
253
|
+
const offset = { x: 0, y: 6 };
|
|
254
|
+
const content = footnoteContent(sourceOf(state), label);
|
|
255
|
+
if (content === null) {
|
|
268
256
|
dom.classList.add("cm-footnote-tooltip-missing");
|
|
269
|
-
|
|
257
|
+
const message = document.createElement("div");
|
|
258
|
+
message.className = "cm-footnote-tooltip-message";
|
|
259
|
+
message.textContent = "정의되지 않은 각주입니다.";
|
|
260
|
+
dom.appendChild(message);
|
|
261
|
+
return { dom, offset };
|
|
270
262
|
}
|
|
271
|
-
|
|
263
|
+
// 여러 줄(문단, 표, 수식 등 블록)이면 넓게, 한 줄이면 내용 너비만큼
|
|
264
|
+
if (content.includes("\n"))
|
|
265
|
+
dom.classList.add("cm-footnote-tooltip-wide");
|
|
266
|
+
const box = document.createElement("div");
|
|
267
|
+
box.className = "cm-footnote-preview";
|
|
268
|
+
dom.appendChild(box);
|
|
269
|
+
const preview = new EditorView({ state: footnotePreviewState(state, content), parent: box });
|
|
270
|
+
return {
|
|
271
|
+
dom,
|
|
272
|
+
offset,
|
|
273
|
+
// 화면에 붙은 뒤 크기를 다시 잽니다.
|
|
274
|
+
mount: () => preview.requestMeasure(),
|
|
275
|
+
destroy: () => preview.destroy(),
|
|
276
|
+
};
|
|
272
277
|
},
|
|
273
278
|
};
|
|
274
279
|
}
|
|
@@ -308,15 +313,51 @@ function firstReference(state, label) {
|
|
|
308
313
|
});
|
|
309
314
|
return found;
|
|
310
315
|
}
|
|
311
|
-
|
|
316
|
+
/** 반짝임 시간 (테마의 애니메이션 길이와 같음) */
|
|
317
|
+
const FLASH_MS = 1200;
|
|
318
|
+
/** pos로 이동합니다. flash가 있으면 그 위치에서 시작하는 정의를 잠깐 반짝입니다. */
|
|
319
|
+
function jump(view, pos, flash = -1) {
|
|
312
320
|
view.dispatch({
|
|
313
321
|
selection: EditorSelection.cursor(pos),
|
|
314
|
-
effects: EditorView.scrollIntoView(pos, { y: "center" }),
|
|
322
|
+
effects: [EditorView.scrollIntoView(pos, { y: "center" }), setFlashedFootnote.of(flash)],
|
|
315
323
|
userEvent: "select.footnote",
|
|
316
324
|
});
|
|
317
325
|
view.focus();
|
|
326
|
+
if (flash < 0)
|
|
327
|
+
return;
|
|
328
|
+
const token = (flashTokens.get(view) ?? 0) + 1;
|
|
329
|
+
flashTokens.set(view, token);
|
|
330
|
+
setTimeout(() => {
|
|
331
|
+
// 그사이 다시 이동했거나(새 반짝임) 에디터가 닫혔으면 건드리지 않습니다.
|
|
332
|
+
if (flashTokens.get(view) === token && view.dom.isConnected) {
|
|
333
|
+
view.dispatch({ effects: setFlashedFootnote.of(-1) });
|
|
334
|
+
}
|
|
335
|
+
}, FLASH_MS);
|
|
336
|
+
}
|
|
337
|
+
/** 에디터별 마지막 반짝임 번호 */
|
|
338
|
+
const flashTokens = new WeakMap();
|
|
339
|
+
/** 마우스를 올린 정의를 기록합니다. (바뀔 때만) */
|
|
340
|
+
function setHover(view, hover) {
|
|
341
|
+
if ((view.state.field(footnoteUi, false)?.hover ?? -1) !== hover) {
|
|
342
|
+
view.dispatch({ effects: setHoveredFootnote.of(hover) });
|
|
343
|
+
}
|
|
318
344
|
}
|
|
319
345
|
const footnoteEvents = EditorView.domEventHandlers({
|
|
346
|
+
mousemove(event, view) {
|
|
347
|
+
const line = event.target?.closest?.(".cm-line");
|
|
348
|
+
let hover = -1;
|
|
349
|
+
if (line?.classList.contains("cm-footnote-def-line") && view.contentDOM.contains(line)) {
|
|
350
|
+
const def = topLevelAt(view.state, view.posAtDOM(line));
|
|
351
|
+
if (def?.name === "FootnoteDefinition")
|
|
352
|
+
hover = def.from;
|
|
353
|
+
}
|
|
354
|
+
setHover(view, hover);
|
|
355
|
+
return false;
|
|
356
|
+
},
|
|
357
|
+
mouseleave(_event, view) {
|
|
358
|
+
setHover(view, -1);
|
|
359
|
+
return false;
|
|
360
|
+
},
|
|
320
361
|
mousedown(event, view) {
|
|
321
362
|
if (event.button !== 0)
|
|
322
363
|
return false;
|
|
@@ -327,6 +368,7 @@ const footnoteEvents = EditorView.domEventHandlers({
|
|
|
327
368
|
if (!el || !view.contentDOM.contains(el))
|
|
328
369
|
return false;
|
|
329
370
|
const { state } = view;
|
|
371
|
+
// (미리보기 에디터의 문서에는 정의가 없으므로 이동하지 않고 일반 클릭이 됩니다)
|
|
330
372
|
const pos = view.posAtDOM(el);
|
|
331
373
|
if (ref) {
|
|
332
374
|
// 참조 → 정의 내용 시작으로 이동
|
|
@@ -336,7 +378,7 @@ const footnoteEvents = EditorView.domEventHandlers({
|
|
|
336
378
|
if (!target)
|
|
337
379
|
return false; // 정의가 없으면 일반 클릭(커서 이동)
|
|
338
380
|
event.preventDefault();
|
|
339
|
-
jump(view, target.contentFrom);
|
|
381
|
+
jump(view, target.contentFrom, target.from);
|
|
340
382
|
return true;
|
|
341
383
|
}
|
|
342
384
|
// 정의 라벨 → 첫 참조 뒤로 이동
|
|
@@ -368,53 +410,188 @@ const footnoteTheme = EditorView.baseTheme({
|
|
|
368
410
|
color: "#dc2626 !important",
|
|
369
411
|
cursor: "text",
|
|
370
412
|
},
|
|
413
|
+
// 정의 라벨: 둥근 배지 (툴팁 머리와 같은 모양). 최소 3칸 + 간격 1칸 = 이어지는 줄의 들여쓰기 4칸이라
|
|
414
|
+
// 짧은 라벨이면 첫 줄 내용과 이어지는 줄 내용이 같은 열에서 시작합니다.
|
|
371
415
|
".cm-footnote-def-label": {
|
|
372
|
-
|
|
416
|
+
display: "inline-block",
|
|
417
|
+
boxSizing: "border-box",
|
|
418
|
+
minWidth: "3ch",
|
|
419
|
+
padding: "0 0.5ch",
|
|
420
|
+
marginRight: "1ch",
|
|
421
|
+
borderRadius: "999px",
|
|
422
|
+
textAlign: "center",
|
|
423
|
+
fontWeight: "600",
|
|
424
|
+
lineHeight: "1.45",
|
|
425
|
+
// 첫 줄에 큰 이미지·수식이 있어도 배지는 줄 위쪽에 (기준선 정렬이면 아래로 내려감)
|
|
426
|
+
verticalAlign: "top",
|
|
373
427
|
cursor: "pointer",
|
|
374
|
-
marginRight: "0.5em",
|
|
375
428
|
},
|
|
376
|
-
".cm-footnote-def-label
|
|
377
|
-
|
|
429
|
+
"&light .cm-footnote-def-label": { backgroundColor: "rgba(37, 99, 235, 0.1)", color: "#2563eb" },
|
|
430
|
+
"&dark .cm-footnote-def-label": { backgroundColor: "rgba(96, 165, 250, 0.16)", color: "#60a5fa" },
|
|
431
|
+
// 라벨이 긴 여러 줄 정의: 배지를 따로 한 줄에 두고 내용은 4칸 열에서 시작
|
|
432
|
+
".cm-footnote-def-head": { paddingLeft: "calc(6px + 4ch)" },
|
|
433
|
+
".cm-footnote-def-head .cm-footnote-def-label": {
|
|
434
|
+
display: "block",
|
|
435
|
+
width: "fit-content",
|
|
436
|
+
marginLeft: "-4ch",
|
|
437
|
+
marginRight: "0",
|
|
378
438
|
},
|
|
439
|
+
// 왼쪽 레일: 정의의 모든 줄 왼쪽 끝에 옅은 세로선 (배경 그림이라 줄 높이에 영향 없음)
|
|
440
|
+
// 인용 줄은 인용 세로줄(같은 자리)을 배경으로 그리므로 레일을 그리지 않습니다.
|
|
441
|
+
"&light .cm-footnote-def-line.cm-footnote-def-hover": { "--cm-footnote-rail": "rgba(37, 99, 235, 0.6)" },
|
|
442
|
+
"&dark .cm-footnote-def-line.cm-footnote-def-hover": { "--cm-footnote-rail": "rgba(96, 165, 250, 0.7)" },
|
|
443
|
+
// 인용 줄은 배경 그림을 인용이 그리므로, 레일을 변수로 넘겨 인용이 함께 그리게 합니다. (quote.ts)
|
|
444
|
+
".cm-footnote-def-line": {
|
|
445
|
+
"--cm-line-rail": "linear-gradient(var(--cm-footnote-rail), var(--cm-footnote-rail))",
|
|
446
|
+
"--cm-line-rail-size": "2px 100%",
|
|
447
|
+
"--cm-line-rail-pos": "0 0",
|
|
448
|
+
},
|
|
449
|
+
".cm-footnote-def-line:not(.cm-quote-line)": {
|
|
450
|
+
backgroundImage: "linear-gradient(var(--cm-footnote-rail), var(--cm-footnote-rail))",
|
|
451
|
+
backgroundRepeat: "no-repeat",
|
|
452
|
+
backgroundSize: "2px 100%",
|
|
453
|
+
backgroundPosition: "0 0",
|
|
454
|
+
},
|
|
455
|
+
// 정의마다 위 간격 (레일은 간격 아래부터 그려 이웃 정의의 레일과 떨어지게)
|
|
456
|
+
".cm-footnote-def-first:not(.cm-quote-line)": {
|
|
457
|
+
paddingTop: "0.6em",
|
|
458
|
+
backgroundSize: "2px calc(100% - 0.6em)",
|
|
459
|
+
backgroundPosition: "0 0.6em",
|
|
460
|
+
},
|
|
461
|
+
// 참조를 눌러 이동해 온 정의를 잠깐 반짝입니다. (안쪽 그림자라 콜아웃 배경 등 위에도 보임)
|
|
462
|
+
"@keyframes cm-footnote-flash": {
|
|
463
|
+
from: { boxShadow: "inset 0 0 0 9999px var(--cm-footnote-flash)" },
|
|
464
|
+
to: { boxShadow: "inset 0 0 0 9999px transparent" },
|
|
465
|
+
},
|
|
466
|
+
".cm-footnote-def-flash": { animation: `cm-footnote-flash ${FLASH_MS}ms ease-out` },
|
|
379
467
|
// 정의: 본문보다 살짝 작은 글자, 묶음 첫 줄 위에 구분선.
|
|
380
468
|
// CodeMirror는 줄 높이를 직접 재므로 줄에 margin을 쓸 수 없습니다. 그래서 위쪽 padding으로 자리를 만들고,
|
|
381
469
|
// 구분선은 배경으로 그 가운데쯤(위 본문과 간격을 두고)에 그립니다.
|
|
382
|
-
|
|
470
|
+
// 코드 블록 줄은 제외: 코드 블록은 줄 글자 크기를 기준으로 들여쓰기(ch)와 숨긴 울타리 줄(0.5em)을 계산하므로
|
|
471
|
+
// 크기를 바꾸면 상자와 글이 어긋납니다.
|
|
472
|
+
".cm-footnote-def-line:not(.cm-codeblock-line)": {
|
|
383
473
|
fontSize: "0.875em",
|
|
384
474
|
},
|
|
385
|
-
|
|
386
|
-
"&
|
|
387
|
-
|
|
475
|
+
// 글자색과 레일·구분선·반짝임 색
|
|
476
|
+
"&light .cm-footnote-def-line": {
|
|
477
|
+
color: "#4b5563",
|
|
478
|
+
"--cm-footnote-rail": "rgba(37, 99, 235, 0.22)",
|
|
479
|
+
"--cm-footnote-sep": "#e5e7eb",
|
|
480
|
+
"--cm-footnote-flash": "rgba(37, 99, 235, 0.14)",
|
|
481
|
+
},
|
|
482
|
+
"&dark .cm-footnote-def-line": {
|
|
483
|
+
color: "#9ca3af",
|
|
484
|
+
"--cm-footnote-rail": "rgba(96, 165, 250, 0.28)",
|
|
485
|
+
"--cm-footnote-sep": "#374151",
|
|
486
|
+
"--cm-footnote-flash": "rgba(96, 165, 250, 0.2)",
|
|
487
|
+
},
|
|
488
|
+
// 묶음 첫 줄: 위 구분선 + 레일 (배경 그림 두 개)
|
|
489
|
+
".cm-footnote-def-first.cm-footnote-defs-start": {
|
|
388
490
|
paddingTop: "1.25em",
|
|
491
|
+
backgroundImage: "linear-gradient(var(--cm-footnote-sep), var(--cm-footnote-sep)), linear-gradient(var(--cm-footnote-rail), var(--cm-footnote-rail))",
|
|
389
492
|
backgroundRepeat: "no-repeat",
|
|
390
|
-
backgroundSize: "100% 1px",
|
|
391
|
-
backgroundPosition: "0 0.75em",
|
|
493
|
+
backgroundSize: "100% 1px, 2px calc(100% - 1.25em)",
|
|
494
|
+
backgroundPosition: "0 0.75em, 0 1.25em",
|
|
392
495
|
},
|
|
393
496
|
// 거터의 글자 크기는 본문 기준이므로 정의 글자 크기(0.875em)를 곱한 값으로 맞춥니다.
|
|
394
497
|
".cm-gutterElement.cm-footnote-defs-start-gutter": { paddingTop: "calc(1.25em * 0.875)" },
|
|
395
|
-
"
|
|
396
|
-
|
|
397
|
-
// 툴팁
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
498
|
+
".cm-gutterElement.cm-footnote-def-first-gutter": { paddingTop: "calc(0.6em * 0.875)" },
|
|
499
|
+
// 툴팁: 둥근 카드 + 옅은 테두리 + 부드러운 그림자, 나타날 때 살짝 떠오름.
|
|
500
|
+
// hoverTooltip은 바깥 상자(.cm-tooltip-hover) 안에 각주 툴팁(.cm-tooltip-section)을 넣으므로
|
|
501
|
+
// 카드 모양은 바깥 상자에 줍니다. (CodeMirror 기본 테두리·배경을 덮어씀)
|
|
502
|
+
"@keyframes cm-footnote-tooltip-in": {
|
|
503
|
+
from: { opacity: "0", transform: "translateY(3px) scale(0.98)" },
|
|
504
|
+
to: { opacity: "1", transform: "none" },
|
|
505
|
+
},
|
|
506
|
+
".cm-tooltip.cm-tooltip-hover:has(> .cm-footnote-tooltip)": {
|
|
507
|
+
borderRadius: "10px",
|
|
508
|
+
overflow: "hidden",
|
|
509
|
+
animation: "cm-footnote-tooltip-in 120ms ease-out",
|
|
510
|
+
},
|
|
511
|
+
"&light .cm-tooltip.cm-tooltip-hover:has(> .cm-footnote-tooltip)": {
|
|
512
|
+
backgroundColor: "#ffffff",
|
|
513
|
+
color: "#1f2328",
|
|
514
|
+
border: "1px solid rgba(31, 35, 40, 0.1)",
|
|
515
|
+
boxShadow: "0 1px 2px rgba(31, 35, 40, 0.04), 0 4px 10px rgba(31, 35, 40, 0.06)",
|
|
516
|
+
},
|
|
517
|
+
"&dark .cm-tooltip.cm-tooltip-hover:has(> .cm-footnote-tooltip)": {
|
|
518
|
+
backgroundColor: "#25262b",
|
|
519
|
+
color: "#e6e7ea",
|
|
520
|
+
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
521
|
+
boxShadow: "0 4px 10px rgba(0, 0, 0, 0.25)",
|
|
522
|
+
},
|
|
523
|
+
"@media (prefers-reduced-motion: reduce)": {
|
|
524
|
+
".cm-tooltip.cm-tooltip-hover:has(> .cm-footnote-tooltip)": { animation: "none" },
|
|
525
|
+
},
|
|
526
|
+
// 왼쪽 여백은 미리보기 줄의 기본 왼쪽 여백(6px)만큼 줄이고, 머리를 그만큼 들여 내용과 맞춥니다.
|
|
527
|
+
".cm-footnote-tooltip": {
|
|
528
|
+
boxSizing: "border-box",
|
|
529
|
+
padding: "10px 12px 10px 8px",
|
|
530
|
+
maxWidth: "min(480px, calc(100vw - 32px))",
|
|
531
|
+
fontSize: "0.9em",
|
|
532
|
+
lineHeight: "1.6",
|
|
403
533
|
whiteSpace: "normal",
|
|
534
|
+
// 한글은 낱말(띄어쓰기) 단위로 줄바꿈하고, 긴 주소처럼 한 낱말이 넘치면 그때만 끊습니다.
|
|
535
|
+
wordBreak: "keep-all",
|
|
536
|
+
overflowWrap: "anywhere",
|
|
404
537
|
},
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
borderRadius: "3px",
|
|
409
|
-
padding: "0 3px",
|
|
538
|
+
// 다른 확장의 hover 툴팁과 함께 뜨면 구분선
|
|
539
|
+
".cm-tooltip-section.cm-footnote-tooltip:not(:first-child)": {
|
|
540
|
+
borderTop: "1px solid rgba(127, 127, 127, 0.2)",
|
|
410
541
|
},
|
|
411
|
-
".cm-footnote-tooltip
|
|
412
|
-
|
|
413
|
-
|
|
542
|
+
".cm-footnote-tooltip-wide": { width: "min(480px, calc(100vw - 32px))" },
|
|
543
|
+
".cm-footnote-tooltip-message": { paddingLeft: "6px" },
|
|
544
|
+
// 미리보기 에디터: 테두리·배경·줄 번호·현재 줄 강조 없이 내용만. 길면 툴팁 안에서 스크롤.
|
|
545
|
+
".cm-footnote-preview > .cm-editor": {
|
|
546
|
+
maxHeight: "360px",
|
|
547
|
+
minHeight: "0",
|
|
548
|
+
height: "auto",
|
|
549
|
+
border: "none",
|
|
550
|
+
backgroundColor: "transparent",
|
|
414
551
|
},
|
|
415
|
-
".cm-footnote-
|
|
552
|
+
".cm-footnote-preview > .cm-editor.cm-focused": { outline: "none" },
|
|
553
|
+
".cm-footnote-preview .cm-scroller": { overflow: "auto" },
|
|
554
|
+
".cm-footnote-preview .cm-content": { padding: "0", minHeight: "0" },
|
|
555
|
+
".cm-footnote-preview .cm-gutters": { display: "none" },
|
|
556
|
+
".cm-footnote-preview .cm-line.cm-activeLine": { backgroundColor: "transparent" },
|
|
557
|
+
// 커서를 두는 마지막 빈 줄은 숨깁니다.
|
|
558
|
+
".cm-footnote-preview .cm-line.cm-footnote-preview-tail": {
|
|
559
|
+
height: "0",
|
|
560
|
+
padding: "0",
|
|
561
|
+
overflow: "hidden",
|
|
562
|
+
},
|
|
563
|
+
// 미리보기에서는 표를 고칠 수 없으므로 행/열 손잡이를 숨깁니다.
|
|
564
|
+
".cm-footnote-preview .cm-table-handles": { display: "none" },
|
|
565
|
+
".cm-footnote-tooltip-header": {
|
|
566
|
+
display: "flex",
|
|
567
|
+
paddingLeft: "6px",
|
|
568
|
+
alignItems: "center",
|
|
569
|
+
gap: "6px",
|
|
570
|
+
marginBottom: "6px",
|
|
571
|
+
fontSize: "0.8em",
|
|
572
|
+
fontWeight: "600",
|
|
573
|
+
letterSpacing: "0.02em",
|
|
574
|
+
},
|
|
575
|
+
"&light .cm-footnote-tooltip-header": { color: "#6b7280" },
|
|
576
|
+
"&dark .cm-footnote-tooltip-header": { color: "#9ca3af" },
|
|
577
|
+
".cm-footnote-tooltip-badge": {
|
|
578
|
+
minWidth: "1.5em",
|
|
579
|
+
padding: "0 6px",
|
|
580
|
+
borderRadius: "999px",
|
|
581
|
+
textAlign: "center",
|
|
582
|
+
lineHeight: "1.6",
|
|
583
|
+
},
|
|
584
|
+
"&light .cm-footnote-tooltip-badge": { backgroundColor: "rgba(37, 99, 235, 0.1)", color: "#2563eb" },
|
|
585
|
+
"&dark .cm-footnote-tooltip-badge": { backgroundColor: "rgba(96, 165, 250, 0.16)", color: "#60a5fa" },
|
|
586
|
+
// 정의 없음: 배지와 안내를 빨강 계열로
|
|
587
|
+
"&light .cm-footnote-tooltip-missing .cm-footnote-tooltip-badge": {
|
|
588
|
+
backgroundColor: "rgba(220, 38, 38, 0.1)",
|
|
416
589
|
color: "#dc2626",
|
|
417
590
|
},
|
|
591
|
+
"&dark .cm-footnote-tooltip-missing .cm-footnote-tooltip-badge": {
|
|
592
|
+
backgroundColor: "rgba(248, 113, 113, 0.16)",
|
|
593
|
+
color: "#f87171",
|
|
594
|
+
},
|
|
418
595
|
});
|
|
419
596
|
/**
|
|
420
597
|
* 각주(`[^label]` 참조, `[^label]: 내용` 정의)를 하이브리드 방식으로 보여주는 확장입니다.
|
|
@@ -431,6 +608,10 @@ const footnoteTheme = EditorView.baseTheme({
|
|
|
431
608
|
const defsStartGutterMarker = new (class extends GutterMarker {
|
|
432
609
|
elementClass = "cm-footnote-defs-start-gutter";
|
|
433
610
|
})();
|
|
611
|
+
/** 묶음 안의 다른 정의 첫 줄 (위 간격만큼 줄 번호를 내림) */
|
|
612
|
+
const defFirstGutterMarker = new (class extends GutterMarker {
|
|
613
|
+
elementClass = "cm-footnote-def-first-gutter";
|
|
614
|
+
})();
|
|
434
615
|
/** 정의 묶음 첫 줄들 (정의는 문서 최상위에만 있으므로 최상위 블록만 훑습니다) */
|
|
435
616
|
function defsStartLines(state) {
|
|
436
617
|
const ranges = [];
|
|
@@ -439,8 +620,8 @@ function defsStartLines(state) {
|
|
|
439
620
|
if (cursor.firstChild()) {
|
|
440
621
|
do {
|
|
441
622
|
const isDef = cursor.type.is("FootnoteDefinition");
|
|
442
|
-
if (isDef
|
|
443
|
-
ranges.push(defsStartGutterMarker.range(state.doc.lineAt(cursor.from).from));
|
|
623
|
+
if (isDef)
|
|
624
|
+
ranges.push((prevIsDef ? defFirstGutterMarker : defsStartGutterMarker).range(state.doc.lineAt(cursor.from).from));
|
|
444
625
|
prevIsDef = isDef;
|
|
445
626
|
} while (cursor.nextSibling());
|
|
446
627
|
}
|
|
@@ -451,6 +632,120 @@ const defsStartGutter = StateField.define({
|
|
|
451
632
|
update: (value, tr) => tr.docChanged || syntaxTree(tr.state) !== syntaxTree(tr.startState) ? defsStartLines(tr.state) : value,
|
|
452
633
|
provide: (field) => gutterLineClass.from(field),
|
|
453
634
|
});
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
// Enter: 각주 안에서 새 줄은 4칸 들여쓰기를 유지합니다.
|
|
637
|
+
// ---------------------------------------------------------------------------
|
|
638
|
+
/** 최상위 블록 (pos가 있는 곳) */
|
|
639
|
+
function topLevelAt(state, pos) {
|
|
640
|
+
let node = syntaxTree(state).resolveInner(pos, 1);
|
|
641
|
+
while (node && node.parent && node.parent.name !== "Document")
|
|
642
|
+
node = node.parent;
|
|
643
|
+
return node && node.name !== "Document" ? node : null;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* 줄이 각주 정의 내용 안이면 그 정의 노드.
|
|
647
|
+
* 정의 범위 안의 줄, 또는 정의 바로 뒤에 이어지는 들여쓴 빈 줄(공백만 있는 줄)이 해당합니다.
|
|
648
|
+
*/
|
|
649
|
+
export function footnoteAtLine(state, lineNumber) {
|
|
650
|
+
const { doc } = state;
|
|
651
|
+
const line = doc.line(lineNumber);
|
|
652
|
+
let n = lineNumber;
|
|
653
|
+
if (!line.text.trim()) {
|
|
654
|
+
while (n > 1 && !doc.line(n).text.trim())
|
|
655
|
+
n--;
|
|
656
|
+
if (!doc.line(n).text.trim())
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
const probe = doc.line(n);
|
|
660
|
+
const node = topLevelAt(state, probe.from + (probe.text.length - probe.text.trimStart().length));
|
|
661
|
+
if (!node || node.name !== "FootnoteDefinition")
|
|
662
|
+
return null;
|
|
663
|
+
if (line.from <= node.to)
|
|
664
|
+
return node;
|
|
665
|
+
// 정의 뒤의 빈 줄: 공백(들여쓰기)이 있을 때만 정의를 이어 쓰는 중으로 봅니다.
|
|
666
|
+
return line.text.length > 0 ? node : null;
|
|
667
|
+
}
|
|
668
|
+
const indentWidth = (text) => {
|
|
669
|
+
let width = 0;
|
|
670
|
+
for (const ch of text) {
|
|
671
|
+
if (ch === " ")
|
|
672
|
+
width++;
|
|
673
|
+
else if (ch === "\t")
|
|
674
|
+
width += 4 - (width % 4);
|
|
675
|
+
else
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
return width;
|
|
679
|
+
};
|
|
680
|
+
/**
|
|
681
|
+
* 각주 안에서 Enter로 만든 새 줄의 들여쓰기가 4칸보다 적으면 4칸으로 채웁니다.
|
|
682
|
+
* (목록 이어쓰기, 목록 끝내기, 일반 줄바꿈 등 어떤 Enter 명령이든)
|
|
683
|
+
*/
|
|
684
|
+
const keepFootnoteIndent = EditorState.transactionFilter.of((tr) => {
|
|
685
|
+
if (!tr.docChanged || tr.annotation(Transaction.userEvent) !== "input")
|
|
686
|
+
return tr;
|
|
687
|
+
const start = tr.startState;
|
|
688
|
+
if (start.selection.ranges.length !== 1 || !start.selection.main.empty || tr.newSelection.ranges.length !== 1) {
|
|
689
|
+
return tr;
|
|
690
|
+
}
|
|
691
|
+
let lineBreak = false;
|
|
692
|
+
tr.changes.iterChanges((_fa, _ta, _fb, _tb, text) => {
|
|
693
|
+
if (text.lines > 1)
|
|
694
|
+
lineBreak = true;
|
|
695
|
+
});
|
|
696
|
+
if (!lineBreak)
|
|
697
|
+
return tr;
|
|
698
|
+
const pos = start.selection.main.head;
|
|
699
|
+
const oldLine = start.doc.lineAt(pos);
|
|
700
|
+
const def = footnoteAtLine(start, oldLine.number);
|
|
701
|
+
if (!def)
|
|
702
|
+
return tr;
|
|
703
|
+
// 정의 첫 줄의 `[^label]:` 앞이나 안에서 누른 Enter(정의를 쪼갬)는 그대로 둡니다.
|
|
704
|
+
const marks = def.getChildren("FootnoteMark");
|
|
705
|
+
if (pos < (marks[1]?.to ?? def.to))
|
|
706
|
+
return tr;
|
|
707
|
+
const head = tr.newSelection.main.head;
|
|
708
|
+
const line = tr.newDoc.lineAt(head);
|
|
709
|
+
if (line.from <= tr.changes.mapPos(oldLine.from))
|
|
710
|
+
return tr; // 새 줄로 가지 않음
|
|
711
|
+
const width = indentWidth(line.text);
|
|
712
|
+
if (width >= FOOTNOTE_INDENT)
|
|
713
|
+
return tr;
|
|
714
|
+
const add = " ".repeat(FOOTNOTE_INDENT - width);
|
|
715
|
+
return [tr, { changes: { from: line.from, insert: add }, selection: { anchor: head + add.length }, sequential: true }];
|
|
716
|
+
});
|
|
717
|
+
/**
|
|
718
|
+
* 각주 안의 들여쓴 빈 줄이 빈 줄 바로 다음이면, Enter로 들여쓰기를 지워 각주 밖으로 나갑니다.
|
|
719
|
+
* (인용에서 빈 줄 두 번으로 나가는 것과 같음)
|
|
720
|
+
*/
|
|
721
|
+
export const exitFootnote = ({ state, dispatch }) => {
|
|
722
|
+
const { selection, doc } = state;
|
|
723
|
+
if (selection.ranges.length !== 1 || !selection.main.empty)
|
|
724
|
+
return false;
|
|
725
|
+
const line = doc.lineAt(selection.main.head);
|
|
726
|
+
if (!line.text.length || line.text.trim() || line.number === 1)
|
|
727
|
+
return false;
|
|
728
|
+
if (doc.line(line.number - 1).text.trim())
|
|
729
|
+
return false;
|
|
730
|
+
if (!footnoteAtLine(state, line.number))
|
|
731
|
+
return false;
|
|
732
|
+
dispatch(state.update({
|
|
733
|
+
changes: { from: line.from, to: line.to },
|
|
734
|
+
selection: { anchor: line.from },
|
|
735
|
+
scrollIntoView: true,
|
|
736
|
+
userEvent: "delete",
|
|
737
|
+
}));
|
|
738
|
+
return true;
|
|
739
|
+
};
|
|
454
740
|
export function footnote() {
|
|
455
|
-
return [
|
|
741
|
+
return [
|
|
742
|
+
footnoteUi,
|
|
743
|
+
footnotePlugin,
|
|
744
|
+
defsStartGutter,
|
|
745
|
+
footnoteEvents,
|
|
746
|
+
footnoteHover,
|
|
747
|
+
footnoteTheme,
|
|
748
|
+
keepFootnoteIndent,
|
|
749
|
+
Prec.high(keymap.of([{ key: "Enter", run: exitFootnote }])),
|
|
750
|
+
];
|
|
456
751
|
}
|