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/tikz.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { syntaxTree } from "@codemirror/language";
|
|
2
|
+
import { Decoration, EditorView, WidgetType } from "@codemirror/view";
|
|
3
|
+
import { readFencedCode, renderedCodeLanguages } from "./codeBlock.js";
|
|
4
|
+
import { createRevealField } from "./utils/reveal.js";
|
|
5
|
+
const PRELOADED = new Set(["tikz", "xcolor", "pgf"]);
|
|
6
|
+
const USE_PACKAGE = /\\usepackage\s*(?:\[([^\]]*)\])?\s*\{([^}]*)\}/g;
|
|
7
|
+
const USE_LIBRARY = /\\usetikzlibrary\s*\{([^}]*)\}/g;
|
|
8
|
+
/** TeX 주석(`%`부터 줄 끝, `\%`는 제외)을 지웁니다. */
|
|
9
|
+
const stripComments = (text) => text.replace(/(^|[^\\])%.*$/gm, "$1");
|
|
10
|
+
/** TikZ 코드 블록 원문을 렌더러가 쓸 수 있게 나눕니다. */
|
|
11
|
+
export function parseTikz(source) {
|
|
12
|
+
const begin = source.search(/\\begin\s*\{document\}/);
|
|
13
|
+
let head;
|
|
14
|
+
let body;
|
|
15
|
+
if (begin >= 0) {
|
|
16
|
+
head = source.slice(0, begin);
|
|
17
|
+
const rest = source.slice(source.indexOf("}", begin) + 1);
|
|
18
|
+
const end = rest.search(/\\end\s*\{document\}/);
|
|
19
|
+
body = end >= 0 ? rest.slice(0, end) : rest;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
head = "";
|
|
23
|
+
body = source;
|
|
24
|
+
}
|
|
25
|
+
const packages = {};
|
|
26
|
+
const libraries = [];
|
|
27
|
+
const collect = (text) => stripComments(text)
|
|
28
|
+
.replace(USE_PACKAGE, (_, options, names) => {
|
|
29
|
+
for (const name of names.split(",").map((n) => n.trim())) {
|
|
30
|
+
if (name && !PRELOADED.has(name))
|
|
31
|
+
packages[name] = (options ?? "").trim();
|
|
32
|
+
}
|
|
33
|
+
return "";
|
|
34
|
+
})
|
|
35
|
+
.replace(USE_LIBRARY, (_, names) => {
|
|
36
|
+
for (const name of names.split(",").map((n) => n.trim()))
|
|
37
|
+
if (name && !libraries.includes(name))
|
|
38
|
+
libraries.push(name);
|
|
39
|
+
return "";
|
|
40
|
+
});
|
|
41
|
+
const preamble = begin >= 0 ? collect(head).trim() : "";
|
|
42
|
+
// 문서 시작이 없으면 선언을 본문에서 뺍니다. (본문에 남으면 TeX 오류)
|
|
43
|
+
if (begin < 0)
|
|
44
|
+
body = collect(body);
|
|
45
|
+
return { source, body: body.trim(), packages, libraries, preamble };
|
|
46
|
+
}
|
|
47
|
+
let tikzjaxLoading = null;
|
|
48
|
+
function loadTikzJax(src, fonts) {
|
|
49
|
+
if (tikzjaxLoading)
|
|
50
|
+
return tikzjaxLoading;
|
|
51
|
+
tikzjaxLoading = new Promise((resolve, reject) => {
|
|
52
|
+
if (fonts) {
|
|
53
|
+
const link = document.head.appendChild(document.createElement("link"));
|
|
54
|
+
link.rel = "stylesheet";
|
|
55
|
+
link.href = fonts;
|
|
56
|
+
}
|
|
57
|
+
const script = document.createElement("script");
|
|
58
|
+
script.src = src;
|
|
59
|
+
script.async = true;
|
|
60
|
+
script.onload = () => resolve();
|
|
61
|
+
script.onerror = () => {
|
|
62
|
+
tikzjaxLoading = null; // 다음에 다시 시도
|
|
63
|
+
script.remove();
|
|
64
|
+
reject(new Error(`TikZJax를 불러오지 못했습니다: ${src}`));
|
|
65
|
+
};
|
|
66
|
+
document.head.appendChild(script);
|
|
67
|
+
});
|
|
68
|
+
return tikzjaxLoading;
|
|
69
|
+
}
|
|
70
|
+
let hiddenHost = null;
|
|
71
|
+
/** TikZJax가 `<script type="text/tikz">`를 찾아 바꾸는 곳 (에디터 밖, 화면 밖) */
|
|
72
|
+
function tikzjaxHost() {
|
|
73
|
+
if (!hiddenHost || !hiddenHost.isConnected) {
|
|
74
|
+
hiddenHost = document.body.appendChild(document.createElement("div"));
|
|
75
|
+
hiddenHost.className = "cm-tikz-host";
|
|
76
|
+
hiddenHost.setAttribute("aria-hidden", "true");
|
|
77
|
+
hiddenHost.style.cssText = "position:fixed;left:-100000px;top:0;width:1px;height:1px;overflow:hidden;visibility:hidden";
|
|
78
|
+
}
|
|
79
|
+
return hiddenHost;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* TikZJax(@drgrice1/tikzjax, 브라우저 안의 WebAssembly TeX)로 그리는 렌더러.
|
|
83
|
+
* TikZJax는 GPL-3.0이므로 이 라이브러리에 포함하지 않고, 앱이 제공하는 주소에서 실행할 때 불러옵니다.
|
|
84
|
+
* 처음 한 번 TeX 엔진(약 7MB)을 내려받고, 결과는 TikZJax가 IndexedDB에 캐시합니다.
|
|
85
|
+
*/
|
|
86
|
+
export function tikzjaxRenderer(options = {}) {
|
|
87
|
+
const src = options.src ?? "/tikzjax/tikzjax.js";
|
|
88
|
+
const fonts = options.fonts ?? src.replace(/[^/]*$/, "fonts.css");
|
|
89
|
+
const timeout = options.timeout ?? 60000;
|
|
90
|
+
return async (doc) => {
|
|
91
|
+
await loadTikzJax(src, fonts);
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const wrapper = document.createElement("div");
|
|
94
|
+
const script = wrapper.appendChild(document.createElement("script"));
|
|
95
|
+
script.type = "text/tikz";
|
|
96
|
+
if (Object.keys(doc.packages).length)
|
|
97
|
+
script.dataset["texPackages"] = JSON.stringify(doc.packages);
|
|
98
|
+
if (doc.libraries.length)
|
|
99
|
+
script.dataset["tikzLibraries"] = doc.libraries.join(",");
|
|
100
|
+
if (doc.preamble)
|
|
101
|
+
script.dataset["addToPreamble"] = doc.preamble;
|
|
102
|
+
script.textContent = doc.body;
|
|
103
|
+
const finish = (error, svg) => {
|
|
104
|
+
observer.disconnect();
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
wrapper.remove();
|
|
107
|
+
if (error)
|
|
108
|
+
reject(error);
|
|
109
|
+
else
|
|
110
|
+
resolve(svg);
|
|
111
|
+
};
|
|
112
|
+
// 성공: 만든 SVG에서 tikzjax-load-finished 이벤트. 실패: TikZJax가 SVG 대신 깨진 이미지를 넣습니다.
|
|
113
|
+
wrapper.addEventListener("tikzjax-load-finished", (event) => {
|
|
114
|
+
finish(null, event.target.outerHTML);
|
|
115
|
+
});
|
|
116
|
+
const observer = new MutationObserver(() => {
|
|
117
|
+
if (wrapper.querySelector("img"))
|
|
118
|
+
finish(new Error("TikZ 코드를 컴파일하지 못했습니다."));
|
|
119
|
+
});
|
|
120
|
+
observer.observe(wrapper, { childList: true, subtree: true });
|
|
121
|
+
const timer = setTimeout(() => finish(new Error("TikZ 렌더링 시간이 초과되었습니다.")), timeout);
|
|
122
|
+
tikzjaxHost().appendChild(wrapper);
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const CACHE_LIMIT = 50;
|
|
127
|
+
class TikzCache {
|
|
128
|
+
entries = new Map();
|
|
129
|
+
render;
|
|
130
|
+
constructor(render) {
|
|
131
|
+
this.render = render;
|
|
132
|
+
}
|
|
133
|
+
get(source) {
|
|
134
|
+
let entry = this.entries.get(source);
|
|
135
|
+
if (entry) {
|
|
136
|
+
this.entries.delete(source); // 최근 사용으로 갱신
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const created = {
|
|
140
|
+
status: "pending",
|
|
141
|
+
svg: "",
|
|
142
|
+
error: "",
|
|
143
|
+
promise: Promise.resolve()
|
|
144
|
+
.then(() => this.render(parseTikz(source)))
|
|
145
|
+
.then((svg) => {
|
|
146
|
+
created.status = "done";
|
|
147
|
+
created.svg = sanitizeSvg(svg);
|
|
148
|
+
}, (error) => {
|
|
149
|
+
created.status = "error";
|
|
150
|
+
created.error = error instanceof Error ? error.message : String(error);
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
entry = created;
|
|
154
|
+
}
|
|
155
|
+
this.entries.set(source, entry);
|
|
156
|
+
if (this.entries.size > CACHE_LIMIT)
|
|
157
|
+
this.entries.delete(this.entries.keys().next().value);
|
|
158
|
+
return entry;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* 렌더러가 돌려준 SVG에서 스크립트와 이벤트 속성, javascript: 링크를 지웁니다.
|
|
163
|
+
* (TeX의 \special로 SVG 조각을 넣을 수 있으므로, 신뢰할 수 없는 문서도 안전하게 보여주기 위함)
|
|
164
|
+
*/
|
|
165
|
+
export function sanitizeSvg(markup) {
|
|
166
|
+
const doc = new DOMParser().parseFromString(markup, "image/svg+xml");
|
|
167
|
+
const root = doc.documentElement;
|
|
168
|
+
if (root.nodeName.toLowerCase() !== "svg" || doc.getElementsByTagName("parsererror").length)
|
|
169
|
+
return "";
|
|
170
|
+
for (const el of [...root.querySelectorAll("script, foreignObject")])
|
|
171
|
+
el.remove();
|
|
172
|
+
for (const el of [root, ...root.querySelectorAll("*")]) {
|
|
173
|
+
for (const attr of [...el.attributes]) {
|
|
174
|
+
const name = attr.name.toLowerCase();
|
|
175
|
+
if (name.startsWith("on"))
|
|
176
|
+
el.removeAttribute(attr.name);
|
|
177
|
+
else if ((name === "href" || name === "xlink:href") && /^\s*javascript:/i.test(attr.value)) {
|
|
178
|
+
el.removeAttribute(attr.name);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return new XMLSerializer().serializeToString(root);
|
|
183
|
+
}
|
|
184
|
+
class TikzWidget extends WidgetType {
|
|
185
|
+
source;
|
|
186
|
+
kind;
|
|
187
|
+
entry;
|
|
188
|
+
constructor(source, kind, entry) {
|
|
189
|
+
super();
|
|
190
|
+
this.source = source;
|
|
191
|
+
this.kind = kind;
|
|
192
|
+
this.entry = entry;
|
|
193
|
+
}
|
|
194
|
+
eq(other) {
|
|
195
|
+
return other.source === this.source && other.kind === this.kind && other.entry === this.entry;
|
|
196
|
+
}
|
|
197
|
+
toDOM(view) {
|
|
198
|
+
const el = document.createElement(this.kind === "block" ? "div" : "span");
|
|
199
|
+
el.className = `cm-tikz cm-tikz-${this.kind}`;
|
|
200
|
+
this.fill(el);
|
|
201
|
+
if (this.entry.status === "pending") {
|
|
202
|
+
void this.entry.promise.then(() => {
|
|
203
|
+
this.fill(el);
|
|
204
|
+
view.requestMeasure(); // 그림이 들어와 높이가 바뀜
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return el;
|
|
208
|
+
}
|
|
209
|
+
fill(el) {
|
|
210
|
+
const { status, svg, error } = this.entry;
|
|
211
|
+
el.classList.toggle("cm-tikz-pending", status === "pending");
|
|
212
|
+
el.classList.toggle("cm-tikz-error", status === "error");
|
|
213
|
+
if (status === "done") {
|
|
214
|
+
el.innerHTML = svg;
|
|
215
|
+
}
|
|
216
|
+
else if (status === "error") {
|
|
217
|
+
el.textContent = `TikZ 오류: ${error}`;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
el.innerHTML = '<span class="cm-tikz-spinner" aria-hidden="true"></span>';
|
|
221
|
+
el.appendChild(document.createTextNode("TikZ 그리는 중…"));
|
|
222
|
+
}
|
|
223
|
+
el.setAttribute("aria-busy", String(status === "pending"));
|
|
224
|
+
}
|
|
225
|
+
get estimatedHeight() {
|
|
226
|
+
return this.kind === "block" ? 160 : -1;
|
|
227
|
+
}
|
|
228
|
+
// 클릭하면 에디터가 처리해 커서가 블록으로 이동하고 원문이 펼쳐집니다.
|
|
229
|
+
ignoreEvent() {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const CONTAINERS = new Set(["Document", "Blockquote", "BulletList", "OrderedList", "ListItem"]);
|
|
234
|
+
function scanTikz(state, languages) {
|
|
235
|
+
const { doc } = state;
|
|
236
|
+
const found = [];
|
|
237
|
+
syntaxTree(state).iterate({
|
|
238
|
+
enter(node) {
|
|
239
|
+
if (node.name === "FencedCode") {
|
|
240
|
+
const block = readFencedCode(state, node.node);
|
|
241
|
+
if (block && languages.has(block.lang) && block.code.trim()) {
|
|
242
|
+
const first = doc.lineAt(node.from);
|
|
243
|
+
const last = doc.lineAt(node.to);
|
|
244
|
+
found.push({
|
|
245
|
+
from: node.from,
|
|
246
|
+
to: node.to,
|
|
247
|
+
source: block.code,
|
|
248
|
+
wholeLines: node.node.parent?.name === "Document" &&
|
|
249
|
+
!first.text.slice(0, node.from - first.from).trim() &&
|
|
250
|
+
!last.text.slice(node.to - last.from).trim(),
|
|
251
|
+
lineFrom: first.from,
|
|
252
|
+
lineTo: last.to,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
if (!CONTAINERS.has(node.name))
|
|
258
|
+
return false;
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
return found;
|
|
262
|
+
}
|
|
263
|
+
const tikzTheme = EditorView.baseTheme({
|
|
264
|
+
".cm-tikz-block, .cm-tikz-inline, .cm-tikz-preview": {
|
|
265
|
+
display: "block",
|
|
266
|
+
textAlign: "center",
|
|
267
|
+
overflowX: "auto",
|
|
268
|
+
overflowY: "hidden",
|
|
269
|
+
},
|
|
270
|
+
// 위젯 안의 세로 margin은 높이 계산을 깨뜨리므로 padding으로 여백을 줍니다.
|
|
271
|
+
".cm-tikz-block": { padding: "0.5em 6px" },
|
|
272
|
+
".cm-tikz-inline": { padding: "0.25em 0" },
|
|
273
|
+
".cm-tikz-preview": { padding: "0.5em 6px", borderRadius: "4px", cursor: "default" },
|
|
274
|
+
"&light .cm-tikz-preview": { backgroundColor: "rgba(135, 131, 120, 0.08)" },
|
|
275
|
+
"&dark .cm-tikz-preview": { backgroundColor: "rgba(255, 255, 255, 0.05)" },
|
|
276
|
+
".cm-tikz svg": { display: "inline-block", maxWidth: "100%", height: "auto", verticalAlign: "middle" },
|
|
277
|
+
// 어두운 테마: 검은 선/글자를 밝게 (색상은 유지)
|
|
278
|
+
"&dark .cm-tikz svg": { filter: "invert(1) hue-rotate(180deg)" },
|
|
279
|
+
".cm-tikz-pending, .cm-tikz-error": {
|
|
280
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
281
|
+
fontSize: "0.875em",
|
|
282
|
+
},
|
|
283
|
+
"&light .cm-tikz-pending": { color: "#6b7280" },
|
|
284
|
+
"&dark .cm-tikz-pending": { color: "#9ca3af" },
|
|
285
|
+
".cm-tikz-error": { color: "#cc0000" },
|
|
286
|
+
".cm-tikz-spinner": {
|
|
287
|
+
display: "inline-block",
|
|
288
|
+
width: "0.9em",
|
|
289
|
+
height: "0.9em",
|
|
290
|
+
marginRight: "0.5em",
|
|
291
|
+
verticalAlign: "-0.1em",
|
|
292
|
+
border: "2px solid currentColor",
|
|
293
|
+
borderRightColor: "transparent",
|
|
294
|
+
borderRadius: "50%",
|
|
295
|
+
animation: "cm-tikz-spin 0.8s linear infinite",
|
|
296
|
+
},
|
|
297
|
+
"@keyframes cm-tikz-spin": { to: { transform: "rotate(360deg)" } },
|
|
298
|
+
"@media (prefers-reduced-motion: reduce)": { ".cm-tikz-spinner": { animation: "none" } },
|
|
299
|
+
});
|
|
300
|
+
/**
|
|
301
|
+
* ```` ```tikz ```` 코드 블록을 그림으로 보여주는 확장입니다.
|
|
302
|
+
* - 원문: Obsidian TikZJax 형식(`\usepackage`, `\usetikzlibrary`, `\begin{document}`…) 또는 tikzpicture만
|
|
303
|
+
* - 커서가 블록 밖이면 그림, 안이면 코드 블록 원문과 그 아래 미리보기
|
|
304
|
+
* - 렌더러는 바꿀 수 있습니다. 기본은 TikZJax(브라우저 WebAssembly TeX)이며, 그 파일은 앱이 제공해야 합니다.
|
|
305
|
+
* ```ts
|
|
306
|
+
* tikz({ render: tikzjaxRenderer({ src: "/tikzjax/tikzjax.js" }) })
|
|
307
|
+
* ```
|
|
308
|
+
* - 같은 원문은 한 번만 렌더링합니다. 그리는 동안에는 진행 표시, 실패하면 오류를 보여줍니다.
|
|
309
|
+
*/
|
|
310
|
+
export function tikz(options = {}) {
|
|
311
|
+
const cache = new TikzCache(options.render ?? tikzjaxRenderer());
|
|
312
|
+
const languages = new Set((options.languages ?? ["tikz"]).map((l) => l.toLowerCase()));
|
|
313
|
+
const field = createRevealField({
|
|
314
|
+
scan: (state) => scanTikz(state, languages),
|
|
315
|
+
build(blocks, revealed) {
|
|
316
|
+
const out = [];
|
|
317
|
+
blocks.forEach((block, i) => {
|
|
318
|
+
const entry = cache.get(block.source);
|
|
319
|
+
if (!revealed[i]) {
|
|
320
|
+
if (block.wholeLines) {
|
|
321
|
+
const widget = new TikzWidget(block.source, "block", entry);
|
|
322
|
+
out.push(Decoration.replace({ widget, block: true }).range(block.lineFrom, block.lineTo));
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
// 인용/목록 안: 줄 중간의 블록 위젯은 줄을 쪼개므로 접두어 뒤를 인라인 위젯으로 대체합니다.
|
|
326
|
+
const widget = new TikzWidget(block.source, "inline", entry);
|
|
327
|
+
out.push(Decoration.replace({ widget }).range(block.from, block.to));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
// 원문은 codeBlock()이 코드 블록으로 보여주고, 그 아래에 미리보기를 둡니다.
|
|
332
|
+
const widget = new TikzWidget(block.source, "preview", entry);
|
|
333
|
+
out.push(block.wholeLines
|
|
334
|
+
? Decoration.widget({ widget, block: true, side: 1 }).range(block.lineTo)
|
|
335
|
+
: Decoration.widget({ widget, side: 1 }).range(block.to));
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
return Decoration.set(out, true);
|
|
339
|
+
},
|
|
340
|
+
block: (block) => block.wholeLines,
|
|
341
|
+
});
|
|
342
|
+
return [field, renderedCodeLanguages.of([...languages]), tikzTheme];
|
|
343
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Tag, tags } from "@lezer/highlight";
|
|
2
|
+
import { doubleDelimiterSyntax } from "./delimiter.js";
|
|
3
|
+
/** `--text--` 밑줄 문법에 사용할 하이라이트 태그 */
|
|
4
|
+
export const underlineTag = Tag.define(tags.content);
|
|
5
|
+
/** `--text--` 인라인 밑줄 문법 (노드: Underline, UnderlineMark) */
|
|
6
|
+
export const Underline = doubleDelimiterSyntax({ name: "Underline", char: "-", tag: underlineTag });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 콜아웃/코드 블록 같은 상자형 블록의 좌우 바깥 여백. (폭은 제한하지 않고 양옆만 살짝 띄웁니다)
|
|
3
|
+
* 에디터(또는 그 조상)에서 CSS 변수 `--cm-block-margin-x`로 바꿀 수 있습니다. 여백 없이: `0`.
|
|
4
|
+
* 줄에 세로 margin을 주면 CodeMirror의 줄 높이 계산이 틀어지지만, 가로 margin은 괜찮습니다.
|
|
5
|
+
*/
|
|
6
|
+
export declare const BLOCK_MARGIN_X = "var(--cm-block-margin-x, 8px)";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 콜아웃/코드 블록 같은 상자형 블록의 좌우 바깥 여백. (폭은 제한하지 않고 양옆만 살짝 띄웁니다)
|
|
3
|
+
* 에디터(또는 그 조상)에서 CSS 변수 `--cm-block-margin-x`로 바꿀 수 있습니다. 여백 없이: `0`.
|
|
4
|
+
* 줄에 세로 margin을 주면 CodeMirror의 줄 높이 계산이 틀어지지만, 가로 margin은 괜찮습니다.
|
|
5
|
+
*/
|
|
6
|
+
export const BLOCK_MARGIN_X = "var(--cm-block-margin-x, 8px)";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InlineContext } from "@lezer/markdown";
|
|
2
|
+
/** 코드 포인트가 차지하는 UTF-16 코드 단위 수 */
|
|
3
|
+
export declare const sizeOf: (codePoint: number) => number;
|
|
4
|
+
/** pos 위치의 코드 포인트. 구간 끝이면 -1 (서로게이트 쌍을 하나로 합칩니다) */
|
|
5
|
+
export declare function codePointAt(cx: InlineContext, pos: number): number;
|
|
6
|
+
/** pos 바로 앞의 코드 포인트. 구간 시작이면 -1 (서로게이트 쌍을 하나로 합칩니다) */
|
|
7
|
+
export declare function codePointBefore(cx: InlineContext, pos: number): number;
|
|
8
|
+
/**
|
|
9
|
+
* 코드 포인트 → 비트 플래그 분류 함수를 만듭니다.
|
|
10
|
+
* ASCII는 미리 계산한 테이블로 O(1) 조회하고, 그 외에는 classify를 호출합니다.
|
|
11
|
+
* 구간 끝(-1)은 0을 반환합니다.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createClassifier(classify: (ch: string) => number): (codePoint: number) => number;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const isHighSurrogate = (c) => c >= 0xd800 && c <= 0xdbff;
|
|
2
|
+
const isLowSurrogate = (c) => c >= 0xdc00 && c <= 0xdfff;
|
|
3
|
+
const combine = (high, low) => ((high - 0xd800) << 10) + (low - 0xdc00) + 0x10000;
|
|
4
|
+
/** 코드 포인트가 차지하는 UTF-16 코드 단위 수 */
|
|
5
|
+
export const sizeOf = (codePoint) => (codePoint > 0xffff ? 2 : 1);
|
|
6
|
+
/** pos 위치의 코드 포인트. 구간 끝이면 -1 (서로게이트 쌍을 하나로 합칩니다) */
|
|
7
|
+
export function codePointAt(cx, pos) {
|
|
8
|
+
const c = cx.char(pos);
|
|
9
|
+
if (isHighSurrogate(c)) {
|
|
10
|
+
const low = cx.char(pos + 1);
|
|
11
|
+
if (isLowSurrogate(low))
|
|
12
|
+
return combine(c, low);
|
|
13
|
+
}
|
|
14
|
+
return c;
|
|
15
|
+
}
|
|
16
|
+
/** pos 바로 앞의 코드 포인트. 구간 시작이면 -1 (서로게이트 쌍을 하나로 합칩니다) */
|
|
17
|
+
export function codePointBefore(cx, pos) {
|
|
18
|
+
// cx.char()는 구간 시작 이전 위치에서 NaN을 반환하므로 직접 경계를 처리합니다.
|
|
19
|
+
if (pos <= cx.offset)
|
|
20
|
+
return -1;
|
|
21
|
+
const c = cx.char(pos - 1);
|
|
22
|
+
if (isLowSurrogate(c) && pos - 2 >= cx.offset) {
|
|
23
|
+
const high = cx.char(pos - 2);
|
|
24
|
+
if (isHighSurrogate(high))
|
|
25
|
+
return combine(high, c);
|
|
26
|
+
}
|
|
27
|
+
return c;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 코드 포인트 → 비트 플래그 분류 함수를 만듭니다.
|
|
31
|
+
* ASCII는 미리 계산한 테이블로 O(1) 조회하고, 그 외에는 classify를 호출합니다.
|
|
32
|
+
* 구간 끝(-1)은 0을 반환합니다.
|
|
33
|
+
*/
|
|
34
|
+
export function createClassifier(classify) {
|
|
35
|
+
const ascii = new Uint8Array(128);
|
|
36
|
+
for (let code = 0; code < 128; code++)
|
|
37
|
+
ascii[code] = classify(String.fromCharCode(code));
|
|
38
|
+
return (codePoint) => codePoint < 0
|
|
39
|
+
? 0
|
|
40
|
+
: codePoint < 128
|
|
41
|
+
? ascii[codePoint]
|
|
42
|
+
: classify(String.fromCodePoint(codePoint));
|
|
43
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 컨테이너(인용, 콜아웃)가 줄에 알려주는 내용 영역 정보. 코드 블록처럼 줄 안에 상자를 그리는 블록이
|
|
3
|
+
* 컨테이너의 세로줄/상자를 가리지 않고 그 안쪽에 그리도록 씁니다.
|
|
4
|
+
* - `--cm-container-indent`: 줄 왼쪽에서 내용이 시작하는 곳까지 (기본: CodeMirror 줄 왼쪽 padding 6px)
|
|
5
|
+
* - `--cm-container-right`: 줄 오른쪽에서 내용이 끝나는 곳까지 (기본: CodeMirror 줄 오른쪽 padding 2px)
|
|
6
|
+
* - `--cm-container-bottom`: 줄 아래에서 내용이 끝나는 곳까지 (콜아웃의 끝 줄 아래 여백, 기본 0)
|
|
7
|
+
*/
|
|
8
|
+
export declare const CONTAINER_INDENT = "var(--cm-container-indent, 6px)";
|
|
9
|
+
export declare const CONTAINER_RIGHT = "var(--cm-container-right, 2px)";
|
|
10
|
+
export declare const CONTAINER_BOTTOM = "var(--cm-container-bottom, 0px)";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 컨테이너(인용, 콜아웃)가 줄에 알려주는 내용 영역 정보. 코드 블록처럼 줄 안에 상자를 그리는 블록이
|
|
3
|
+
* 컨테이너의 세로줄/상자를 가리지 않고 그 안쪽에 그리도록 씁니다.
|
|
4
|
+
* - `--cm-container-indent`: 줄 왼쪽에서 내용이 시작하는 곳까지 (기본: CodeMirror 줄 왼쪽 padding 6px)
|
|
5
|
+
* - `--cm-container-right`: 줄 오른쪽에서 내용이 끝나는 곳까지 (기본: CodeMirror 줄 오른쪽 padding 2px)
|
|
6
|
+
* - `--cm-container-bottom`: 줄 아래에서 내용이 끝나는 곳까지 (콜아웃의 끝 줄 아래 여백, 기본 0)
|
|
7
|
+
*/
|
|
8
|
+
export const CONTAINER_INDENT = "var(--cm-container-indent, 6px)";
|
|
9
|
+
export const CONTAINER_RIGHT = "var(--cm-container-right, 2px)";
|
|
10
|
+
export const CONTAINER_BOTTOM = "var(--cm-container-bottom, 0px)";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { EditorState, StateField, type EditorSelection, type Range } from "@codemirror/state";
|
|
2
|
+
import { Decoration, EditorView, ViewPlugin, type DecorationSet, type PluginValue, type ViewUpdate } from "@codemirror/view";
|
|
3
|
+
import { type SyntaxTreeIterator } from "./syntaxTree.js";
|
|
4
|
+
/** 선택 범위(멀티 커서 포함) 중 하나라도 노드에 닿아 있으면 true */
|
|
5
|
+
export declare function isRevealed(selection: EditorSelection, node: {
|
|
6
|
+
from: number;
|
|
7
|
+
to: number;
|
|
8
|
+
}): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* 데코레이션을 처음부터 다시 만들어야 하는지 여부
|
|
11
|
+
* - 백그라운드 파싱이 끝나면 문서/뷰포트/선택 변화 없이 트리만 바뀌므로 트리도 비교합니다.
|
|
12
|
+
* - 설정이 바뀌면(확장 추가/제거, Facet 값 변경) 다른 플러그인과의 역할 분담이 달라질 수 있습니다.
|
|
13
|
+
*/
|
|
14
|
+
export declare function needsRebuild(update: ViewUpdate): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* 빌드 시 각 노드의 "기호 표시 여부"를 기록해 두었다가,
|
|
17
|
+
* 선택만 바뀌었을 때 표시 여부가 달라진 노드가 있는지 빠르게 확인합니다.
|
|
18
|
+
*/
|
|
19
|
+
export declare class RevealTracker {
|
|
20
|
+
private ranges;
|
|
21
|
+
private flags;
|
|
22
|
+
/** 새 빌드를 시작할 때 호출합니다. */
|
|
23
|
+
reset(): void;
|
|
24
|
+
/** 노드의 표시 여부를 계산하고 기록합니다. */
|
|
25
|
+
track(selection: EditorSelection, from: number, to: number): boolean;
|
|
26
|
+
/** 주어진 선택에서 표시 여부가 달라지는 노드가 하나라도 있으면 true */
|
|
27
|
+
changed(selection: EditorSelection): boolean;
|
|
28
|
+
}
|
|
29
|
+
/** 빌드 1회 동안 visit 콜백에 전달되는 컨텍스트 */
|
|
30
|
+
export interface RevealContext {
|
|
31
|
+
readonly state: EditorState;
|
|
32
|
+
/** 에디터 뷰 (보이는 범위 등이 필요할 때) */
|
|
33
|
+
readonly view: EditorView;
|
|
34
|
+
/** 결과 데코레이션 목록. 여기에 push 합니다. */
|
|
35
|
+
readonly decorations: Range<Decoration>[];
|
|
36
|
+
/**
|
|
37
|
+
* 노드의 기호를 보여줄지(선택 범위가 노드에 닿는지) 계산하고 기록합니다.
|
|
38
|
+
* 기록된 노드만 "선택만 바뀐 경우 재빌드 생략" 판정에 사용되므로,
|
|
39
|
+
* 표시 여부에 따라 데코레이션이 달라지는 노드는 반드시 이 함수로 판정해야 합니다.
|
|
40
|
+
*/
|
|
41
|
+
isRevealed(from: number, to: number): boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface RevealPluginSpec {
|
|
44
|
+
/**
|
|
45
|
+
* 빌드마다 호출되어, 보이는 구문 트리를 순회할 콜백을 돌려줍니다.
|
|
46
|
+
* 빌드 단위 상태(스택 등)는 이 함수의 클로저에 둡니다.
|
|
47
|
+
*/
|
|
48
|
+
visit(cx: RevealContext): SyntaxTreeIterator;
|
|
49
|
+
/** 데코레이션이 문서 순서대로 추가되지 않는다면 true (기본값 false) */
|
|
50
|
+
readonly sort?: boolean;
|
|
51
|
+
/** 기본 조건(문서/뷰포트/구문 트리/설정 변경) 외에 다시 빌드해야 하는 업데이트 (예: 접기 상태 변경) */
|
|
52
|
+
rebuildOn?(update: ViewUpdate): boolean;
|
|
53
|
+
}
|
|
54
|
+
/** createRevealPlugin()이 만드는 플러그인 인스턴스 */
|
|
55
|
+
export declare class RevealPluginValue implements PluginValue {
|
|
56
|
+
decorations: DecorationSet;
|
|
57
|
+
private readonly tracker;
|
|
58
|
+
private readonly spec;
|
|
59
|
+
constructor(view: EditorView, spec: RevealPluginSpec);
|
|
60
|
+
update(update: ViewUpdate): void;
|
|
61
|
+
private build;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* "커서가 닿은 노드만 기호를 보여주는" 플러그인의 공용 골격을 만듭니다.
|
|
65
|
+
*
|
|
66
|
+
* 공통으로 처리하는 것:
|
|
67
|
+
* - 보이는 범위의 구문 트리만 순회 (접힌 영역 중복 방문 없음)
|
|
68
|
+
* - 문서/뷰포트/구문 트리 변경 시 재빌드
|
|
69
|
+
* - 선택만 바뀐 경우 표시 여부가 달라진 노드가 있을 때만 재빌드
|
|
70
|
+
* - 멀티 커서 지원
|
|
71
|
+
*/
|
|
72
|
+
export declare function createRevealPlugin(spec: RevealPluginSpec): ViewPlugin<RevealPluginValue>;
|
|
73
|
+
export interface RevealFieldSpec<T extends {
|
|
74
|
+
readonly from: number;
|
|
75
|
+
readonly to: number;
|
|
76
|
+
}> {
|
|
77
|
+
/**
|
|
78
|
+
* 대상(블록)들을 문서 순서대로 찾습니다. 문서/구문 트리/설정이 바뀔 때만 호출됩니다.
|
|
79
|
+
* 문서 전체를 다루므로, 대상이 올 수 있는 곳만 훑도록 구현하는 것이 좋습니다.
|
|
80
|
+
*/
|
|
81
|
+
scan(state: EditorState): readonly T[];
|
|
82
|
+
/** 대상과 각 대상의 표시 여부(커서가 닿았는지)로 데코레이션을 만듭니다. */
|
|
83
|
+
build(items: readonly T[], revealed: readonly boolean[], state: EditorState): DecorationSet;
|
|
84
|
+
/**
|
|
85
|
+
* 숨겨진 대상이 줄 전체를 대체하는 블록 위젯으로 그려지는지.
|
|
86
|
+
* CodeMirror의 위/아래 화살표는 블록 위젯을 건너뛰므로, true인 대상은 건너뛰는 대신 커서를 대상 안으로
|
|
87
|
+
* 옮겨 원문을 펼칩니다. (없으면 건너뛰기를 고치지 않습니다)
|
|
88
|
+
*/
|
|
89
|
+
block?(item: T): boolean;
|
|
90
|
+
}
|
|
91
|
+
export interface RevealFieldValue<T> {
|
|
92
|
+
readonly items: readonly T[];
|
|
93
|
+
readonly revealed: readonly boolean[];
|
|
94
|
+
readonly decorations: DecorationSet;
|
|
95
|
+
}
|
|
96
|
+
export declare function createRevealField<T extends {
|
|
97
|
+
readonly from: number;
|
|
98
|
+
readonly to: number;
|
|
99
|
+
}>(spec: RevealFieldSpec<T>): StateField<RevealFieldValue<T>>;
|