deel-local-cli 1.20.12 → 2.0.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.ko.md +96 -77
- package/README.md +119 -80
- package/bin/deel.js +398 -32
- package/package.json +2 -1
- package/src/acp/jsonrpc.js +87 -7
- package/src/acp/map.js +62 -13
- package/src/acp/serve.js +338 -30
- package/src/agent/agents.js +110 -12
- package/src/agent/askcheck.js +50 -6
- package/src/agent/asks.js +59 -5
- package/src/agent/budget.js +9 -2
- package/src/agent/card.js +8 -2
- package/src/agent/commit.js +168 -16
- package/src/agent/compact.js +93 -21
- package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
- package/src/agent/effort.js +85 -15
- package/src/agent/evidence.js +63 -7
- package/src/agent/evolve.js +132 -24
- package/src/agent/grade.js +12 -3
- package/src/agent/loop.js +408 -81
- package/src/agent/memory.js +176 -29
- package/src/agent/mention.js +22 -5
- package/src/agent/models.js +31 -7
- package/src/agent/modes.js +25 -10
- package/src/agent/outschema.js +381 -56
- package/src/agent/pins.js +33 -2
- package/src/agent/project.js +73 -8
- package/src/agent/recall.js +40 -9
- package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
- package/src/agent/review.js +143 -13
- package/src/agent/route.js +506 -32
- package/src/agent/salvage.js +2 -1
- package/src/agent/session.js +457 -31
- package/src/agent/store.js +232 -22
- package/src/agent/threads.js +52 -3
- package/src/backend/adapter.js +508 -85
- package/src/backend/azure.js +12 -1
- package/src/backend/cachemark.js +31 -5
- package/src/backend/clientcert.js +23 -1
- package/src/backend/ctxsize.js +56 -9
- package/src/backend/detect.js +252 -19
- package/src/backend/http.js +383 -42
- package/src/backend/learn.js +101 -11
- package/src/backend/mcp.js +333 -24
- package/src/backend/price.js +61 -13
- package/src/backend/probe.js +188 -36
- package/src/backend/proxy.js +108 -14
- package/src/backend/quota.js +226 -30
- package/src/backend/retry.js +46 -3
- package/src/backend/scan.js +47 -10
- package/src/backend/scanui.js +57 -7
- package/src/backend/toolfit.js +204 -22
- package/src/backend/vision.js +14 -1
- package/src/backend/wire.js +58 -10
- package/src/cmdnames.js +16 -0
- package/src/commands/common.js +70 -0
- package/src/commands/extend.js +300 -0
- package/src/commands/model.js +852 -0
- package/src/commands/view.js +328 -0
- package/src/commands/work.js +823 -0
- package/src/commands.js +193 -2082
- package/src/completion.js +45 -3
- package/src/config.js +452 -39
- package/src/configexplain.js +125 -14
- package/src/doctor.js +164 -26
- package/src/i18n/en.js +22 -0
- package/src/i18n/index.js +31 -1
- package/src/i18n/ja.js +25 -1
- package/src/i18n/ko.js +25 -2
- package/src/i18n/zh.js +25 -1
- package/src/lsp/client.js +111 -9
- package/src/lsp/servers.js +86 -22
- package/src/oneshot.js +325 -65
- package/src/pack/sbom.js +28 -5
- package/src/pack/selfpack.js +54 -5
- package/src/pack/sheet.en.js +24 -3
- package/src/pack/tar.js +45 -8
- package/src/pack/zip.js +103 -8
- package/src/plugins/manage.js +165 -14
- package/src/preview/serve.js +239 -23
- package/src/providers/bedrock.js +30 -5
- package/src/providers/gemini.js +8 -0
- package/src/providers/index.js +17 -1
- package/src/repl.js +254 -61
- package/src/report.js +7 -2
- package/src/reset.js +151 -25
- package/src/safety/audit.js +108 -8
- package/src/safety/authcmd.js +75 -5
- package/src/safety/guard.js +507 -15
- package/src/safety/hooks.js +172 -20
- package/src/safety/keystore.js +178 -18
- package/src/safety/network.js +209 -6
- package/src/safety/policy.js +377 -36
- package/src/safety/runmode.js +9 -2
- package/src/safety/secrets.js +243 -17
- package/src/safety/shellenv.js +64 -3
- package/src/safety/trust.js +298 -32
- package/src/safety/undo.js +409 -17
- package/src/setup.js +175 -17
- package/src/skills/discover.js +122 -19
- package/src/stats.js +57 -16
- package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
- package/src/tools/clipboard.js +53 -20
- package/src/tools/convert.js +118 -37
- package/src/tools/desc.en.js +48 -5
- package/src/tools/doc2md.js +11 -1
- package/src/tools/docs.js +94 -18
- package/src/tools/edit-match.js +212 -13
- package/src/tools/encoding.js +525 -28
- package/src/tools/excel-com.js +63 -15
- package/src/tools/excel.js +12 -7
- package/src/tools/fastgrep.js +444 -36
- package/src/tools/fig.js +40 -9
- package/src/tools/fsutil.js +235 -21
- package/src/tools/hwpxwrite.js +80 -19
- package/src/tools/ignore.js +55 -7
- package/src/tools/index.js +1029 -145
- package/src/tools/jobs.js +325 -50
- package/src/tools/kiwi.js +50 -5
- package/src/tools/lsp.js +164 -32
- package/src/tools/outline.js +52 -12
- package/src/tools/pdf.js +366 -56
- package/src/tools/shell.js +12 -4
- package/src/tools/spawn.js +203 -18
- package/src/tools/todo.js +73 -4
- package/src/tools/verify.js +312 -43
- package/src/tools/webfetch.js +184 -25
- package/src/tools/xlsx.js +232 -31
- package/src/ui/ansi.js +209 -15
- package/src/ui/banner.js +2 -1
- package/src/ui/complete.js +46 -4
- package/src/ui/diff.js +4 -2
- package/src/ui/export.js +91 -17
- package/src/ui/inputbox.js +58 -18
- package/src/ui/intro.js +9 -2
- package/src/ui/level.js +32 -7
- package/src/ui/md.js +101 -15
- package/src/ui/motion.js +5 -2
- package/src/ui/notify.js +7 -1
- package/src/ui/office.js +37 -5
- package/src/ui/pastechip.js +3 -2
- package/src/ui/pick.js +85 -12
- package/src/ui/prompt.js +176 -13
- package/src/ui/screen.js +2 -1
- package/src/ui/spinner.js +6 -1
- package/src/ui/status.js +80 -12
- package/src/ui/wrap.js +23 -37
- /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
- /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
- /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
- /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
package/src/tools/xlsx.js
CHANGED
|
@@ -25,7 +25,8 @@ const 되돌림 = { lt: '<', gt: '>', amp: '&', quot: '"', apos: "'" };
|
|
|
25
25
|
|
|
26
26
|
export function unescapeXml(s) {
|
|
27
27
|
if (!s.includes('&')) return s;
|
|
28
|
-
|
|
28
|
+
// 16진 참조의 x 는 대문자도 된다(XML 규격). 소문자만 봐서 `A` 이 글자째 남았다 (2.0.0 6회차 엑셀6).
|
|
29
|
+
return s.replace(/&(#[xX]?[0-9a-fA-F]+|[a-z]+);/g, (전체, 안) => {
|
|
29
30
|
if (안[0] === '#') {
|
|
30
31
|
const n = 안[1] === 'x' || 안[1] === 'X' ? parseInt(안.slice(2), 16) : parseInt(안.slice(1), 10);
|
|
31
32
|
return Number.isFinite(n) && n >= 0 && n <= 0x10ffff ? String.fromCodePoint(n) : 전체;
|
|
@@ -51,7 +52,7 @@ export function* tags(xml) {
|
|
|
51
52
|
i = 끝 < 0 ? xml.length : 끝 + 1;
|
|
52
53
|
continue;
|
|
53
54
|
}
|
|
54
|
-
const 닫힘 = xml
|
|
55
|
+
const 닫힘 = 태그끝(xml, 열림);
|
|
55
56
|
if (닫힘 < 0) break;
|
|
56
57
|
let 안 = xml.slice(열림 + 1, 닫힘);
|
|
57
58
|
const closing = 안[0] === '/';
|
|
@@ -59,13 +60,42 @@ export function* tags(xml) {
|
|
|
59
60
|
const selfClosing = 안.endsWith('/');
|
|
60
61
|
if (selfClosing) 안 = 안.slice(0, -1);
|
|
61
62
|
const 빈칸 = 안.search(/[\s]/);
|
|
62
|
-
const
|
|
63
|
+
const 온이름 = 빈칸 < 0 ? 안 : 안.slice(0, 빈칸);
|
|
64
|
+
/*
|
|
65
|
+
* 이름의 접두사(`x:worksheet`)는 뗀다 (2.0.0 6회차 엑셀6). 기본 이름공간을 접두사로 적는 도구가 있고,
|
|
66
|
+
* 이름을 글자 그대로 견줬더니 `sheet` 를 하나도 못 찾아 「읽을 수 있는 시트가 없습니다」 로 끝났다.
|
|
67
|
+
* 문서 읽기(docs.js)도 이 함수를 쓰는데 거기는 이미 끝이름 으로 떼고 본다. 속성 이름(`r:id`)은 그대로 —
|
|
68
|
+
* 부르는 쪽이 접두사째 찾는다.
|
|
69
|
+
*/
|
|
70
|
+
const name = 온이름.slice(온이름.indexOf(':') + 1);
|
|
63
71
|
const attrs = 빈칸 < 0 ? {} : 속성(안.slice(빈칸));
|
|
64
72
|
yield { name, attrs, closing, selfClosing };
|
|
65
73
|
i = 닫힘 + 1;
|
|
66
74
|
}
|
|
67
75
|
}
|
|
68
76
|
|
|
77
|
+
/*
|
|
78
|
+
* 태그를 닫는 `>` 자리. 속성 값 따옴표 안의 `>` 는 건너뛴다 (2.0.0 6회차 엑셀6).
|
|
79
|
+
*
|
|
80
|
+
* XML 은 속성 값에 `>` 를 날로 적어도 되고, 엑셀 밖 도구는 `formatCode="[>100]0.0"` 처럼 적는다.
|
|
81
|
+
* 첫 `>` 에서 끊었더니 서식 문자열이 사라져 날짜 칸이 숫자로 나왔다. 따옴표가 없는 태그는 indexOf
|
|
82
|
+
* 한 번이다. 따옴표가 끝내 안 닫히는 망가진 태그는 옛날처럼 첫 `>` 에서 끊는다 — 뒤를 통째로 삼키지 않게.
|
|
83
|
+
*/
|
|
84
|
+
function 태그끝(xml, 열림) {
|
|
85
|
+
const 첫 = xml.indexOf('>', 열림);
|
|
86
|
+
if (첫 < 0) return -1;
|
|
87
|
+
const 앞 = xml.slice(열림, 첫);
|
|
88
|
+
if (!앞.includes('"') && !앞.includes("'")) return 첫;
|
|
89
|
+
let 따옴 = null;
|
|
90
|
+
for (let j = 열림 + 1; j < xml.length; j++) {
|
|
91
|
+
const ch = xml[j];
|
|
92
|
+
if (따옴) { if (ch === 따옴) 따옴 = null; continue; }
|
|
93
|
+
if (ch === '>') return j;
|
|
94
|
+
if (ch === '"' || ch === "'") 따옴 = ch;
|
|
95
|
+
}
|
|
96
|
+
return 첫;
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
function 속성(s) {
|
|
70
100
|
const out = {};
|
|
71
101
|
const re = /([\w:.-]+)\s*=\s*"([^"]*)"|([\w:.-]+)\s*=\s*'([^']*)'/g;
|
|
@@ -76,7 +106,7 @@ function 속성(s) {
|
|
|
76
106
|
|
|
77
107
|
// ── 셀 주소 ─────────────────────────────────────────────────────────────
|
|
78
108
|
|
|
79
|
-
/** 'BC12' → { col:
|
|
109
|
+
/** 'BC12' → { col: 55, row: 12 } (col 은 1부터. BC = 2×26+3 이다 — 54 는 BB) */
|
|
80
110
|
export function cellRef(ref) {
|
|
81
111
|
let col = 0;
|
|
82
112
|
let i = 0;
|
|
@@ -117,7 +147,17 @@ function 시트목록(workbookXml, rels) {
|
|
|
117
147
|
const out = [];
|
|
118
148
|
for (const n of tags(workbookXml)) {
|
|
119
149
|
if (n.name === 'sheet' && !n.closing) {
|
|
120
|
-
|
|
150
|
+
/*
|
|
151
|
+
* 관계 열쇠는 **접두사를 떼고** 찾는다 (막판 훑기).
|
|
152
|
+
*
|
|
153
|
+
* 태그 이름의 접두사는 위에서 뗐는데(tags), 여기만 `r:id` 를 글자 그대로 찾았다.
|
|
154
|
+
* 접두사 이름은 규격이 정해 주는 것이 아니라 그 파일이 정하는 별명이라,
|
|
155
|
+
* `xmlns:rel="…/relationships"` 로 적는 도구가 만든 통합문서는 접두사를 뗀 고침
|
|
156
|
+
* 뒤에도 똑같이 「읽을 수 있는 시트가 없습니다」 로 죽었다. 반쪽만 고쳐져 있었다.
|
|
157
|
+
*/
|
|
158
|
+
const rid = n.attrs['r:id']
|
|
159
|
+
?? Object.entries(n.attrs).find(([k]) => k.includes(':') && k.slice(k.indexOf(':') + 1) === 'id')?.[1]
|
|
160
|
+
?? n.attrs.id;
|
|
121
161
|
out.push({
|
|
122
162
|
name: n.attrs.name ?? `시트${out.length + 1}`,
|
|
123
163
|
// state="hidden" 인 시트도 담는다. 숨겨진 데 진짜 값이 있는 경우가 있다.
|
|
@@ -154,8 +194,13 @@ function 날짜스타일(stylesXml) {
|
|
|
154
194
|
const code = n.attrs.formatCode ?? '';
|
|
155
195
|
// 서식 문자열에 연·월·일·시가 들어 있으면 날짜로 본다.
|
|
156
196
|
// 따옴표 안의 글자는 그냥 붙는 말이라 빼고 본다.
|
|
197
|
+
//
|
|
198
|
+
// 여기 「그러면서 숫자 서식은 아닌 것」(`!/^[#0.,%\s]*$/`)이 뒤에 붙어 있었다.
|
|
199
|
+
// 앞이 참이면 y·m·d·h·s 중 한 글자가 남아 있다는 뜻이라 뒤는 **늘** 참이다 —
|
|
200
|
+
// 무작정 다 돌려 보니 앞이 참인 1,560,474건 중 걸린 것이 0건이었다. 지키는
|
|
201
|
+
// 척만 하는 조건은 다음 사람이 그것을 믿고 진짜 조건을 빼게 만든다.
|
|
157
202
|
const 순수 = code.replace(/"[^"]*"/g, '').replace(/\[[^\]]*\]/g, '');
|
|
158
|
-
if (/[ymdhs]/i.test(순수)
|
|
203
|
+
if (/[ymdhs]/i.test(순수)) 날짜Fmt.add(id);
|
|
159
204
|
}
|
|
160
205
|
}
|
|
161
206
|
// cellXfs 의 순서가 곧 셀의 s= 값이다.
|
|
@@ -182,23 +227,84 @@ function 날짜로(n) {
|
|
|
182
227
|
return 날 + 시;
|
|
183
228
|
}
|
|
184
229
|
|
|
230
|
+
/*
|
|
231
|
+
* 엑셀이 담을 수 있는 가장 큰 표 — 2007 판부터 1,048,576행 × 16,384열(XFD).
|
|
232
|
+
* 이 밖을 가리키는 주소는 엑셀이 만든 파일에서 나올 수 없다. 나왔다면 누가
|
|
233
|
+
* 손으로 적은 바이트다.
|
|
234
|
+
*/
|
|
235
|
+
export const 엑셀최대행 = 1048576;
|
|
236
|
+
export const 엑셀최대열 = 16384;
|
|
237
|
+
/** 빈 줄·빈 열이 이만큼 넘게 이어지면 자리를 안 지키고 건너뛰었다고 말한다. */
|
|
238
|
+
export const 빈틈상한 = 1000;
|
|
239
|
+
/** 펼친 표(줄 × 폭)의 칸 수 상한. 넘는 아래쪽 줄은 빼고 뺐다고 말한다. */
|
|
240
|
+
export const 칸수상한 = 1_000_000;
|
|
241
|
+
|
|
242
|
+
const 천단위 = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
243
|
+
|
|
244
|
+
/** 1 → 'A', 27 → 'AA', 16384 → 'XFD' */
|
|
245
|
+
function 열이름(n) {
|
|
246
|
+
let s = '';
|
|
247
|
+
while (n > 0) {
|
|
248
|
+
const r = (n - 1) % 26;
|
|
249
|
+
s = String.fromCharCode(65 + r) + s;
|
|
250
|
+
n = Math.floor((n - 1) / 26);
|
|
251
|
+
}
|
|
252
|
+
return s;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** 건너뛴 범위 목록을 말로. 너무 많으면 앞의 셋만 적고 나머지는 곳 수만. */
|
|
256
|
+
function 범위말(목록, 꼴) {
|
|
257
|
+
const 앞 = 목록.slice(0, 3).map(꼴).join(', ');
|
|
258
|
+
return 목록.length > 3 ? `${앞} 외 ${천단위(목록.length - 3)}곳` : 앞;
|
|
259
|
+
}
|
|
260
|
+
|
|
185
261
|
function 시트읽기(xml, { shared, 날짜여부 }) {
|
|
186
|
-
|
|
262
|
+
/*
|
|
263
|
+
* ── 값이 있는 칸만 모으고, 펼치기는 맨 끝에 한 번 ─────────────────────
|
|
264
|
+
*
|
|
265
|
+
* 전에는 읽는 대로 줄 배열을 그 번호까지 빈 줄로, 칸 배열을 그 열까지 빈 칸으로
|
|
266
|
+
* 채웠다. 그러면 메모리가 **파일 크기가 아니라 주소가 우기는 번호**를 따라간다.
|
|
267
|
+
* 1KB 도 안 되는 파일의 `<row r="80000000">` 한 줄에 빈 배열 팔천만 개,
|
|
268
|
+
* `<c r="AAAAAA1">` 한 칸에 빈 칸 천이백만 개 — 힙이 바닥나 프로세스가 죽었다
|
|
269
|
+
* (2.0.0 4회차 사냥). 엑셀 파일은 남이 보낸 바이트다.
|
|
270
|
+
*
|
|
271
|
+
* 그래서 읽는 동안은 **값이 있는 칸만** 담는다 — 메모리가 파일 크기를 따른다.
|
|
272
|
+
* 펼칠 때(표펼치기) 세 가지를 막고, 막은 것은 전부 말로 남긴다. 조용히 줄이면
|
|
273
|
+
* 표가 멀쩡해 보여서 사람은 무엇이 빠졌는지 모른다.
|
|
274
|
+
*/
|
|
275
|
+
const 줄들 = []; // { 번호, 칸: Map<열, 값>, 끝열 } — 번호 오름차순
|
|
187
276
|
let 지금줄 = null;
|
|
188
|
-
let
|
|
277
|
+
let 앞줄번호 = 0;
|
|
189
278
|
let 셀 = null;
|
|
190
279
|
let 안에 = null; // 'v' | 't' | null
|
|
191
280
|
let 모은글 = '';
|
|
192
|
-
let
|
|
281
|
+
let 읽기표깊이 = 0; // <rPh> 안 — 인라인 문자열의 읽기(후리가나)라 본문이 아니다
|
|
282
|
+
let 한계밖 = 0;
|
|
283
|
+
|
|
284
|
+
const 줄열기 = (r) => {
|
|
285
|
+
const n = Number(r);
|
|
286
|
+
// 엑셀은 줄을 오름차순으로 적는다. 번호가 없거나 뒷걸음치면 앞 줄 바로 다음으로
|
|
287
|
+
// 본다 — 전에도 그런 줄은 뒤에 이어 붙였다.
|
|
288
|
+
const 번호 = Number.isInteger(n) && n > 앞줄번호 ? n : 앞줄번호 + 1;
|
|
289
|
+
지금줄 = { 번호, 칸: new Map(), 끝열: 0, 앞칸열: 0, 밖: 번호 > 엑셀최대행 };
|
|
290
|
+
};
|
|
291
|
+
const 줄닫기 = () => {
|
|
292
|
+
셀마감();
|
|
293
|
+
if (지금줄 && !지금줄.밖) { 줄들.push(지금줄); 앞줄번호 = 지금줄.번호; }
|
|
294
|
+
지금줄 = null;
|
|
295
|
+
};
|
|
193
296
|
|
|
194
297
|
const 셀마감 = () => {
|
|
195
298
|
if (!셀) return;
|
|
196
299
|
let 값 = 모은글;
|
|
197
300
|
if (셀.t === 's') {
|
|
198
|
-
|
|
301
|
+
// 값 없는 칸(`<c t="s"></c>` · `<v></v>`)은 빈 칸이다. Number('') 가 0 이라 0번 공용 글이 들어갔다 (2.0.0 6회차 엑셀6).
|
|
302
|
+
const i = 값.trim() === '' ? NaN : Number(값);
|
|
199
303
|
값 = Number.isInteger(i) && i >= 0 && i < shared.length ? shared[i] : '';
|
|
200
304
|
} else if (셀.t === 'b') {
|
|
201
|
-
|
|
305
|
+
// 규격은 1·0 인데 `true`·`false` 로 적는 도구가 있다. 값 없는 불리언은 FALSE 가 아니라 빈 칸 (2.0.0 6회차 엑셀6).
|
|
306
|
+
const 글 = 값.trim().toLowerCase();
|
|
307
|
+
값 = 글 === '' ? '' : (글 === '1' || 글 === 'true' ? 'TRUE' : 'FALSE');
|
|
202
308
|
} else if (셀.t === 'e') {
|
|
203
309
|
// #REF! 같은 오류값. 지우면 왜 비었는지 알 수 없다.
|
|
204
310
|
값 = 값 || '#오류';
|
|
@@ -208,35 +314,41 @@ function 시트읽기(xml, { shared, 날짜여부 }) {
|
|
|
208
314
|
값 = 날짜로(Number(값)) ?? 값;
|
|
209
315
|
}
|
|
210
316
|
if (값 !== '') {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
317
|
+
// <row> 밖에 떨어진 칸 — 망가진 파일이다. 죽지 않고 앞 줄 다음 줄로 본다.
|
|
318
|
+
if (!지금줄) 줄열기(null);
|
|
319
|
+
// 엑셀 한계 밖 주소는 담지 않고 센다. 센 것은 말로 남긴다(아래).
|
|
320
|
+
if (지금줄.밖 || 셀.col > 엑셀최대열) 한계밖 += 1;
|
|
321
|
+
else {
|
|
322
|
+
지금줄.칸.set(셀.col, 값);
|
|
323
|
+
if (셀.col > 지금줄.끝열) 지금줄.끝열 = 셀.col;
|
|
324
|
+
}
|
|
214
325
|
}
|
|
215
326
|
셀 = null;
|
|
216
327
|
모은글 = '';
|
|
217
328
|
};
|
|
218
329
|
|
|
219
330
|
for (const n of tags(xml)) {
|
|
220
|
-
if (n.text !== undefined) { if (안에) 모은글 += unescapeXml(n.text); continue; }
|
|
331
|
+
if (n.text !== undefined) { if (안에 && !읽기표깊이) 모은글 += unescapeXml(n.text); continue; }
|
|
332
|
+
// 인라인 문자열의 <rPh> 는 공용문자열 쪽처럼 본문에 안 붙인다 — 붙였더니 `東京トウキョウ` 가 됐다 (2.0.0 6회차 엑셀6).
|
|
333
|
+
if (n.name === 'rPh') { 읽기표깊이 = Math.max(0, 읽기표깊이 + (n.closing ? -1 : (n.selfClosing ? 0 : 1))); continue; }
|
|
221
334
|
if (n.name === 'row') {
|
|
222
|
-
if (n.closing)
|
|
223
|
-
|
|
224
|
-
// 건너뛴 빈 줄도 자리를 지킨다. 안 그러면 행 번호가 밀린다.
|
|
225
|
-
while (rows.length < 줄번호 - 1) rows.push([]);
|
|
226
|
-
rows.push(지금줄 ?? []);
|
|
227
|
-
지금줄 = null;
|
|
228
|
-
} else if (!n.selfClosing) {
|
|
229
|
-
지금줄 = [];
|
|
230
|
-
줄번호 = Number(n.attrs.r) || rows.length + 1;
|
|
231
|
-
}
|
|
335
|
+
if (n.closing) 줄닫기();
|
|
336
|
+
else if (!n.selfClosing) { 줄닫기(); 줄열기(n.attrs.r); }
|
|
232
337
|
continue;
|
|
233
338
|
}
|
|
234
339
|
if (n.name === 'c') {
|
|
235
340
|
if (n.closing) { 셀마감(); continue; }
|
|
236
341
|
셀마감();
|
|
237
|
-
|
|
342
|
+
/*
|
|
343
|
+
* 주소 없는 칸은 **앞 칸 다음 열**이다 — 앞 칸에 값이 있었든 없었든.
|
|
344
|
+
* 「값이 들어간 마지막 열 + 1」 로 세면 `<c/>` · 값 없는 `<c s="2"></c>` 가 낀 자리가
|
|
345
|
+
* 안 세어져 뒤 칸이 한 칸씩 왼쪽으로 당겨졌다 (4회차 Gemini 리뷰). 끝열은 폭을 재는 데만 쓴다.
|
|
346
|
+
*/
|
|
347
|
+
const { col } = n.attrs.r ? cellRef(n.attrs.r) : { col: (지금줄?.앞칸열 ?? 0) + 1 };
|
|
238
348
|
셀 = { col: col || 1, t: n.attrs.t ?? null, s: Number(n.attrs.s ?? 0) };
|
|
349
|
+
if (지금줄) 지금줄.앞칸열 = 셀.col;
|
|
239
350
|
모은글 = '';
|
|
351
|
+
읽기표깊이 = 0; // 안 닫힌 <rPh> 가 다음 칸까지 삼키지 않게
|
|
240
352
|
if (n.selfClosing) { 셀 = null; } // <c r="A1"/> — 빈 칸
|
|
241
353
|
continue;
|
|
242
354
|
}
|
|
@@ -247,11 +359,97 @@ function 시트읽기(xml, { shared, 날짜여부 }) {
|
|
|
247
359
|
// <f> 는 수식이다. 값은 <v> 에 따로 들어 있으므로 수식 자체는 안 읽는다.
|
|
248
360
|
if (n.name === 'f' && !n.closing) 안에 = null;
|
|
249
361
|
}
|
|
250
|
-
|
|
251
|
-
if (지금줄) rows.push(지금줄);
|
|
362
|
+
줄닫기();
|
|
252
363
|
|
|
253
|
-
|
|
254
|
-
|
|
364
|
+
const 말 = [];
|
|
365
|
+
if (한계밖) {
|
|
366
|
+
말.push(`엑셀 한계(${천단위(엑셀최대행)}행 × ${천단위(엑셀최대열)}열) 밖 주소를 가리키는 칸 ${천단위(한계밖)}개는 뺐습니다`
|
|
367
|
+
+ ' — 엑셀이 만든 파일에서는 나올 수 없는 주소입니다');
|
|
368
|
+
}
|
|
369
|
+
return { rows: 표펼치기(줄들, 말), 말 };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 값이 있는 칸만 모은 줄들을 사람이 읽는 네모난 표(string[][])로 편다.
|
|
374
|
+
*
|
|
375
|
+
* 막는 것 셋 — 모두 **주소가 우기는 번호**만큼 메모리를 쓰게 두지 않으려는 것이고,
|
|
376
|
+
* 막은 것은 전부 `말` 에 적는다.
|
|
377
|
+
*
|
|
378
|
+
* 1. 빈 줄이 빈틈상한 넘게 이어지면 자리를 안 지키고 건너뛴다. 백만째 줄의 한 칸
|
|
379
|
+
* 때문에 빈 줄 백만 개를 내놓아 봐야 읽을 사람이 없다. 좁은 틈은 전처럼 지킨다
|
|
380
|
+
* — 안 그러면 행 번호가 밀린다. 넓은 틈은 몇 행부터 몇 행까지 건너뛰었고
|
|
381
|
+
* 다음 값이 몇 행인지 적는다. 표에서 그 아래 줄이 당겨져 보이기 때문이다.
|
|
382
|
+
* 2. 빈 열도 같다. XFD 열의 한 칸 때문에 줄마다 빈 칸 만육천 개를 붙이지 않는다.
|
|
383
|
+
* 3. 틈이 좁아도 칸이 대각선으로 흩어지면 줄 × 폭 이 곱해서 커진다. 펼친 칸 수가
|
|
384
|
+
* 칸수상한을 넘으면 아래쪽 줄을 빼고, 몇 줄을 몇 행부터 뺐는지 적는다.
|
|
385
|
+
*/
|
|
386
|
+
function 표펼치기(줄들, 말) {
|
|
387
|
+
// ── 줄: 값 있는 줄 사이의 틈을 재서 좁으면 빈 줄로 채우고, 넓으면 건너뛴다 ──
|
|
388
|
+
const 마디 = []; // { 번호, 줄 } 또는 { 번호, 빈: 개수 }
|
|
389
|
+
const 건너뛴줄 = []; // [처음, 끝]
|
|
390
|
+
const 마지막번호 = 줄들.length ? 줄들[줄들.length - 1].번호 : 0;
|
|
391
|
+
let 앞 = 0;
|
|
392
|
+
const 틈 = (다음) => { // 앞+1 ~ 다음-1
|
|
393
|
+
const k = 다음 - 앞 - 1;
|
|
394
|
+
if (k <= 0) return;
|
|
395
|
+
if (k > 빈틈상한) 건너뛴줄.push([앞 + 1, 다음 - 1]);
|
|
396
|
+
else 마디.push({ 번호: 앞 + 1, 빈: k });
|
|
397
|
+
};
|
|
398
|
+
const 쓰인열 = new Set();
|
|
399
|
+
for (const z of 줄들) {
|
|
400
|
+
// 값 없는 줄(서식만 있는 줄)은 틈으로 센다. 끝자리 계산에만 번호가 쓰인다.
|
|
401
|
+
if (!z.칸.size) continue;
|
|
402
|
+
틈(z.번호);
|
|
403
|
+
마디.push({ 번호: z.번호, 줄: z });
|
|
404
|
+
for (const c of z.칸.keys()) 쓰인열.add(c);
|
|
405
|
+
앞 = z.번호;
|
|
406
|
+
}
|
|
407
|
+
틈(마지막번호 + 1);
|
|
408
|
+
if (건너뛴줄.length) {
|
|
409
|
+
const 합 = 건너뛴줄.reduce((s, [a, b]) => s + (b - a + 1), 0);
|
|
410
|
+
말.push(`빈 줄 ${천단위(합)}개를 건너뛰었습니다 (${범위말(건너뛴줄, ([a, b]) => (b < 마지막번호
|
|
411
|
+
? `${천단위(a)}~${천단위(b)}행 → ${천단위(b + 1)}행부터 이어짐`
|
|
412
|
+
: `${천단위(a)}~${천단위(b)}행 · 시트 끝`))}) — 표의 아래쪽 줄은 그만큼 당겨져 있습니다`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── 열: 쓰인 열 사이의 틈도 같은 식으로. 열 번호 → 펼친 표의 칸 자리 ──
|
|
416
|
+
const 자리 = new Map();
|
|
417
|
+
const 건너뛴열 = [];
|
|
418
|
+
let 폭 = 0;
|
|
419
|
+
let 앞열 = 0;
|
|
420
|
+
for (const c of [...쓰인열].sort((a, b) => a - b)) {
|
|
421
|
+
const k = c - 앞열 - 1;
|
|
422
|
+
if (k > 빈틈상한) 건너뛴열.push([앞열 + 1, c - 1]);
|
|
423
|
+
else 폭 += k;
|
|
424
|
+
자리.set(c, 폭);
|
|
425
|
+
폭 += 1;
|
|
426
|
+
앞열 = c;
|
|
427
|
+
}
|
|
428
|
+
if (건너뛴열.length) {
|
|
429
|
+
const 합 = 건너뛴열.reduce((s, [a, b]) => s + (b - a + 1), 0);
|
|
430
|
+
말.push(`빈 열 ${천단위(합)}개를 건너뛰었습니다 (${범위말(건너뛴열, ([a, b]) => `${열이름(a)}~${열이름(b)}열 → ${열이름(b + 1)}열부터 이어짐`)})`
|
|
431
|
+
+ ' — 표의 오른쪽 칸은 그만큼 당겨져 있습니다');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── 펼치기. 칸 수 상한에 닿으면 거기서 멈춘다 ──
|
|
435
|
+
const 줄상한 = Math.floor(칸수상한 / Math.max(폭, 1));
|
|
436
|
+
const 총줄 = 마디.reduce((s, m) => s + (m.줄 ? 1 : m.빈), 0);
|
|
437
|
+
const rows = [];
|
|
438
|
+
let 뺀첫번호 = null;
|
|
439
|
+
for (const m of 마디) {
|
|
440
|
+
const 몇 = m.줄 ? 1 : m.빈;
|
|
441
|
+
for (let i = 0; i < 몇; i++) {
|
|
442
|
+
if (rows.length >= 줄상한) { 뺀첫번호 = m.번호 + i; break; }
|
|
443
|
+
const r = new Array(폭).fill('');
|
|
444
|
+
if (m.줄) for (const [c, v] of m.줄.칸) r[자리.get(c)] = v;
|
|
445
|
+
rows.push(r);
|
|
446
|
+
}
|
|
447
|
+
if (뺀첫번호 !== null) break;
|
|
448
|
+
}
|
|
449
|
+
if (뺀첫번호 !== null) {
|
|
450
|
+
말.push(`펼친 표가 칸 수 상한(${천단위(칸수상한)}칸 — 줄 × 폭 ${천단위(폭)})을 넘어 아래쪽 ${천단위(총줄 - rows.length)}줄`
|
|
451
|
+
+ `(${천단위(뺀첫번호)}행부터)은 뺐습니다`);
|
|
452
|
+
}
|
|
255
453
|
return rows;
|
|
256
454
|
}
|
|
257
455
|
|
|
@@ -312,7 +510,10 @@ export function readXlsx(buf) {
|
|
|
312
510
|
for (const s of 목록) {
|
|
313
511
|
const xml = s.path ? 글(s.path) : null;
|
|
314
512
|
if (!xml) { notes.push(`시트 '${s.name}' 의 내용을 못 찾았습니다`); continue; }
|
|
315
|
-
|
|
513
|
+
const { rows, 말 } = 시트읽기(xml, { shared, 날짜여부 });
|
|
514
|
+
// 줄이거나 뺀 것은 시트 이름을 붙여 말로 올린다 — Read 와 doc2md 가 이 notes 를 그대로 보여 준다.
|
|
515
|
+
for (const m of 말) notes.push(`시트 '${s.name}': ${m}`);
|
|
516
|
+
sheets.push({ name: s.name, hidden: s.hidden, rows });
|
|
316
517
|
}
|
|
317
518
|
if (!sheets.length) throw new Error('읽을 수 있는 시트가 없습니다');
|
|
318
519
|
return { sheets, notes };
|
package/src/ui/ansi.js
CHANGED
|
@@ -90,9 +90,117 @@ export const cursor = {
|
|
|
90
90
|
export const 판시작 = '\x1b[?2026h';
|
|
91
91
|
export const 판끝 = '\x1b[?2026l';
|
|
92
92
|
|
|
93
|
+
/*
|
|
94
|
+
* ── 바깥에서 온 글에 섞인 터미널 제어 순서를 뗀다 (2.0.0 3회차 사냥) ─────
|
|
95
|
+
*
|
|
96
|
+
* 모델 답은 우리가 쓴 글이 아니다. 모델이 읽은 파일 · 웹 페이지 · 도구 출력에 든 글이 그대로
|
|
97
|
+
* 되풀려 오기도 한다. 그 안에 ESC 로 시작하는 순서가 있으면 터미널은 **글자가 아니라 명령으로**
|
|
98
|
+
* 받는다. 마크다운 그리개가 이것을 그대로 흘려보내고 있었다:
|
|
99
|
+
*
|
|
100
|
+
* ESC ]52;c;…BEL 클립보드를 몰래 바꾼다 (붙여넣은 명령이 다른 명령이 된다)
|
|
101
|
+
* ESC ]8;;주소 ESC \ 보이는 글과 다른 곳으로 가는 링크
|
|
102
|
+
* ESC [1A ESC [2K 윗줄 「✗ 실패」 를 「✓ 통과」 로 덮어쓴다
|
|
103
|
+
* ESC [2J · \b · \r 화면을 지우거나 이미 찍힌 글을 덮는다
|
|
104
|
+
* ESC ]2;… 창 제목
|
|
105
|
+
*
|
|
106
|
+
* 줄바꿈과 탭만 남기고 제어 글자(C0 · DEL · C1)와 그 순서를 통째로 뗀다. 순서를 통째로 떼야
|
|
107
|
+
* `[2J` 같은 찌꺼기가 안 남는다. 토막 사이에서 순서가 끊겨 와도 ESC 와 BEL 자체는 각 토막에서
|
|
108
|
+
* 빠지므로 터미널에 명령으로 닿지 않는다 — 남는 것은 보이는 글자뿐이다.
|
|
109
|
+
*/
|
|
110
|
+
// 글자가 놓이는 **방향**을 뒤집는 문자(RLO · LRE · 격리 …). 화면에서 `rm txt.exe` 가 다른 이름으로
|
|
111
|
+
// 보이게 만든다(Trojan Source). 이 파일에 보이지 않는 글자를 직접 적지 않으려고 코드값으로 만든다.
|
|
112
|
+
const 방향틀기 = new RegExp(`[${String.fromCharCode(0x202a)}-${String.fromCharCode(0x202e)}${String.fromCharCode(0x2066)}-${String.fromCharCode(0x2069)}]`, 'g');
|
|
113
|
+
|
|
114
|
+
/*
|
|
115
|
+
* 순서를 **끝난 것만** 통째로 뗀다 (Gemini 2차 검토). 마침 글자(BEL · ESC \) 가 없는 OSC 를 끝까지 삼키면
|
|
116
|
+
* 뒤따르는 답과 diff 가 통째로 사라졌다 — 글을 잃는 것이 제일 나쁘다. 끝 안 난 순서는 머리(ESC ])만
|
|
117
|
+
* 떼면 터미널이 명령으로 못 받고, 몸통은 보이는 글자로 남는다.
|
|
118
|
+
*/
|
|
119
|
+
export function 화면글거르기(글, { 색남김 = false } = {}) {
|
|
120
|
+
/*
|
|
121
|
+
* `색남김` — 우리가 입힌 색(SGR `ESC [ … m`)은 두고 **그 사이**만 거른다. say 처럼 우리 색과 바깥 글이
|
|
122
|
+
* 한 줄에 섞여 나가는 자리에서 쓴다(할 일 목록 · 서버가 준 모델 이름 · 승인 물음). 바깥 글에 든 색 코드도
|
|
123
|
+
* 남지만 색은 명령이 아니라 모양뿐이다.
|
|
124
|
+
*/
|
|
125
|
+
if (색남김) {
|
|
126
|
+
return String(글 ?? '').split(/(\x1b\[[0-9;]*m)/).map((조각, i) => (i % 2 ? 조각 : 화면글거르기(조각))).join('');
|
|
127
|
+
}
|
|
128
|
+
return String(글 ?? '')
|
|
129
|
+
.replace(/\x1b\][^\x07\x1b\n]*(?:\x07|\x1b\\)/g, '') // OSC — 클립보드 · 링크 · 제목 (끝난 것만)
|
|
130
|
+
.replace(/\x1b[PX^_][^\x1b\n]*\x1b\\/g, '') // DCS · SOS · PM · APC (끝난 것만)
|
|
131
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '') // CSI — 커서 · 지우기 · 색
|
|
132
|
+
.replace(/\x9b[0-?]*[ -/]*[@-~]/g, '') // 8비트 CSI
|
|
133
|
+
.replace(/\x1b[ -/]*[0-~]?/g, '') // ESC 두 글자 꼴(ESC 7 · ESC ( 0 · ESC c) · 끝 안 난 순서의 머리 · 홀 ESC
|
|
134
|
+
.replace(/\r\n/g, '\n')
|
|
135
|
+
.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, '') // 남은 제어 글자 (\t · \n 빼고)
|
|
136
|
+
.replace(방향틀기, '');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/*
|
|
140
|
+
* ── 칸을 안 먹는 글자 · 두 칸 먹는 그림글자 ────────────────────────────
|
|
141
|
+
*
|
|
142
|
+
* 입력 상자와 접어쓰기가 이 값으로 **화면 줄 수**를 센다. 한 칸이라도 모자라게 세면 줄이
|
|
143
|
+
* 터미널보다 넓어져 터미널은 두 줄로 접고 상자는 한 줄로 센다 — 키를 칠 때마다 위쪽 대화가
|
|
144
|
+
* 갉여 나간다(inputbox.js 머리말). 재 보니 이런 자리가 있었다:
|
|
145
|
+
*
|
|
146
|
+
* ✅ ❌ ⚡ ⭐ ☕ 한 칸으로 셌다. 터미널은 두 칸에 그린다(Emoji_Presentation).
|
|
147
|
+
* 🫠 (U+1FA70~) 그림글자 표를 1F9FF 에서 끊어 한 칸으로 셌다.
|
|
148
|
+
* 𠀀 (한자 확장 B~) 한 칸으로 셌다.
|
|
149
|
+
* ZWSP·ZWJ·결합 부호 한 칸씩 셌다. 터미널은 칸을 안 준다.
|
|
150
|
+
* 가족 그림(ZWJ 이음) 여덟 칸으로 셌다. 터미널은 두 칸 하나로 겹쳐 그린다.
|
|
151
|
+
* 풀어 쓴 한글(NFD) ㅎ+ㅏ+ㄴ 을 네 칸으로 셌다. 가운뎃소리·끝소리는 앞 글자에 붙는다.
|
|
152
|
+
*
|
|
153
|
+
* U+2600~27BF 는 섞여 있다. ✅ ⚡ 처럼 그림으로 그리는 것(Emoji_Presentation)만 두 칸이고,
|
|
154
|
+
* ✓ ✗ ⚠ ★ → 같은 글자 꼴은 한 칸이다. deel 이 화면에 그리는 표시가 바로 그쪽이라, 넓게 잡으면
|
|
155
|
+
* 상태줄·표가 한 칸씩 밀린다. 그래서 범위가 아니라 속성으로 가른다.
|
|
156
|
+
*/
|
|
157
|
+
const 칸없음 = /^[\p{Mn}\p{Me}\p{Cf}]$/u;
|
|
158
|
+
const 그림으로그림 = /^\p{Emoji_Presentation}$/u;
|
|
159
|
+
const 그림글자 = /^\p{Extended_Pictographic}$/u;
|
|
160
|
+
|
|
161
|
+
function 넓은가(cp, ch) {
|
|
162
|
+
return (cp >= 0x1100 && cp <= 0x115f)
|
|
163
|
+
|| (cp >= 0x2e80 && cp <= 0xa4cf)
|
|
164
|
+
|| (cp >= 0xa960 && cp <= 0xa97f) // 한글 첫소리 확장
|
|
165
|
+
|| (cp >= 0xac00 && cp <= 0xd7a3)
|
|
166
|
+
|| (cp >= 0xf900 && cp <= 0xfaff)
|
|
167
|
+
|| (cp >= 0xfe30 && cp <= 0xfe6f)
|
|
168
|
+
|| (cp >= 0xff00 && cp <= 0xff60)
|
|
169
|
+
|| (cp >= 0xffe0 && cp <= 0xffe6)
|
|
170
|
+
|| (cp >= 0x1f300 && cp <= 0x1f9ff)
|
|
171
|
+
|| (cp >= 0x1fa70 && cp <= 0x1faff) // 그림글자 확장 A
|
|
172
|
+
|| (cp >= 0x20000 && cp <= 0x3fffd) // 한자 확장 B 부터
|
|
173
|
+
|| 그림으로그림.test(ch);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/*
|
|
177
|
+
* ── 한 칸 바탕을 두 칸 그림으로 바꾸는 꼬리 · 국기 짝 (Gemini 화면4) ─────────
|
|
178
|
+
*
|
|
179
|
+
* U+FE0F 는 「그림으로 그려라」 는 표시다. 윈도 터미널·iTerm2·kitty 는 하트·경고·체크 뒤에 이것이
|
|
180
|
+
* 붙으면 **두 칸**에 그린다. 우리는 바탕 한 칸 + 선택자 0칸 = 한 칸으로 셌다 — 모델 답에 흔한
|
|
181
|
+
* 모양이라 한 줄에 하나만 있어도 입력 상자가 줄 수를 틀려 위쪽 대화가 갉여 나갔다. 키캡(1+FE0F
|
|
182
|
+
* +20E3)과 한 칸 그림(검지 U+261D)에 붙은 살색 조절도 그랬다.
|
|
183
|
+
*
|
|
184
|
+
* 국기는 거꾸로였다. 지역 표시 글자 둘이 두 칸 하나에 겹치는데 둘 다 두 칸으로 세어 넷이 됐다.
|
|
185
|
+
* 짝을 짓는 것은 앞에서부터 둘씩이다(🇺🇸🇰🇷 는 넷 칸, 홀로 남은 반쪽은 두 칸).
|
|
186
|
+
*
|
|
187
|
+
* 선택자가 없는 ⚠ ✔ 는 여전히 한 칸이다 — deel 이 그리는 표시가 그쪽이다(위 머리말). 숫자 뒤
|
|
188
|
+
* FE0F 만으로는 키캡이 아니고(1+FE0F 는 한 칸), 20E3 이 붙어야 두 칸이다.
|
|
189
|
+
*/
|
|
190
|
+
const 지역표시인가 = (cp) => cp >= 0x1f1e6 && cp <= 0x1f1ff;
|
|
191
|
+
const 키캡바탕인가 = (cp) => (cp >= 0x30 && cp <= 0x39) || cp === 0x23 || cp === 0x2a;
|
|
192
|
+
|
|
93
193
|
// 한글·한자·가나는 터미널에서 두 칸을 차지한다. 표 정렬이 이걸 모르면 어긋난다.
|
|
94
194
|
export function width(str) {
|
|
95
195
|
let w = 0;
|
|
196
|
+
// 바로 앞 글자가 ZWJ 였나 · 그림글자였나. 이어붙인 그림은 앞 그림과 한 칸에 겹친다.
|
|
197
|
+
let 앞이음 = false;
|
|
198
|
+
let 앞그림 = false;
|
|
199
|
+
// 바로 앞 바탕 글자와 그 글자에 준 칸 수. 칸 안 먹는 글자(ZWJ·결합 부호)는 이 값을 안 바꾼다.
|
|
200
|
+
let 바탕 = -1;
|
|
201
|
+
let 바탕칸 = 0;
|
|
202
|
+
// 짝을 기다리는 지역 표시 글자가 바로 앞에 있나.
|
|
203
|
+
let 반쪽국기 = false;
|
|
96
204
|
// 없는 값은 빈 글자로 본다.
|
|
97
205
|
//
|
|
98
206
|
// String(null) 은 'null' 이라 폭이 4 로 나온다. 그러면 상태줄이 네 칸씩
|
|
@@ -101,21 +209,85 @@ export function width(str) {
|
|
|
101
209
|
if (str === null || str === undefined) return 0;
|
|
102
210
|
for (const ch of String(str).replace(/\x1b\[[0-9;]*m/g, '')) {
|
|
103
211
|
const cp = ch.codePointAt(0);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
212
|
+
const 이음뒤 = 앞이음;
|
|
213
|
+
const 그림뒤 = 앞그림;
|
|
214
|
+
앞이음 = cp === 0x200d;
|
|
215
|
+
// 거의 다 여기서 끝난다 — 로마자·숫자·기호. 속성 검사는 비싸고 이 함수는 자주 불린다.
|
|
216
|
+
if (cp < 0x0300) { 앞그림 = false; 반쪽국기 = false; 바탕 = cp; 바탕칸 = 1; w += 1; continue; }
|
|
217
|
+
// 그림으로 그리라는 선택자 · 키캡 꼬리 — 한 칸 바탕을 두 칸으로 넓힌다 (위 머리말).
|
|
218
|
+
if (cp === 0xfe0f || cp === 0x20e3) {
|
|
219
|
+
const 넓힌다 = 바탕칸 === 1
|
|
220
|
+
&& (cp === 0xfe0f ? 그림글자.test(String.fromCodePoint(바탕)) : 키캡바탕인가(바탕));
|
|
221
|
+
if (넓힌다) { w += 1; 바탕칸 = 2; }
|
|
222
|
+
앞그림 = cp === 0xfe0f && 그림뒤;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const 그림 = 그림글자.test(ch);
|
|
226
|
+
const 살색 = cp >= 0x1f3fb && cp <= 0x1f3ff;
|
|
227
|
+
// 그림 뒤에 붙는 ZWJ · 변형 선택자 · 살색 조절은 「아직 그 그림」 이다.
|
|
228
|
+
앞그림 = 그림 || (그림뒤 && (cp === 0x200d || (cp >= 0xfe00 && cp <= 0xfe0f) || 살색));
|
|
229
|
+
if (칸없음.test(ch)) continue;
|
|
230
|
+
// 풀어 쓴 한글의 가운뎃소리·끝소리는 앞 첫소리 칸에 겹친다.
|
|
231
|
+
if ((cp >= 0x1160 && cp <= 0x11ff) || (cp >= 0xd7b0 && cp <= 0xd7ff)) continue;
|
|
232
|
+
// ZWJ 뒤의 그림은 앞 그림과 같은 칸이다.
|
|
233
|
+
if (이음뒤 && 그림) continue;
|
|
234
|
+
// 그림 뒤의 살색 조절도 같은 칸이다 — 다만 한 칸 그림이었으면 두 칸으로 넓힌다.
|
|
235
|
+
if (그림뒤 && 살색) { if (바탕칸 === 1) { w += 1; 바탕칸 = 2; } continue; }
|
|
236
|
+
// 국기 반쪽 둘은 두 칸 하나다.
|
|
237
|
+
if (지역표시인가(cp)) {
|
|
238
|
+
if (반쪽국기) { 반쪽국기 = false; continue; }
|
|
239
|
+
반쪽국기 = true;
|
|
240
|
+
} else {
|
|
241
|
+
반쪽국기 = false;
|
|
242
|
+
}
|
|
243
|
+
const 칸 = 넓은가(cp, ch) ? 2 : 1;
|
|
244
|
+
바탕 = cp;
|
|
245
|
+
바탕칸 = 칸;
|
|
246
|
+
w += 칸;
|
|
115
247
|
}
|
|
116
248
|
return w;
|
|
117
249
|
}
|
|
118
250
|
|
|
251
|
+
/*
|
|
252
|
+
* i 자리에서 **눈에 보이는 한 덩이**를 통째로 떼어 온다.
|
|
253
|
+
*
|
|
254
|
+
* 글자를 s[i] 로 한 칸씩 읽으면 UTF-16 코드 단위로 잘린다. 😀 같은 글자는
|
|
255
|
+
* 두 칸을 차지하므로 하필 그 사이에서 줄이 접히면 반쪽짜리 서로게이트가
|
|
256
|
+
* 양쪽 줄에 하나씩 남는다 — 접어쓰기("a😀", 2) 가 ["a\ud83d","\ude00"] 를
|
|
257
|
+
* 내놨다. 화면에는 물음표 두 개로 찍히고, 폭 계산도 그때부터 어긋난다.
|
|
258
|
+
*
|
|
259
|
+
* 그래서 코드 포인트로 읽고, 뒤에 붙는 것들(살색 조절·변형 선택자·결합
|
|
260
|
+
* 부호·ZWJ 로 이어붙인 다음 글자)까지 한 덩이로 본다. 완전한 문자소 분할은
|
|
261
|
+
* 아니지만 — 그건 표가 있어야 한다 — 실제로 깨지던 자리는 이걸로 다 막힌다.
|
|
262
|
+
*
|
|
263
|
+
* 접어쓰기(wrap.js)에만 있던 것을 여기로 옮겼다. clip 은 코드 포인트 하나씩 잘라서 그림과
|
|
264
|
+
* 선택자·ZWJ 사이를 끊었다 — 자른 끝에 ZWJ 가 남으면 뒤에 붙는 말줄임표와 겹쳐 그려지고,
|
|
265
|
+
* 선택자가 떨어지면 두 칸 그림이 한 칸 글 모양으로 바뀐다. 국기 짝도 한 덩이다 — 반쪽 국기
|
|
266
|
+
* 둘은 글자 두 개로 그려진다(위 width 머리말).
|
|
267
|
+
*/
|
|
268
|
+
export function 한덩이(s, i) {
|
|
269
|
+
const 첫 = s.codePointAt(i);
|
|
270
|
+
let 끝 = i + String.fromCodePoint(첫).length;
|
|
271
|
+
if (지역표시인가(첫) && 끝 < s.length && 지역표시인가(s.codePointAt(끝))) 끝 += 2;
|
|
272
|
+
for (;;) {
|
|
273
|
+
if (끝 >= s.length) return s.slice(i, 끝);
|
|
274
|
+
const cp = s.codePointAt(끝);
|
|
275
|
+
// ZWJ 는 다음 글자까지 끌고 온다 (가족 그림처럼 이어붙인 그림글자)
|
|
276
|
+
if (cp === 0x200d) {
|
|
277
|
+
const 다음 = 끝 + 1;
|
|
278
|
+
if (다음 >= s.length) return s.slice(i, 끝);
|
|
279
|
+
끝 = 다음 + String.fromCodePoint(s.codePointAt(다음)).length;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const 딸린것 = (cp >= 0xfe00 && cp <= 0xfe0f) // 변형 선택자
|
|
283
|
+
|| (cp >= 0x1f3fb && cp <= 0x1f3ff) // 살색 조절
|
|
284
|
+
|| (cp >= 0x0300 && cp <= 0x036f) // 결합 부호
|
|
285
|
+
|| cp === 0x20e3; // 키캡
|
|
286
|
+
if (!딸린것) return s.slice(i, 끝);
|
|
287
|
+
끝 += String.fromCodePoint(cp).length;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
119
291
|
export function pad(str, target, align = 'left') {
|
|
120
292
|
const gap = Math.max(0, target - width(str));
|
|
121
293
|
if (align === 'right') return ' '.repeat(gap) + str;
|
|
@@ -140,6 +312,13 @@ export function pad(str, target, align = 'left') {
|
|
|
140
312
|
*/
|
|
141
313
|
export function clip(str, max, tail = '…') {
|
|
142
314
|
if (width(str) <= max) return str;
|
|
315
|
+
/*
|
|
316
|
+
* 꼬리보다 좁은 칸 (Gemini 화면5). 남은 예산이 음수여도 꼬리를 붙여서 `clip('테스트', 0)` 이 「…」
|
|
317
|
+
* 한 칸, 꼬리가 두 칸이면 `max 1` 에 두 칸을 냈다 — 좁은 창에서 줄이 넘친다. 칸이 없으면 빈 글,
|
|
318
|
+
* 꼬리가 안 들어가면 꼬리 없이 자른다.
|
|
319
|
+
*/
|
|
320
|
+
if (!(max > 0)) return '';
|
|
321
|
+
if (width(tail) > max) return clip(str, max, '');
|
|
143
322
|
const budget = max - width(tail);
|
|
144
323
|
let out = '';
|
|
145
324
|
let w = 0;
|
|
@@ -153,7 +332,10 @@ export function clip(str, max, tail = '…') {
|
|
|
153
332
|
색켜짐 = !/^\x1b\[0?m$/.test(조각);
|
|
154
333
|
continue;
|
|
155
334
|
}
|
|
156
|
-
|
|
335
|
+
// 코드 포인트가 아니라 보이는 덩이로 자른다 — 그림과 선택자·ZWJ 사이를 안 끊는다 (한덩이 머리말).
|
|
336
|
+
for (let i = 0; i < 조각.length;) {
|
|
337
|
+
const ch = 한덩이(조각, i);
|
|
338
|
+
i += ch.length;
|
|
157
339
|
const cw = width(ch);
|
|
158
340
|
if (w + cw > budget) { 찼다 = true; break; }
|
|
159
341
|
out += ch;
|
|
@@ -166,7 +348,9 @@ export function clip(str, max, tail = '…') {
|
|
|
166
348
|
// 터미널 가로 폭. 파이프로 넘어가면 알 수 없으니 넉넉히 잡는다.
|
|
167
349
|
export const cols = () => process.stdout.columns || 100;
|
|
168
350
|
|
|
169
|
-
|
|
351
|
+
// 줄 하나를 낸다. 여기로 바깥 글(할 일 · 모델 이름 · 플러그인 설명 …)이 곧장 오므로 들어온 제어 순서를 뗀다 —
|
|
352
|
+
// 우리 색은 남긴다(화면글거르기 머리말). 커서 옮기기 같은 우리 제어는 이 길을 안 쓴다(cursor · 판시작).
|
|
353
|
+
export const say = (s = '') => process.stdout.write(화면글거르기(s, { 색남김: true }) + '\n');
|
|
170
354
|
|
|
171
355
|
export function rule(label = '', total = 64) {
|
|
172
356
|
if (!label) return say(c.gray('─'.repeat(total)));
|
|
@@ -176,7 +360,11 @@ export function rule(label = '', total = 64) {
|
|
|
176
360
|
|
|
177
361
|
// 채움 막대. 반쪽 칸까지 써서 좁은 폭에서도 눈금이 보인다.
|
|
178
362
|
export function bar(used, total, cells = 32) {
|
|
179
|
-
|
|
363
|
+
// 말이 안 되는 셈은 0 으로 본다 — 아래 gauge() 와 같은 까닭이다.
|
|
364
|
+
// Math.min 은 NaN 을 그대로 흘려 repeat(NaN) 이 빈 글자가 되고(막대가 통째로
|
|
365
|
+
// 사라진다), 음수는 repeat(-4) 로 **던진다** — 화면 한 칸이 프로그램을 죽인다.
|
|
366
|
+
const 몫 = Number(used) / Number(total);
|
|
367
|
+
const ratio = total > 0 && Number.isFinite(몫) ? Math.min(1, Math.max(0, 몫)) : 0;
|
|
180
368
|
const exact = ratio * cells;
|
|
181
369
|
const full = Math.floor(exact);
|
|
182
370
|
const half = exact - full >= 0.5 && full < cells;
|
|
@@ -243,7 +431,13 @@ const BOX = { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' };
|
|
|
243
431
|
export function box(lines, { title = '', pad: gap = 1, tone = c.gray, max = cols() - 4 } = {}) {
|
|
244
432
|
const body = lines.map((l) => clip(l, max - gap * 2 - 2));
|
|
245
433
|
const inner = Math.max(
|
|
246
|
-
|
|
434
|
+
// 윗줄은 `╭─ 이름 ` 까지가 다섯 칸이고 오른쪽 모서리가 한 칸이다. 안쪽이
|
|
435
|
+
// 이름+3 보다 좁으면 윗줄만 길어져 테두리가 한 칸 어긋난다(여백 0 이면 늘 그렇다).
|
|
436
|
+
//
|
|
437
|
+
// 이름이 없으면 그 바닥도 없다. 빈 이름에 +3 을 하면 안쪽 폭이 3 아래로 못 내려가
|
|
438
|
+
// 한 글자짜리 상자가 세 칸으로 그려진다 — 위 JSDoc 이 적은 「가장 긴 줄에 맞춘다」 와
|
|
439
|
+
// 어긋난다 (8회차 AN1).
|
|
440
|
+
title ? width(title) + 3 : 0,
|
|
247
441
|
...body.map((l) => width(l)),
|
|
248
442
|
) + gap * 2;
|
|
249
443
|
const top = title
|
package/src/ui/banner.js
CHANGED
|
@@ -235,7 +235,8 @@ export function 마지막틀(opts = {}) { return 틀(틀수 - 1, opts); }
|
|
|
235
235
|
*/
|
|
236
236
|
export function 쓸배율(칸) {
|
|
237
237
|
const w = Number.isFinite(칸) ? 칸 : (process.stdout.columns ?? 80);
|
|
238
|
-
// 들여쓰기 두 칸 + 오른쪽 한
|
|
238
|
+
// 들여쓰기 두 칸 + 오른쪽 두 칸. 한 칸만 두면 그림이 창 마지막 칸까지 닿는데,
|
|
239
|
+
// 마지막 칸에 쓰면 줄을 넘기는 터미널이 있어 그림이 한 줄 아래로 흘러내린다.
|
|
239
240
|
if (w >= 이름폭(2) + 4) return 2;
|
|
240
241
|
if (w >= 이름폭(1) + 4) return 1;
|
|
241
242
|
return 0;
|