@aeriz/wysiwyg 1.0.4 → 1.0.5
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 +13 -13
- package/dist/aeriz-wysiwyg.cjs +153 -3
- package/dist/aeriz-wysiwyg.esm.js +153 -3
- package/dist/aeriz-wysiwyg.esm.min.js +891 -784
- package/dist/aeriz-wysiwyg.js +153 -3
- package/dist/aeriz-wysiwyg.min.cjs +40 -32
- package/dist/aeriz-wysiwyg.min.js +41 -33
- package/dist-types/cjs/core/AerizEditor.d.cts +34 -0
- package/dist-types/cjs/core/highlight.d.cts +2 -0
- package/dist-types/core/AerizEditor.d.ts +34 -0
- package/dist-types/core/AerizEditor.d.ts.map +1 -1
- package/dist-types/core/blocks.d.ts.map +1 -1
- package/dist-types/core/highlight.d.ts +2 -0
- package/dist-types/core/highlight.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,18 +71,18 @@ editor.onStateChange(() => viewer.setBlocks(editor.serialize()));
|
|
|
71
71
|
<!-- ✓ 프로덕션 — 특정 버전 고정 (immutable, 가장 긴 캐시) -->
|
|
72
72
|
<link
|
|
73
73
|
rel="stylesheet"
|
|
74
|
-
href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.
|
|
74
|
+
href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.css"
|
|
75
75
|
/>
|
|
76
|
-
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.
|
|
76
|
+
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.js"></script>
|
|
77
77
|
|
|
78
78
|
<!-- 1.x 트랙 — 패치만 자동 수신 -->
|
|
79
|
-
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@^1.0.
|
|
79
|
+
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@^1.0.5/dist/aeriz-wysiwyg.min.js"></script>
|
|
80
80
|
|
|
81
81
|
<!-- 항상 최신 — 프로토타이핑용 (프로덕션 권장 X) -->
|
|
82
82
|
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@latest/dist/aeriz-wysiwyg.min.js"></script>
|
|
83
83
|
|
|
84
84
|
<!-- unpkg 미러 -->
|
|
85
|
-
<script src="https://unpkg.com/@aeriz/wysiwyg@1.0.
|
|
85
|
+
<script src="https://unpkg.com/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.js"></script>
|
|
86
86
|
|
|
87
87
|
<div id="editor"></div>
|
|
88
88
|
<script>
|
|
@@ -99,13 +99,13 @@ editor.onStateChange(() => viewer.setBlocks(editor.serialize()));
|
|
|
99
99
|
```html
|
|
100
100
|
<link
|
|
101
101
|
rel="stylesheet"
|
|
102
|
-
href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.
|
|
102
|
+
href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.css"
|
|
103
103
|
/>
|
|
104
104
|
<script type="module">
|
|
105
105
|
import {
|
|
106
106
|
AerizEditor,
|
|
107
107
|
AerizViewer,
|
|
108
|
-
} from 'https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.
|
|
108
|
+
} from 'https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.esm.min.js';
|
|
109
109
|
|
|
110
110
|
new AerizEditor({ element: document.querySelector('#editor') });
|
|
111
111
|
</script>
|
|
@@ -130,12 +130,12 @@ editor.onStateChange(() => viewer.setBlocks(editor.serialize()));
|
|
|
130
130
|
```html
|
|
131
131
|
<link
|
|
132
132
|
rel="stylesheet"
|
|
133
|
-
href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.
|
|
133
|
+
href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.css"
|
|
134
134
|
/>
|
|
135
135
|
<!-- 1) 코어 먼저 -->
|
|
136
|
-
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.
|
|
136
|
+
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.js"></script>
|
|
137
137
|
<!-- 2) 필요한 어댑터 -->
|
|
138
|
-
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.
|
|
138
|
+
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg-wc.min.js"></script>
|
|
139
139
|
|
|
140
140
|
<aeriz-editor theme="dark" placeholder="내용을 입력하세요"></aeriz-editor>
|
|
141
141
|
```
|
|
@@ -144,8 +144,8 @@ jQuery 는 jQuery 자체를 먼저 로드해야 자동 등록됩니다.
|
|
|
144
144
|
|
|
145
145
|
```html
|
|
146
146
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
147
|
-
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.
|
|
148
|
-
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.
|
|
147
|
+
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.js"></script>
|
|
148
|
+
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg-jquery.min.js"></script>
|
|
149
149
|
<script>
|
|
150
150
|
$('#editor').aerizEditor({ value: '<p>안녕하세요</p>' });
|
|
151
151
|
</script>
|
|
@@ -162,8 +162,8 @@ React / Vue 어댑터는 IIFE 로 제공하지 않습니다. 소비자 앱의
|
|
|
162
162
|
{
|
|
163
163
|
"imports": {
|
|
164
164
|
"vue": "https://esm.sh/vue@3",
|
|
165
|
-
"@aeriz/wysiwyg": "https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.
|
|
166
|
-
"@aeriz/wysiwyg/vue": "https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.
|
|
165
|
+
"@aeriz/wysiwyg": "https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.esm.min.js",
|
|
166
|
+
"@aeriz/wysiwyg/vue": "https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/adapters/vue.js"
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
</script>
|
package/dist/aeriz-wysiwyg.cjs
CHANGED
|
@@ -2425,8 +2425,9 @@ const LANG_BY_ID = (() => {
|
|
|
2425
2425
|
for (const l of LANGUAGES) m.set(l.id, l);
|
|
2426
2426
|
return m;
|
|
2427
2427
|
})();
|
|
2428
|
+
const PLAIN_TEXT_ID = "plaintext";
|
|
2428
2429
|
function highlightCode(code, langId) {
|
|
2429
|
-
if (!langId) return escapeHtml$2(code);
|
|
2430
|
+
if (!langId || langId === PLAIN_TEXT_ID) return escapeHtml$2(code);
|
|
2430
2431
|
const lang = LANG_BY_ID.get(langId);
|
|
2431
2432
|
if (!lang) return escapeHtml$2(code);
|
|
2432
2433
|
if (lang.custom) return lang.custom(code);
|
|
@@ -3485,6 +3486,7 @@ const CODE_BLOCK = {
|
|
|
3485
3486
|
const pre = document.createElement("pre");
|
|
3486
3487
|
const code = document.createElement("code");
|
|
3487
3488
|
if (attrs == null ? void 0 : attrs.language) code.setAttribute("data-language", attrs.language);
|
|
3489
|
+
fillEmpty(code);
|
|
3488
3490
|
pre.appendChild(code);
|
|
3489
3491
|
return pre;
|
|
3490
3492
|
},
|
|
@@ -3990,9 +3992,15 @@ function normalizeContent(root, options = {}) {
|
|
|
3990
3992
|
fig.appendChild(makeResizeHandle());
|
|
3991
3993
|
}
|
|
3992
3994
|
}
|
|
3995
|
+
ensureCodeCarets(root);
|
|
3993
3996
|
dropEmptyCaptions(root);
|
|
3994
3997
|
if (options.writableTail !== false) ensureWritableTail(root);
|
|
3995
3998
|
}
|
|
3999
|
+
function ensureCodeCarets(root) {
|
|
4000
|
+
for (const code of Array.from(root.querySelectorAll("pre > code"))) {
|
|
4001
|
+
if (code.childNodes.length === 0) code.appendChild(document.createElement("br"));
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
3996
4004
|
function dropEmptyCaptions(root) {
|
|
3997
4005
|
var _a;
|
|
3998
4006
|
const captions = root.querySelectorAll("figure > figcaption");
|
|
@@ -5619,7 +5627,7 @@ const BLOCK_TOGGLE_MAP = {
|
|
|
5619
5627
|
};
|
|
5620
5628
|
const DRAG_MIME = "application/x-aeriz-block";
|
|
5621
5629
|
const MIN_BLOCK_WIDTH = 80;
|
|
5622
|
-
class
|
|
5630
|
+
const _AerizEditor = class _AerizEditor {
|
|
5623
5631
|
constructor(options) {
|
|
5624
5632
|
__publicField(this, "root");
|
|
5625
5633
|
__publicField(this, "content");
|
|
@@ -5657,6 +5665,11 @@ class AerizEditor {
|
|
|
5657
5665
|
__publicField(this, "hoverRaf", null);
|
|
5658
5666
|
/** 라벨 pill 의 등장 애니메이션을 블록이 **바뀔 때만** 다시 재생하기 위한 표식. */
|
|
5659
5667
|
__publicField(this, "pillBlockId", null);
|
|
5668
|
+
/**
|
|
5669
|
+
* IME 조합이 진행 중인지. 한글 · 일본어 · 중국어 입력은 글자 하나가 여러
|
|
5670
|
+
* 입력에 걸쳐 만들어지는데, 그 사이에 DOM 을 다시 쓰면 조합이 끊긴다.
|
|
5671
|
+
*/
|
|
5672
|
+
__publicField(this, "composing", false);
|
|
5660
5673
|
/**
|
|
5661
5674
|
* 편집 이력. 브라우저 기본 undo 는 우리가 직접 한 DOM 조작(블록 이동 · 표
|
|
5662
5675
|
* 편집 · 유형 변경) 을 모르기 때문에 자체 스택을 둔다.
|
|
@@ -5820,6 +5833,13 @@ class AerizEditor {
|
|
|
5820
5833
|
document.addEventListener("mouseup", this.handleDocumentMouseUp);
|
|
5821
5834
|
this.content.addEventListener("input", () => this.scheduleCodeHighlight());
|
|
5822
5835
|
this.content.addEventListener("input", (e) => this.handleInputRule(e));
|
|
5836
|
+
this.content.addEventListener("compositionstart", () => {
|
|
5837
|
+
this.composing = true;
|
|
5838
|
+
});
|
|
5839
|
+
this.content.addEventListener("compositionend", () => {
|
|
5840
|
+
this.composing = false;
|
|
5841
|
+
this.scheduleCodeHighlight();
|
|
5842
|
+
});
|
|
5823
5843
|
this.content.addEventListener("dragstart", (e) => {
|
|
5824
5844
|
const t2 = e.target;
|
|
5825
5845
|
if (!t2) return;
|
|
@@ -6486,6 +6506,7 @@ class AerizEditor {
|
|
|
6486
6506
|
}
|
|
6487
6507
|
this.codeHighlightTimer = window.setTimeout(() => {
|
|
6488
6508
|
this.codeHighlightTimer = null;
|
|
6509
|
+
if (this.composing) return;
|
|
6489
6510
|
const active = this.getActiveBlock();
|
|
6490
6511
|
if ((active == null ? void 0 : active.kind) !== "codeBlock") return;
|
|
6491
6512
|
const code = active.element.querySelector(":scope > code");
|
|
@@ -6515,7 +6536,7 @@ class AerizEditor {
|
|
|
6515
6536
|
* code 안에 있다면 textContent offset 으로 caret 위치 보존.
|
|
6516
6537
|
*/
|
|
6517
6538
|
rehighlightCode(code, langId) {
|
|
6518
|
-
const text = code
|
|
6539
|
+
const text = codeTextOf(code);
|
|
6519
6540
|
const sel = saveCodeSelection(code);
|
|
6520
6541
|
code.innerHTML = highlightCode(text, langId);
|
|
6521
6542
|
if (sel) restoreCodeSelection(code, sel);
|
|
@@ -7229,6 +7250,11 @@ class AerizEditor {
|
|
|
7229
7250
|
if (e.key === "Backspace" && !e.isComposing) {
|
|
7230
7251
|
if (this.handleBackspaceKey(e)) return;
|
|
7231
7252
|
}
|
|
7253
|
+
if (e.key === "Enter" && e.shiftKey && !e.isComposing) {
|
|
7254
|
+
const active2 = this.getActiveBlock();
|
|
7255
|
+
if ((active2 == null ? void 0 : active2.kind) === "codeBlock") this.insertCodeLineBreak(e, active2);
|
|
7256
|
+
return;
|
|
7257
|
+
}
|
|
7232
7258
|
if (e.key !== "Enter" || e.shiftKey || e.isComposing) return;
|
|
7233
7259
|
const sel = document.getSelection();
|
|
7234
7260
|
if (!sel || sel.rangeCount === 0 || !sel.isCollapsed) return;
|
|
@@ -7405,8 +7431,72 @@ class AerizEditor {
|
|
|
7405
7431
|
this.queueSync();
|
|
7406
7432
|
return true;
|
|
7407
7433
|
}
|
|
7434
|
+
if (active.kind === "codeBlock") {
|
|
7435
|
+
return this.indentCode(e, active, e.shiftKey);
|
|
7436
|
+
}
|
|
7408
7437
|
return false;
|
|
7409
7438
|
}
|
|
7439
|
+
/**
|
|
7440
|
+
* 코드 블록 들여쓰기 / 내어쓰기 (Tab · Shift+Tab).
|
|
7441
|
+
*
|
|
7442
|
+
* 코드를 쓰는 자리에서 Tab 이 포커스를 툴바로 옮겨 버리면 들여쓰기를 할
|
|
7443
|
+
* 방법이 없다. 다른 블록에서는 그 탈출구가 필요하지만(§7.5) 코드 블록은
|
|
7444
|
+
* 예외다.
|
|
7445
|
+
*
|
|
7446
|
+
* 선택 영역이 여러 줄에 걸치면 **걸친 줄 전체**를 옮긴다. 한 줄 안에 캐럿만
|
|
7447
|
+
* 있으면 그 자리에 들여쓰기를 넣는다 — 편집기들이 공통으로 하는 동작이다.
|
|
7448
|
+
*
|
|
7449
|
+
* 텍스트를 통째로 다시 쓰고 캐럿은 문자 오프셋으로 되돌린다. 하이라이트가
|
|
7450
|
+
* 이미 같은 방식으로 동작하므로 경로가 하나로 모인다.
|
|
7451
|
+
*/
|
|
7452
|
+
indentCode(e, active, outdent) {
|
|
7453
|
+
const code = active.element.querySelector(":scope > code");
|
|
7454
|
+
if (!code) return false;
|
|
7455
|
+
const offsets = captureSelectionOffsets(code) ?? { start: 0, end: 0 };
|
|
7456
|
+
const text = codeTextOf(code);
|
|
7457
|
+
const indent = _AerizEditor.CODE_INDENT;
|
|
7458
|
+
let next;
|
|
7459
|
+
let start;
|
|
7460
|
+
let end;
|
|
7461
|
+
if (!outdent && offsets.start === offsets.end) {
|
|
7462
|
+
next = text.slice(0, offsets.start) + indent + text.slice(offsets.start);
|
|
7463
|
+
start = end = offsets.start + indent.length;
|
|
7464
|
+
} else {
|
|
7465
|
+
const from = text.lastIndexOf("\n", offsets.start - 1) + 1;
|
|
7466
|
+
const toRaw = text.indexOf("\n", offsets.end);
|
|
7467
|
+
const to = toRaw === -1 ? text.length : toRaw;
|
|
7468
|
+
let firstDelta = 0;
|
|
7469
|
+
let totalDelta = 0;
|
|
7470
|
+
const moved = text.slice(from, to).split("\n").map((line, i) => {
|
|
7471
|
+
if (outdent) {
|
|
7472
|
+
const m = /^(\t| {1,2})/.exec(line);
|
|
7473
|
+
const removed = (m == null ? void 0 : m[0].length) ?? 0;
|
|
7474
|
+
if (i === 0) firstDelta = -removed;
|
|
7475
|
+
totalDelta -= removed;
|
|
7476
|
+
return line.slice(removed);
|
|
7477
|
+
}
|
|
7478
|
+
if (line === "") return line;
|
|
7479
|
+
if (i === 0) firstDelta = indent.length;
|
|
7480
|
+
totalDelta += indent.length;
|
|
7481
|
+
return indent + line;
|
|
7482
|
+
}).join("\n");
|
|
7483
|
+
if (totalDelta === 0) {
|
|
7484
|
+
e.preventDefault();
|
|
7485
|
+
return true;
|
|
7486
|
+
}
|
|
7487
|
+
next = text.slice(0, from) + moved + text.slice(to);
|
|
7488
|
+
start = Math.max(from, offsets.start + firstDelta);
|
|
7489
|
+
end = Math.max(start, offsets.end + totalDelta);
|
|
7490
|
+
}
|
|
7491
|
+
e.preventDefault();
|
|
7492
|
+
this.markStructuralChange();
|
|
7493
|
+
const langId = code.getAttribute("data-language") ?? void 0;
|
|
7494
|
+
code.innerHTML = highlightCode(next, langId);
|
|
7495
|
+
this.content.focus();
|
|
7496
|
+
restoreSelectionOffsets(code, { start, end });
|
|
7497
|
+
this.flush();
|
|
7498
|
+
return true;
|
|
7499
|
+
}
|
|
7410
7500
|
/**
|
|
7411
7501
|
* Backspace 처리. 처리했으면 `true`.
|
|
7412
7502
|
*
|
|
@@ -7432,6 +7522,43 @@ class AerizEditor {
|
|
|
7432
7522
|
placeCaretAtStart(p);
|
|
7433
7523
|
return true;
|
|
7434
7524
|
}
|
|
7525
|
+
/**
|
|
7526
|
+
* 코드 블록 안에 줄바꿈을 넣는다 (Shift+Enter).
|
|
7527
|
+
*
|
|
7528
|
+
* `<pre>` 는 개행 문자를 그대로 보존하므로 줄바꿈은 **문자 하나**여야 한다.
|
|
7529
|
+
* `<br>` 을 쓰면 `textContent` 가 그 자리를 빈 것으로 읽어, 하이라이트가
|
|
7530
|
+
* 다시 그려지는 순간(타이핑 멈춘 뒤 300ms) 줄이 사라진다.
|
|
7531
|
+
*
|
|
7532
|
+
* 맨 끝에서는 개행을 **하나 더** 넣는다. `<pre>` 는 마지막 개행 하나를
|
|
7533
|
+
* 접어 버려서, 하나만 넣으면 새 줄이 화면에 나타나지 않고 캐럿도 앞 줄에
|
|
7534
|
+
* 머문 것처럼 보인다. 뒤쪽 개행은 자리를 만드는 용도이고, 이어서 글자를
|
|
7535
|
+
* 치면 자연스럽게 그 줄의 내용이 된다.
|
|
7536
|
+
*/
|
|
7537
|
+
insertCodeLineBreak(e, active) {
|
|
7538
|
+
const code = active.element.querySelector(":scope > code");
|
|
7539
|
+
if (!code) return;
|
|
7540
|
+
const sel = document.getSelection();
|
|
7541
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
7542
|
+
e.preventDefault();
|
|
7543
|
+
if (!code.contains(sel.getRangeAt(0).commonAncestorContainer)) {
|
|
7544
|
+
placeCaretAtStart(active.element);
|
|
7545
|
+
}
|
|
7546
|
+
const range = sel.getRangeAt(0);
|
|
7547
|
+
if (!code.contains(range.commonAncestorContainer)) return;
|
|
7548
|
+
range.deleteContents();
|
|
7549
|
+
const after = document.createRange();
|
|
7550
|
+
after.selectNodeContents(code);
|
|
7551
|
+
after.setStart(range.endContainer, range.endOffset);
|
|
7552
|
+
const atEnd = after.toString() === "";
|
|
7553
|
+
const inserted = document.createTextNode(atEnd ? "\n\n" : "\n");
|
|
7554
|
+
range.insertNode(inserted);
|
|
7555
|
+
const caret = document.createRange();
|
|
7556
|
+
caret.setStart(inserted, 1);
|
|
7557
|
+
caret.collapse(true);
|
|
7558
|
+
sel.removeAllRanges();
|
|
7559
|
+
sel.addRange(caret);
|
|
7560
|
+
this.flush();
|
|
7561
|
+
}
|
|
7435
7562
|
/**
|
|
7436
7563
|
* 활성 블록 뒤에 새 빈 **본문 블록**을 만들고 캐럿을 그 안에 둔다.
|
|
7437
7564
|
* 인용구 / 코드 블록의 Enter 탈출, 제목 끝 Enter 에 사용.
|
|
@@ -7775,6 +7902,29 @@ class AerizEditor {
|
|
|
7775
7902
|
this.toolbar.setVisible("codeLang", (active == null ? void 0 : active.kind) === "codeBlock");
|
|
7776
7903
|
this.toolbar.syncGroupVisibility();
|
|
7777
7904
|
}
|
|
7905
|
+
};
|
|
7906
|
+
/** 코드 블록의 들여쓰기 한 단계. 공백 두 칸 — 이 저장소의 코드 스타일과 같다. */
|
|
7907
|
+
__publicField(_AerizEditor, "CODE_INDENT", " ");
|
|
7908
|
+
let AerizEditor = _AerizEditor;
|
|
7909
|
+
function codeTextOf(code) {
|
|
7910
|
+
var _a;
|
|
7911
|
+
if (code.childNodes.length === 1 && ((_a = code.firstChild) == null ? void 0 : _a.nodeName) === "BR") {
|
|
7912
|
+
return "";
|
|
7913
|
+
}
|
|
7914
|
+
let out = "";
|
|
7915
|
+
const walk = (node) => {
|
|
7916
|
+
for (const child of Array.from(node.childNodes)) {
|
|
7917
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
7918
|
+
out += child.textContent ?? "";
|
|
7919
|
+
} else if (child.nodeName === "BR") {
|
|
7920
|
+
out += "\n";
|
|
7921
|
+
} else {
|
|
7922
|
+
walk(child);
|
|
7923
|
+
}
|
|
7924
|
+
}
|
|
7925
|
+
};
|
|
7926
|
+
walk(code);
|
|
7927
|
+
return out;
|
|
7778
7928
|
}
|
|
7779
7929
|
function parseYouTubeId(input) {
|
|
7780
7930
|
const trimmed = input.trim();
|
|
@@ -2423,8 +2423,9 @@ const LANG_BY_ID = (() => {
|
|
|
2423
2423
|
for (const l of LANGUAGES) m.set(l.id, l);
|
|
2424
2424
|
return m;
|
|
2425
2425
|
})();
|
|
2426
|
+
const PLAIN_TEXT_ID = "plaintext";
|
|
2426
2427
|
function highlightCode(code, langId) {
|
|
2427
|
-
if (!langId) return escapeHtml$2(code);
|
|
2428
|
+
if (!langId || langId === PLAIN_TEXT_ID) return escapeHtml$2(code);
|
|
2428
2429
|
const lang = LANG_BY_ID.get(langId);
|
|
2429
2430
|
if (!lang) return escapeHtml$2(code);
|
|
2430
2431
|
if (lang.custom) return lang.custom(code);
|
|
@@ -3483,6 +3484,7 @@ const CODE_BLOCK = {
|
|
|
3483
3484
|
const pre = document.createElement("pre");
|
|
3484
3485
|
const code = document.createElement("code");
|
|
3485
3486
|
if (attrs == null ? void 0 : attrs.language) code.setAttribute("data-language", attrs.language);
|
|
3487
|
+
fillEmpty(code);
|
|
3486
3488
|
pre.appendChild(code);
|
|
3487
3489
|
return pre;
|
|
3488
3490
|
},
|
|
@@ -3988,9 +3990,15 @@ function normalizeContent(root, options = {}) {
|
|
|
3988
3990
|
fig.appendChild(makeResizeHandle());
|
|
3989
3991
|
}
|
|
3990
3992
|
}
|
|
3993
|
+
ensureCodeCarets(root);
|
|
3991
3994
|
dropEmptyCaptions(root);
|
|
3992
3995
|
if (options.writableTail !== false) ensureWritableTail(root);
|
|
3993
3996
|
}
|
|
3997
|
+
function ensureCodeCarets(root) {
|
|
3998
|
+
for (const code of Array.from(root.querySelectorAll("pre > code"))) {
|
|
3999
|
+
if (code.childNodes.length === 0) code.appendChild(document.createElement("br"));
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
3994
4002
|
function dropEmptyCaptions(root) {
|
|
3995
4003
|
var _a;
|
|
3996
4004
|
const captions = root.querySelectorAll("figure > figcaption");
|
|
@@ -5617,7 +5625,7 @@ const BLOCK_TOGGLE_MAP = {
|
|
|
5617
5625
|
};
|
|
5618
5626
|
const DRAG_MIME = "application/x-aeriz-block";
|
|
5619
5627
|
const MIN_BLOCK_WIDTH = 80;
|
|
5620
|
-
class
|
|
5628
|
+
const _AerizEditor = class _AerizEditor {
|
|
5621
5629
|
constructor(options) {
|
|
5622
5630
|
__publicField(this, "root");
|
|
5623
5631
|
__publicField(this, "content");
|
|
@@ -5655,6 +5663,11 @@ class AerizEditor {
|
|
|
5655
5663
|
__publicField(this, "hoverRaf", null);
|
|
5656
5664
|
/** 라벨 pill 의 등장 애니메이션을 블록이 **바뀔 때만** 다시 재생하기 위한 표식. */
|
|
5657
5665
|
__publicField(this, "pillBlockId", null);
|
|
5666
|
+
/**
|
|
5667
|
+
* IME 조합이 진행 중인지. 한글 · 일본어 · 중국어 입력은 글자 하나가 여러
|
|
5668
|
+
* 입력에 걸쳐 만들어지는데, 그 사이에 DOM 을 다시 쓰면 조합이 끊긴다.
|
|
5669
|
+
*/
|
|
5670
|
+
__publicField(this, "composing", false);
|
|
5658
5671
|
/**
|
|
5659
5672
|
* 편집 이력. 브라우저 기본 undo 는 우리가 직접 한 DOM 조작(블록 이동 · 표
|
|
5660
5673
|
* 편집 · 유형 변경) 을 모르기 때문에 자체 스택을 둔다.
|
|
@@ -5818,6 +5831,13 @@ class AerizEditor {
|
|
|
5818
5831
|
document.addEventListener("mouseup", this.handleDocumentMouseUp);
|
|
5819
5832
|
this.content.addEventListener("input", () => this.scheduleCodeHighlight());
|
|
5820
5833
|
this.content.addEventListener("input", (e) => this.handleInputRule(e));
|
|
5834
|
+
this.content.addEventListener("compositionstart", () => {
|
|
5835
|
+
this.composing = true;
|
|
5836
|
+
});
|
|
5837
|
+
this.content.addEventListener("compositionend", () => {
|
|
5838
|
+
this.composing = false;
|
|
5839
|
+
this.scheduleCodeHighlight();
|
|
5840
|
+
});
|
|
5821
5841
|
this.content.addEventListener("dragstart", (e) => {
|
|
5822
5842
|
const t2 = e.target;
|
|
5823
5843
|
if (!t2) return;
|
|
@@ -6484,6 +6504,7 @@ class AerizEditor {
|
|
|
6484
6504
|
}
|
|
6485
6505
|
this.codeHighlightTimer = window.setTimeout(() => {
|
|
6486
6506
|
this.codeHighlightTimer = null;
|
|
6507
|
+
if (this.composing) return;
|
|
6487
6508
|
const active = this.getActiveBlock();
|
|
6488
6509
|
if ((active == null ? void 0 : active.kind) !== "codeBlock") return;
|
|
6489
6510
|
const code = active.element.querySelector(":scope > code");
|
|
@@ -6513,7 +6534,7 @@ class AerizEditor {
|
|
|
6513
6534
|
* code 안에 있다면 textContent offset 으로 caret 위치 보존.
|
|
6514
6535
|
*/
|
|
6515
6536
|
rehighlightCode(code, langId) {
|
|
6516
|
-
const text = code
|
|
6537
|
+
const text = codeTextOf(code);
|
|
6517
6538
|
const sel = saveCodeSelection(code);
|
|
6518
6539
|
code.innerHTML = highlightCode(text, langId);
|
|
6519
6540
|
if (sel) restoreCodeSelection(code, sel);
|
|
@@ -7227,6 +7248,11 @@ class AerizEditor {
|
|
|
7227
7248
|
if (e.key === "Backspace" && !e.isComposing) {
|
|
7228
7249
|
if (this.handleBackspaceKey(e)) return;
|
|
7229
7250
|
}
|
|
7251
|
+
if (e.key === "Enter" && e.shiftKey && !e.isComposing) {
|
|
7252
|
+
const active2 = this.getActiveBlock();
|
|
7253
|
+
if ((active2 == null ? void 0 : active2.kind) === "codeBlock") this.insertCodeLineBreak(e, active2);
|
|
7254
|
+
return;
|
|
7255
|
+
}
|
|
7230
7256
|
if (e.key !== "Enter" || e.shiftKey || e.isComposing) return;
|
|
7231
7257
|
const sel = document.getSelection();
|
|
7232
7258
|
if (!sel || sel.rangeCount === 0 || !sel.isCollapsed) return;
|
|
@@ -7403,8 +7429,72 @@ class AerizEditor {
|
|
|
7403
7429
|
this.queueSync();
|
|
7404
7430
|
return true;
|
|
7405
7431
|
}
|
|
7432
|
+
if (active.kind === "codeBlock") {
|
|
7433
|
+
return this.indentCode(e, active, e.shiftKey);
|
|
7434
|
+
}
|
|
7406
7435
|
return false;
|
|
7407
7436
|
}
|
|
7437
|
+
/**
|
|
7438
|
+
* 코드 블록 들여쓰기 / 내어쓰기 (Tab · Shift+Tab).
|
|
7439
|
+
*
|
|
7440
|
+
* 코드를 쓰는 자리에서 Tab 이 포커스를 툴바로 옮겨 버리면 들여쓰기를 할
|
|
7441
|
+
* 방법이 없다. 다른 블록에서는 그 탈출구가 필요하지만(§7.5) 코드 블록은
|
|
7442
|
+
* 예외다.
|
|
7443
|
+
*
|
|
7444
|
+
* 선택 영역이 여러 줄에 걸치면 **걸친 줄 전체**를 옮긴다. 한 줄 안에 캐럿만
|
|
7445
|
+
* 있으면 그 자리에 들여쓰기를 넣는다 — 편집기들이 공통으로 하는 동작이다.
|
|
7446
|
+
*
|
|
7447
|
+
* 텍스트를 통째로 다시 쓰고 캐럿은 문자 오프셋으로 되돌린다. 하이라이트가
|
|
7448
|
+
* 이미 같은 방식으로 동작하므로 경로가 하나로 모인다.
|
|
7449
|
+
*/
|
|
7450
|
+
indentCode(e, active, outdent) {
|
|
7451
|
+
const code = active.element.querySelector(":scope > code");
|
|
7452
|
+
if (!code) return false;
|
|
7453
|
+
const offsets = captureSelectionOffsets(code) ?? { start: 0, end: 0 };
|
|
7454
|
+
const text = codeTextOf(code);
|
|
7455
|
+
const indent = _AerizEditor.CODE_INDENT;
|
|
7456
|
+
let next;
|
|
7457
|
+
let start;
|
|
7458
|
+
let end;
|
|
7459
|
+
if (!outdent && offsets.start === offsets.end) {
|
|
7460
|
+
next = text.slice(0, offsets.start) + indent + text.slice(offsets.start);
|
|
7461
|
+
start = end = offsets.start + indent.length;
|
|
7462
|
+
} else {
|
|
7463
|
+
const from = text.lastIndexOf("\n", offsets.start - 1) + 1;
|
|
7464
|
+
const toRaw = text.indexOf("\n", offsets.end);
|
|
7465
|
+
const to = toRaw === -1 ? text.length : toRaw;
|
|
7466
|
+
let firstDelta = 0;
|
|
7467
|
+
let totalDelta = 0;
|
|
7468
|
+
const moved = text.slice(from, to).split("\n").map((line, i) => {
|
|
7469
|
+
if (outdent) {
|
|
7470
|
+
const m = /^(\t| {1,2})/.exec(line);
|
|
7471
|
+
const removed = (m == null ? void 0 : m[0].length) ?? 0;
|
|
7472
|
+
if (i === 0) firstDelta = -removed;
|
|
7473
|
+
totalDelta -= removed;
|
|
7474
|
+
return line.slice(removed);
|
|
7475
|
+
}
|
|
7476
|
+
if (line === "") return line;
|
|
7477
|
+
if (i === 0) firstDelta = indent.length;
|
|
7478
|
+
totalDelta += indent.length;
|
|
7479
|
+
return indent + line;
|
|
7480
|
+
}).join("\n");
|
|
7481
|
+
if (totalDelta === 0) {
|
|
7482
|
+
e.preventDefault();
|
|
7483
|
+
return true;
|
|
7484
|
+
}
|
|
7485
|
+
next = text.slice(0, from) + moved + text.slice(to);
|
|
7486
|
+
start = Math.max(from, offsets.start + firstDelta);
|
|
7487
|
+
end = Math.max(start, offsets.end + totalDelta);
|
|
7488
|
+
}
|
|
7489
|
+
e.preventDefault();
|
|
7490
|
+
this.markStructuralChange();
|
|
7491
|
+
const langId = code.getAttribute("data-language") ?? void 0;
|
|
7492
|
+
code.innerHTML = highlightCode(next, langId);
|
|
7493
|
+
this.content.focus();
|
|
7494
|
+
restoreSelectionOffsets(code, { start, end });
|
|
7495
|
+
this.flush();
|
|
7496
|
+
return true;
|
|
7497
|
+
}
|
|
7408
7498
|
/**
|
|
7409
7499
|
* Backspace 처리. 처리했으면 `true`.
|
|
7410
7500
|
*
|
|
@@ -7430,6 +7520,43 @@ class AerizEditor {
|
|
|
7430
7520
|
placeCaretAtStart(p);
|
|
7431
7521
|
return true;
|
|
7432
7522
|
}
|
|
7523
|
+
/**
|
|
7524
|
+
* 코드 블록 안에 줄바꿈을 넣는다 (Shift+Enter).
|
|
7525
|
+
*
|
|
7526
|
+
* `<pre>` 는 개행 문자를 그대로 보존하므로 줄바꿈은 **문자 하나**여야 한다.
|
|
7527
|
+
* `<br>` 을 쓰면 `textContent` 가 그 자리를 빈 것으로 읽어, 하이라이트가
|
|
7528
|
+
* 다시 그려지는 순간(타이핑 멈춘 뒤 300ms) 줄이 사라진다.
|
|
7529
|
+
*
|
|
7530
|
+
* 맨 끝에서는 개행을 **하나 더** 넣는다. `<pre>` 는 마지막 개행 하나를
|
|
7531
|
+
* 접어 버려서, 하나만 넣으면 새 줄이 화면에 나타나지 않고 캐럿도 앞 줄에
|
|
7532
|
+
* 머문 것처럼 보인다. 뒤쪽 개행은 자리를 만드는 용도이고, 이어서 글자를
|
|
7533
|
+
* 치면 자연스럽게 그 줄의 내용이 된다.
|
|
7534
|
+
*/
|
|
7535
|
+
insertCodeLineBreak(e, active) {
|
|
7536
|
+
const code = active.element.querySelector(":scope > code");
|
|
7537
|
+
if (!code) return;
|
|
7538
|
+
const sel = document.getSelection();
|
|
7539
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
7540
|
+
e.preventDefault();
|
|
7541
|
+
if (!code.contains(sel.getRangeAt(0).commonAncestorContainer)) {
|
|
7542
|
+
placeCaretAtStart(active.element);
|
|
7543
|
+
}
|
|
7544
|
+
const range = sel.getRangeAt(0);
|
|
7545
|
+
if (!code.contains(range.commonAncestorContainer)) return;
|
|
7546
|
+
range.deleteContents();
|
|
7547
|
+
const after = document.createRange();
|
|
7548
|
+
after.selectNodeContents(code);
|
|
7549
|
+
after.setStart(range.endContainer, range.endOffset);
|
|
7550
|
+
const atEnd = after.toString() === "";
|
|
7551
|
+
const inserted = document.createTextNode(atEnd ? "\n\n" : "\n");
|
|
7552
|
+
range.insertNode(inserted);
|
|
7553
|
+
const caret = document.createRange();
|
|
7554
|
+
caret.setStart(inserted, 1);
|
|
7555
|
+
caret.collapse(true);
|
|
7556
|
+
sel.removeAllRanges();
|
|
7557
|
+
sel.addRange(caret);
|
|
7558
|
+
this.flush();
|
|
7559
|
+
}
|
|
7433
7560
|
/**
|
|
7434
7561
|
* 활성 블록 뒤에 새 빈 **본문 블록**을 만들고 캐럿을 그 안에 둔다.
|
|
7435
7562
|
* 인용구 / 코드 블록의 Enter 탈출, 제목 끝 Enter 에 사용.
|
|
@@ -7773,6 +7900,29 @@ class AerizEditor {
|
|
|
7773
7900
|
this.toolbar.setVisible("codeLang", (active == null ? void 0 : active.kind) === "codeBlock");
|
|
7774
7901
|
this.toolbar.syncGroupVisibility();
|
|
7775
7902
|
}
|
|
7903
|
+
};
|
|
7904
|
+
/** 코드 블록의 들여쓰기 한 단계. 공백 두 칸 — 이 저장소의 코드 스타일과 같다. */
|
|
7905
|
+
__publicField(_AerizEditor, "CODE_INDENT", " ");
|
|
7906
|
+
let AerizEditor = _AerizEditor;
|
|
7907
|
+
function codeTextOf(code) {
|
|
7908
|
+
var _a;
|
|
7909
|
+
if (code.childNodes.length === 1 && ((_a = code.firstChild) == null ? void 0 : _a.nodeName) === "BR") {
|
|
7910
|
+
return "";
|
|
7911
|
+
}
|
|
7912
|
+
let out = "";
|
|
7913
|
+
const walk = (node) => {
|
|
7914
|
+
for (const child of Array.from(node.childNodes)) {
|
|
7915
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
7916
|
+
out += child.textContent ?? "";
|
|
7917
|
+
} else if (child.nodeName === "BR") {
|
|
7918
|
+
out += "\n";
|
|
7919
|
+
} else {
|
|
7920
|
+
walk(child);
|
|
7921
|
+
}
|
|
7922
|
+
}
|
|
7923
|
+
};
|
|
7924
|
+
walk(code);
|
|
7925
|
+
return out;
|
|
7776
7926
|
}
|
|
7777
7927
|
function parseYouTubeId(input) {
|
|
7778
7928
|
const trimmed = input.trim();
|