mdsquare 0.0.0-stage → 0.1.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 +206 -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 +114 -0
- package/dist/editor.js +199 -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 +66 -0
- package/dist/extensions.js +79 -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/image.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { syntaxTree } from "@codemirror/language";
|
|
2
|
+
import { Decoration, EditorView, WidgetType } from "@codemirror/view";
|
|
3
|
+
import { createRevealField, createRevealPlugin } from "./utils/reveal.js";
|
|
4
|
+
const SIZE_RE = /^(?:(\d{1,4})(%)?)?(?:x(\d{1,4}))?$/i;
|
|
5
|
+
const ALIGN_RE = /^(left|center|right)$/i;
|
|
6
|
+
/**
|
|
7
|
+
* 옵션 구분자 위치들: [구분자 시작, 구분자 뒤]. 이스케이프되지 않은 "|".
|
|
8
|
+
* escapedPipes가 true면 `\|`도 구분자입니다. (표 칸 안에서는 "|"가 칸을 나누므로 `\|`로 써야 함. Obsidian과 같음)
|
|
9
|
+
*/
|
|
10
|
+
function pipePositions(raw, escapedPipes) {
|
|
11
|
+
const out = [];
|
|
12
|
+
for (let i = 0; i < raw.length; i++) {
|
|
13
|
+
if (raw[i] === "\\") {
|
|
14
|
+
if (escapedPipes && raw[i + 1] === "|")
|
|
15
|
+
out.push([i, i + 2]);
|
|
16
|
+
i++;
|
|
17
|
+
}
|
|
18
|
+
else if (raw[i] === "|")
|
|
19
|
+
out.push([i, i + 1]);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
const unescape = (text) => text.replace(/\\([!-/:-@[-`{-~])/g, "$1");
|
|
24
|
+
/**
|
|
25
|
+
* 이미지 대체 텍스트 `caption|size|align`을 해석합니다.
|
|
26
|
+
* - 뒤에서부터 크기(`300`, `300x200`, `x200`, `50%`)나 정렬(`left|center|right`) 형식인 조각만
|
|
27
|
+
* 옵션으로 인식하고, 나머지는 모두 캡션입니다. (캡션에 "|"가 있어도 깨지지 않음, `\|`로 이스케이프 가능)
|
|
28
|
+
* - inTable: 표 칸 안의 이미지. `\|`도 구분자로 봅니다. (``)
|
|
29
|
+
* - 크기와 정렬의 순서는 바뀌어도 되고 각각 생략할 수 있습니다.
|
|
30
|
+
*/
|
|
31
|
+
export function parseImageAlt(raw, inTable = false) {
|
|
32
|
+
const pipes = pipePositions(raw, inTable);
|
|
33
|
+
let width = null;
|
|
34
|
+
let height = null;
|
|
35
|
+
let align = null;
|
|
36
|
+
let end = raw.length;
|
|
37
|
+
for (let i = pipes.length - 1; i >= 0; i--) {
|
|
38
|
+
const part = raw.slice(pipes[i][1], end).trim();
|
|
39
|
+
const alignMatch = align === null ? ALIGN_RE.exec(part) : null;
|
|
40
|
+
const sizeMatch = width === null && height === null && part ? SIZE_RE.exec(part) : null;
|
|
41
|
+
if (alignMatch) {
|
|
42
|
+
align = alignMatch[1].toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
else if (sizeMatch && (sizeMatch[1] || sizeMatch[3])) {
|
|
45
|
+
if (sizeMatch[1])
|
|
46
|
+
width = `${Number(sizeMatch[1])}${sizeMatch[2] ? "%" : "px"}`;
|
|
47
|
+
if (sizeMatch[3])
|
|
48
|
+
height = `${Number(sizeMatch[3])}px`;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
end = pipes[i][0];
|
|
54
|
+
}
|
|
55
|
+
return { caption: unescape(raw.slice(0, end).trim()), width, height, align, captionLength: end };
|
|
56
|
+
}
|
|
57
|
+
function inTableCell(node) {
|
|
58
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
59
|
+
if (parent.name === "TableCell")
|
|
60
|
+
return true;
|
|
61
|
+
if (parent.name === "Table" || parent.name === "Paragraph")
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
/** Image 노드에서 `` 정보를 읽습니다. 참조 이미지(`![a][ref]`)는 null */
|
|
67
|
+
export function readImage(state, node) {
|
|
68
|
+
if (node.name !== "Image")
|
|
69
|
+
return null;
|
|
70
|
+
const marks = [];
|
|
71
|
+
let url = null;
|
|
72
|
+
let titleNode = null;
|
|
73
|
+
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
74
|
+
if (child.name === "LinkMark")
|
|
75
|
+
marks.push(child);
|
|
76
|
+
else if (child.name === "URL")
|
|
77
|
+
url = child;
|
|
78
|
+
else if (child.name === "LinkTitle")
|
|
79
|
+
titleNode = child;
|
|
80
|
+
}
|
|
81
|
+
if (marks.length !== 4)
|
|
82
|
+
return null;
|
|
83
|
+
const [open, close, paren, end] = marks;
|
|
84
|
+
const { doc } = state;
|
|
85
|
+
if (doc.sliceString(open.from, open.to) !== "![" || doc.sliceString(paren.from, paren.to) !== "(") {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
let src = url ? doc.sliceString(url.from, url.to) : "";
|
|
89
|
+
if (src.startsWith("<") && src.endsWith(">"))
|
|
90
|
+
src = src.slice(1, -1);
|
|
91
|
+
const rawTitle = titleNode ? doc.sliceString(titleNode.from, titleNode.to) : null;
|
|
92
|
+
// 최상위 문단의 직접 자식이고 줄에 이미지만 있으면(앞뒤 공백 제외) 블록 이미지
|
|
93
|
+
// (목록/인용 안이나 다른 서식 안의 이미지는 인라인 이미지)
|
|
94
|
+
const line = doc.lineAt(node.from);
|
|
95
|
+
const standalone = node.parent?.name === "Paragraph" &&
|
|
96
|
+
node.parent.parent?.name === "Document" &&
|
|
97
|
+
node.to <= line.to &&
|
|
98
|
+
!line.text.slice(0, node.from - line.from).trim() &&
|
|
99
|
+
!line.text.slice(node.to - line.from).trim();
|
|
100
|
+
return {
|
|
101
|
+
...parseImageAlt(doc.sliceString(open.to, close.from), inTableCell(node)),
|
|
102
|
+
from: node.from,
|
|
103
|
+
to: node.to,
|
|
104
|
+
altFrom: open.to,
|
|
105
|
+
altTo: close.from,
|
|
106
|
+
src: unescape(src),
|
|
107
|
+
title: rawTitle === null ? null : unescape(rawTitle.slice(1, -1)),
|
|
108
|
+
line: standalone ? { from: line.from, to: line.to } : null,
|
|
109
|
+
parts: { open, close, paren, end, url, title: titleNode },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
class ImageWidget extends WidgetType {
|
|
113
|
+
spec;
|
|
114
|
+
constructor(spec) {
|
|
115
|
+
super();
|
|
116
|
+
this.spec = spec;
|
|
117
|
+
}
|
|
118
|
+
eq(other) {
|
|
119
|
+
const a = this.spec;
|
|
120
|
+
const b = other.spec;
|
|
121
|
+
return (a.src === b.src &&
|
|
122
|
+
a.caption === b.caption &&
|
|
123
|
+
a.title === b.title &&
|
|
124
|
+
a.width === b.width &&
|
|
125
|
+
a.height === b.height &&
|
|
126
|
+
a.align === b.align &&
|
|
127
|
+
a.block === b.block);
|
|
128
|
+
}
|
|
129
|
+
toDOM(view) {
|
|
130
|
+
const { src, caption, title, width, height, align, block } = this.spec;
|
|
131
|
+
const img = document.createElement("img");
|
|
132
|
+
img.className = block ? "cm-image" : "cm-image cm-image-inline";
|
|
133
|
+
img.src = src;
|
|
134
|
+
img.alt = caption;
|
|
135
|
+
if (title)
|
|
136
|
+
img.title = title;
|
|
137
|
+
if (width)
|
|
138
|
+
img.style.width = width;
|
|
139
|
+
if (height)
|
|
140
|
+
img.style.height = height;
|
|
141
|
+
// 이미지가 로드되면 높이가 바뀌므로 에디터가 레이아웃(높이 맵)을 다시 측정하게 합니다.
|
|
142
|
+
img.addEventListener("load", () => view.requestMeasure());
|
|
143
|
+
img.addEventListener("error", () => {
|
|
144
|
+
img.classList.add("cm-image-error");
|
|
145
|
+
view.requestMeasure();
|
|
146
|
+
});
|
|
147
|
+
if (!block)
|
|
148
|
+
return img;
|
|
149
|
+
const figure = document.createElement("figure");
|
|
150
|
+
figure.className = `cm-image-block cm-image-align-${align ?? "left"}`;
|
|
151
|
+
figure.appendChild(img);
|
|
152
|
+
if (caption) {
|
|
153
|
+
const figcaption = figure.appendChild(document.createElement("figcaption"));
|
|
154
|
+
figcaption.textContent = caption;
|
|
155
|
+
}
|
|
156
|
+
return figure;
|
|
157
|
+
}
|
|
158
|
+
/** 높이를 알면 로드 전에도 레이아웃이 크게 흔들리지 않도록 알려 줍니다. */
|
|
159
|
+
get estimatedHeight() {
|
|
160
|
+
return this.spec.block && this.spec.height ? parseInt(this.spec.height) : -1;
|
|
161
|
+
}
|
|
162
|
+
// 클릭을 에디터가 처리해 커서가 이미지로 이동하고 원문이 펼쳐지도록 합니다.
|
|
163
|
+
ignoreEvent() {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// 데코레이션
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
const imageMark = Decoration.mark({ class: "cm-formatting cm-formatting-image" });
|
|
171
|
+
const captionDeco = Decoration.mark({ class: "cm-image-caption" });
|
|
172
|
+
const optionDeco = Decoration.mark({ class: "cm-formatting cm-image-option" });
|
|
173
|
+
const urlMark = Decoration.mark({ class: "cm-formatting cm-formatting-url cm-url" });
|
|
174
|
+
const urlDeco = Decoration.mark({ class: "cm-url" });
|
|
175
|
+
const titleDeco = Decoration.mark({ class: "cm-url cm-link-title" });
|
|
176
|
+
function widgetFor(image, resolveUrl) {
|
|
177
|
+
const block = image.line !== null;
|
|
178
|
+
return new ImageWidget({
|
|
179
|
+
src: resolveUrl(image.src),
|
|
180
|
+
caption: image.caption,
|
|
181
|
+
title: image.title,
|
|
182
|
+
width: image.width,
|
|
183
|
+
height: image.height,
|
|
184
|
+
align: block ? image.align : null,
|
|
185
|
+
block,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/** 커서가 닿은 이미지의 원문을 부분별로 스타일링 (문서 순서대로 추가) */
|
|
189
|
+
function pushSourceMarks(out, image) {
|
|
190
|
+
const { open, close, paren, end, url, title } = image.parts;
|
|
191
|
+
out.push(imageMark.range(open.from, open.to));
|
|
192
|
+
const optionsFrom = image.altFrom + image.captionLength;
|
|
193
|
+
if (image.altFrom < optionsFrom)
|
|
194
|
+
out.push(captionDeco.range(image.altFrom, optionsFrom));
|
|
195
|
+
if (optionsFrom < image.altTo)
|
|
196
|
+
out.push(optionDeco.range(optionsFrom, image.altTo));
|
|
197
|
+
out.push(imageMark.range(close.from, close.to));
|
|
198
|
+
out.push(urlMark.range(paren.from, paren.to));
|
|
199
|
+
if (url && url.from < url.to)
|
|
200
|
+
out.push(urlDeco.range(url.from, url.to));
|
|
201
|
+
if (title)
|
|
202
|
+
out.push(titleDeco.range(title.from, title.to));
|
|
203
|
+
out.push(urlMark.range(end.from, end.to));
|
|
204
|
+
}
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// 인라인 이미지 (글 사이, 목록/인용/제목 안): 보이는 범위만 처리하는 ViewPlugin
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
/** 이미지가 올 수 없으므로 자식 순회를 생략하는 노드 */
|
|
209
|
+
const SKIP_CHILDREN = new Set([
|
|
210
|
+
"FencedCode",
|
|
211
|
+
"CodeBlock",
|
|
212
|
+
"HTMLBlock",
|
|
213
|
+
"CommentBlock",
|
|
214
|
+
"InlineCode",
|
|
215
|
+
"URL",
|
|
216
|
+
"Autolink",
|
|
217
|
+
"LinkLabel",
|
|
218
|
+
"LinkTitle",
|
|
219
|
+
]);
|
|
220
|
+
function inlineImagePlugin(resolveUrl) {
|
|
221
|
+
return createRevealPlugin({
|
|
222
|
+
// 이미지마다 문서 순서대로(대체 또는 기호 → 캡션 → 옵션 → URL) 추가되므로 정렬이 필요 없습니다.
|
|
223
|
+
visit({ state, decorations, isRevealed }) {
|
|
224
|
+
return {
|
|
225
|
+
enter(node) {
|
|
226
|
+
if (node.name === "Image") {
|
|
227
|
+
const image = readImage(state, node.node);
|
|
228
|
+
// 블록 이미지는 blockImageField가 담당합니다.
|
|
229
|
+
if (!image || image.line)
|
|
230
|
+
return false;
|
|
231
|
+
if (isRevealed(image.from, image.to)) {
|
|
232
|
+
pushSourceMarks(decorations, image);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
decorations.push(Decoration.replace({ widget: widgetFor(image, resolveUrl) }).range(image.from, image.to));
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
if (SKIP_CHILDREN.has(node.name))
|
|
240
|
+
return false;
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// 블록 이미지 (최상위 문단에서 이미지만 있는 줄): StateField
|
|
248
|
+
// 블록 위젯은 ViewPlugin으로 제공할 수 없으므로 StateField에서 문서 전체를 다룹니다.
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
/**
|
|
251
|
+
* 블록 이미지 후보만 찾습니다. 문서 전체 트리를 훑지 않고,
|
|
252
|
+
* 최상위 블록 중 문단의 직접 자식만 확인합니다. (블록 이미지는 그 위치에만 올 수 있음)
|
|
253
|
+
*/
|
|
254
|
+
function scanBlockImages(state) {
|
|
255
|
+
const images = [];
|
|
256
|
+
const cursor = syntaxTree(state).cursor();
|
|
257
|
+
if (!cursor.firstChild())
|
|
258
|
+
return images; // Document의 자식(최상위 블록)
|
|
259
|
+
do {
|
|
260
|
+
if (!cursor.type.is("Paragraph") || !cursor.firstChild())
|
|
261
|
+
continue;
|
|
262
|
+
do {
|
|
263
|
+
if (cursor.type.is("Image")) {
|
|
264
|
+
const image = readImage(state, cursor.node);
|
|
265
|
+
if (image?.line)
|
|
266
|
+
images.push(image);
|
|
267
|
+
}
|
|
268
|
+
} while (cursor.nextSibling());
|
|
269
|
+
cursor.parent();
|
|
270
|
+
} while (cursor.nextSibling());
|
|
271
|
+
return images;
|
|
272
|
+
}
|
|
273
|
+
function buildBlockDecorations(images, revealed, resolveUrl) {
|
|
274
|
+
const out = [];
|
|
275
|
+
images.forEach((image, i) => {
|
|
276
|
+
const widget = widgetFor(image, resolveUrl);
|
|
277
|
+
const line = image.line;
|
|
278
|
+
if (revealed[i]) {
|
|
279
|
+
// 원문을 보여주고, 원문 아래에 미리보기를 유지합니다. (줄 끝의 블록 위젯)
|
|
280
|
+
pushSourceMarks(out, image);
|
|
281
|
+
out.push(Decoration.widget({ widget, block: true, side: 1 }).range(line.to));
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
// 줄 전체를 블록 이미지로 대체 (줄 중간의 블록 위젯은 줄을 둘로 쪼개므로 줄 단위로만 사용)
|
|
285
|
+
out.push(Decoration.replace({ widget, block: true }).range(line.from, line.to));
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
// 이미지가 문서 순서이고, 이미지마다 기호 → 줄 끝 위젯 순서로 추가되므로 정렬이 필요 없습니다.
|
|
289
|
+
return Decoration.set(out);
|
|
290
|
+
}
|
|
291
|
+
function blockImageField(resolveUrl) {
|
|
292
|
+
return createRevealField({
|
|
293
|
+
scan: scanBlockImages,
|
|
294
|
+
build: (images, revealed) => buildBlockDecorations(images, revealed, resolveUrl),
|
|
295
|
+
block: () => true,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// 테마
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
const imageTheme = EditorView.baseTheme({
|
|
302
|
+
".cm-image": {
|
|
303
|
+
maxWidth: "100%",
|
|
304
|
+
},
|
|
305
|
+
".cm-image-inline": {
|
|
306
|
+
verticalAlign: "text-bottom",
|
|
307
|
+
},
|
|
308
|
+
// 블록 위젯에는 세로 margin을 쓰면 높이 계산이 어긋나므로 padding을 씁니다.
|
|
309
|
+
".cm-image-block": {
|
|
310
|
+
display: "block",
|
|
311
|
+
margin: "0",
|
|
312
|
+
padding: "4px 0",
|
|
313
|
+
},
|
|
314
|
+
".cm-image-block img": {
|
|
315
|
+
display: "block",
|
|
316
|
+
},
|
|
317
|
+
".cm-image-align-center img": { marginLeft: "auto", marginRight: "auto" },
|
|
318
|
+
".cm-image-align-right img": { marginLeft: "auto" },
|
|
319
|
+
".cm-image-block figcaption": {
|
|
320
|
+
fontSize: "0.85em",
|
|
321
|
+
color: "#6b7280",
|
|
322
|
+
paddingTop: "2px",
|
|
323
|
+
},
|
|
324
|
+
".cm-image-align-center figcaption": { textAlign: "center" },
|
|
325
|
+
".cm-image-align-right figcaption": { textAlign: "right" },
|
|
326
|
+
".cm-image-error": {
|
|
327
|
+
minWidth: "1em",
|
|
328
|
+
minHeight: "1em",
|
|
329
|
+
outline: "1px dashed #d1d5db",
|
|
330
|
+
},
|
|
331
|
+
".cm-image-option": {
|
|
332
|
+
opacity: "0.6",
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
/**
|
|
336
|
+
* 이미지 ``를 하이브리드 방식으로 보여주는 확장입니다.
|
|
337
|
+
* - 이미지만 있는 줄은 블록 이미지(캡션/정렬 포함)로, 글 사이의 이미지는 인라인 이미지로 보여줍니다.
|
|
338
|
+
* - 커서가 이미지에 닿으면 원문을 보여주고, 이미지만 있는 줄은 원문 아래에 미리보기를 유지합니다.
|
|
339
|
+
* - 대체 텍스트의 `|size|align` 옵션: 크기 `300`, `300x200`, `x200`, `50%` / 정렬 `left|center|right`
|
|
340
|
+
*/
|
|
341
|
+
export function image(options = {}) {
|
|
342
|
+
const resolveUrl = options.resolveUrl ?? ((src) => src);
|
|
343
|
+
return [blockImageField(resolveUrl), inlineImagePlugin(resolveUrl), imageTheme];
|
|
344
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mdsquare: 하이브리드 마크다운 에디터 (CodeMirror 6 기반)
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import MDSquareEditor from "mdsquare";
|
|
6
|
+
* import "mdsquare/style.css";
|
|
7
|
+
*
|
|
8
|
+
* const editor = new MDSquareEditor({ parent: document.getElementById("editor")!, value: "# 안녕하세요" });
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* CodeMirror를 직접 구성하려면 확장들을 쓰세요: `import { table, math } from "mdsquare/extensions"` (이 진입점에서도 가져올 수 있음)
|
|
12
|
+
*/
|
|
13
|
+
import { MDSquareEditor } from "./editor.js";
|
|
14
|
+
export default MDSquareEditor;
|
|
15
|
+
export { MDSquareEditor, markdownExtensions, type MDSquareEditorOptions, type MDSquareFeatures, type MDSquareTheme, } from "./editor.js";
|
|
16
|
+
export * from "./extensions.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mdsquare: 하이브리드 마크다운 에디터 (CodeMirror 6 기반)
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import MDSquareEditor from "mdsquare";
|
|
6
|
+
* import "mdsquare/style.css";
|
|
7
|
+
*
|
|
8
|
+
* const editor = new MDSquareEditor({ parent: document.getElementById("editor")!, value: "# 안녕하세요" });
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* CodeMirror를 직접 구성하려면 확장들을 쓰세요: `import { table, math } from "mdsquare/extensions"` (이 진입점에서도 가져올 수 있음)
|
|
12
|
+
*/
|
|
13
|
+
import { MDSquareEditor } from "./editor.js";
|
|
14
|
+
export default MDSquareEditor;
|
|
15
|
+
export { MDSquareEditor, markdownExtensions, } from "./editor.js";
|
|
16
|
+
export * from "./extensions.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Extension } from "@codemirror/state";
|
|
2
|
+
export declare const inlineCodePlugin: import("@codemirror/view").ViewPlugin<import("./extensions.js").RevealPluginValue, undefined>;
|
|
3
|
+
/** 인라인 코드(`` `code` ``)의 백틱을 커서가 닿아 있을 때만 보여주는 확장입니다. */
|
|
4
|
+
export declare function inlineCode(): Extension;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Decoration, EditorView } from "@codemirror/view";
|
|
2
|
+
import { createRevealPlugin } from "./utils/reveal.js";
|
|
3
|
+
// 데코레이션 객체는 모듈 로드 시 한 번만 만들고 재사용합니다.
|
|
4
|
+
const content = Decoration.mark({ class: "cm-inline-code" });
|
|
5
|
+
const revealedMark = Decoration.mark({
|
|
6
|
+
class: "cm-formatting cm-formatting-inline-code cm-inline-code",
|
|
7
|
+
});
|
|
8
|
+
const hiddenMark = Decoration.replace({});
|
|
9
|
+
/** 인라인 코드가 올 수 없으므로 자식 순회를 생략하는 노드 */
|
|
10
|
+
const SKIP_CHILDREN = new Set([
|
|
11
|
+
"FencedCode",
|
|
12
|
+
"CodeBlock",
|
|
13
|
+
"HTMLBlock",
|
|
14
|
+
"CommentBlock",
|
|
15
|
+
"URL",
|
|
16
|
+
"Autolink",
|
|
17
|
+
]);
|
|
18
|
+
export const inlineCodePlugin = createRevealPlugin({
|
|
19
|
+
// 여는 기호 → 내용 → 닫는 기호 순서로 추가되므로 정렬이 필요 없습니다.
|
|
20
|
+
visit({ decorations, isRevealed }) {
|
|
21
|
+
return {
|
|
22
|
+
enter(node) {
|
|
23
|
+
if (node.name === "InlineCode") {
|
|
24
|
+
// 백틱은 여러 개(``a`b``)일 수 있으므로 트리의 CodeMark 위치를 그대로 사용합니다.
|
|
25
|
+
const open = node.node.firstChild;
|
|
26
|
+
const close = node.node.lastChild;
|
|
27
|
+
if (!open || !close || open.name !== "CodeMark" || close.name !== "CodeMark") {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const mark = isRevealed(node.from, node.to) ? revealedMark : hiddenMark;
|
|
31
|
+
decorations.push(mark.range(open.from, open.to));
|
|
32
|
+
if (open.to < close.from)
|
|
33
|
+
decorations.push(content.range(open.to, close.from));
|
|
34
|
+
decorations.push(mark.range(close.from, close.to));
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (SKIP_CHILDREN.has(node.name))
|
|
38
|
+
return false;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const inlineCodeTheme = EditorView.baseTheme({
|
|
44
|
+
".cm-inline-code": {
|
|
45
|
+
backgroundColor: "rgba(135, 131, 120, 0.15)",
|
|
46
|
+
},
|
|
47
|
+
".cm-inline-code:has(+ .cm-formatting-inline-code)": {
|
|
48
|
+
padding: "1px 0",
|
|
49
|
+
},
|
|
50
|
+
".cm-inline-code:not(.cm-formatting-inline-code):not(:has(+ .cm-formatting-inline-code))": {
|
|
51
|
+
borderRadius: "2px",
|
|
52
|
+
padding: "1px 4px",
|
|
53
|
+
},
|
|
54
|
+
".cm-formatting-inline-code:has(+ .cm-inline-code)": {
|
|
55
|
+
borderTopLeftRadius: "2px",
|
|
56
|
+
borderBottomLeftRadius: "2px",
|
|
57
|
+
padding: "1px 0 1px 4px",
|
|
58
|
+
},
|
|
59
|
+
".cm-inline-code + .cm-formatting-inline-code": {
|
|
60
|
+
borderTopRightRadius: "2px",
|
|
61
|
+
borderBottomRightRadius: "2px",
|
|
62
|
+
padding: "1px 4px 1px 0",
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
/** 인라인 코드(`` `code` ``)의 백틱을 커서가 닿아 있을 때만 보여주는 확장입니다. */
|
|
66
|
+
export function inlineCode() {
|
|
67
|
+
return [inlineCodePlugin, inlineCodeTheme];
|
|
68
|
+
}
|
package/dist/link.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type EditorState, type Extension } from "@codemirror/state";
|
|
2
|
+
import type { SyntaxNode } from "@lezer/common";
|
|
3
|
+
export interface InlineLink {
|
|
4
|
+
/** 링크 전체 범위 */
|
|
5
|
+
readonly from: number;
|
|
6
|
+
readonly to: number;
|
|
7
|
+
/** 라벨 범위 ([ 와 ] 사이) */
|
|
8
|
+
readonly labelFrom: number;
|
|
9
|
+
readonly labelTo: number;
|
|
10
|
+
/** URL 노드 범위 (없으면 null: `[a]()`) */
|
|
11
|
+
readonly urlFrom: number | null;
|
|
12
|
+
readonly urlTo: number | null;
|
|
13
|
+
/** LinkTitle 노드 범위 (없으면 null) */
|
|
14
|
+
readonly titleFrom: number | null;
|
|
15
|
+
readonly titleTo: number | null;
|
|
16
|
+
/** 실제 주소 (꺾쇠 제거, 백슬래시 이스케이프 해제) */
|
|
17
|
+
readonly href: string;
|
|
18
|
+
/** 링크 제목 (따옴표/괄호 제거). 없으면 null */
|
|
19
|
+
readonly title: string | null;
|
|
20
|
+
/** 기호 노드들: [ ] ( ) */
|
|
21
|
+
readonly marks: readonly SyntaxNode[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Link 노드에서 인라인 링크 `[label](url "title")` 정보를 읽습니다.
|
|
25
|
+
* 참조 링크(`[a][ref]`, `[a]`)처럼 인라인 링크가 아니면 null을 반환합니다.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readInlineLink(state: EditorState, link: SyntaxNode): InlineLink | null;
|
|
28
|
+
export declare const linkPlugin: import("@codemirror/view").ViewPlugin<import("./extensions.js").RevealPluginValue, undefined>;
|
|
29
|
+
/** 기본으로 열어도 안전한 주소인지 (http(s), mailto, tel, ftp) */
|
|
30
|
+
export declare function isSafeUrl(href: string): boolean;
|
|
31
|
+
/** 기본 열기 동작: 안전한 절대 주소만 새 탭으로 엽니다. */
|
|
32
|
+
export declare function openLinkSafely(href: string): void;
|
|
33
|
+
export interface LinkOptions {
|
|
34
|
+
/**
|
|
35
|
+
* 렌더링된 링크를 클릭했을 때 호출됩니다. 기본값: openLinkSafely
|
|
36
|
+
* 상대 경로(`./doc.md`)나 앱 내부 이동을 처리하려면 직접 지정하세요.
|
|
37
|
+
*/
|
|
38
|
+
openLink?: (href: string, event: MouseEvent) => void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 인라인 링크 `[label](url "title")`를 하이브리드 방식으로 보여주는 확장입니다.
|
|
42
|
+
* - 커서가 링크에 닿지 않으면 기호와 URL을 숨기고 라벨만 링크 스타일로 보여줍니다.
|
|
43
|
+
* (라벨 안의 굵게/코드 등 인라인 문법은 그대로 유지)
|
|
44
|
+
* - 렌더링된 라벨을 클릭하면 링크를 엽니다. (기본: http(s)/mailto/tel/ftp만 새 탭으로)
|
|
45
|
+
* - 참조 링크(`[a][ref]`, `[a]`), 자동 링크, 이미지는 다루지 않습니다.
|
|
46
|
+
*/
|
|
47
|
+
export declare function link(options?: LinkOptions): Extension;
|