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/fig.js
CHANGED
|
@@ -170,7 +170,12 @@ function 열쇠(g) {
|
|
|
170
170
|
return `${g.sessionID ?? 0}:${g.localID ?? 0}`;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* 크기를 `375×812` 로. 소수점은 **반올림**한다 — 시안 치수는 정수로 읽는 게 맞다.
|
|
175
|
+
*
|
|
176
|
+
* 버리면 `375.8` 이 375 가 되어, 사람이 Figma 에서 보는 376 과 한 칸 어긋난다.
|
|
177
|
+
* 시안 치수는 자를 대는 값이라 가까운 쪽으로 붙이는 것이 맞다.
|
|
178
|
+
*/
|
|
174
179
|
function 크기말(size) {
|
|
175
180
|
if (!size || typeof size !== 'object') return '';
|
|
176
181
|
const w = Number(size.x);
|
|
@@ -217,9 +222,15 @@ function 나무엮기(노드들) {
|
|
|
217
222
|
return { 자리표, 자식표, 뿌리들 };
|
|
218
223
|
}
|
|
219
224
|
|
|
220
|
-
/**
|
|
225
|
+
/**
|
|
226
|
+
* 한 갈래를 줄들로 편다. 깊이는 두 칸씩.
|
|
227
|
+
*
|
|
228
|
+
* 상한에 걸려 **못 편 것이 실제로 있을 때만** `셈.잘림` 을 켠다. 「남은 것이
|
|
229
|
+
* 0 인가」 로 물으면 상한과 도형 수가 딱 같을 때 — 한 줄도 안 잘린 판에 —
|
|
230
|
+
* 「뒷부분은 안 실렸습니다」 가 나간다. 거짓 경고는 진짜 경고를 죽인다.
|
|
231
|
+
*/
|
|
221
232
|
function 가지풀기(뿌리, 자식표, 줄들, 깊이, 셈) {
|
|
222
|
-
if (셈.남은 <= 0) return;
|
|
233
|
+
if (셈.남은 <= 0) { 셈.잘림 = true; return; }
|
|
223
234
|
const 들여 = ' '.repeat(Math.min(깊이, 12));
|
|
224
235
|
const 갈래 = typeof 뿌리.type === 'string' ? 뿌리.type : String(뿌리.type ?? '?');
|
|
225
236
|
const 이름 = String(뿌리.name ?? '').trim();
|
|
@@ -241,7 +252,7 @@ function 가지풀기(뿌리, 자식표, 줄들, 깊이, 셈) {
|
|
|
241
252
|
* @returns {{ok:true, 갈래:'fig', 덩이들:[{이름,문단들}], 말:string[], 노드수:number}
|
|
242
253
|
* |{ok:false, error:string, 끝났다?:boolean}}
|
|
243
254
|
*/
|
|
244
|
-
export function readFig(
|
|
255
|
+
export function readFig(경로또는버퍼, { 최대노드 = 노드최대 } = {}) {
|
|
245
256
|
let buf;
|
|
246
257
|
try {
|
|
247
258
|
buf = Buffer.isBuffer(경로또는버퍼) ? 경로또는버퍼 : readFileSync(경로또는버퍼);
|
|
@@ -270,6 +281,11 @@ export function readFig(경로또는버퍼) {
|
|
|
270
281
|
const 메타몸 = 꾸러미.files.get('meta.json');
|
|
271
282
|
if (메타몸) { try { 메타 = JSON.parse(메타몸.toString('utf8')); } catch { 메타 = null; } }
|
|
272
283
|
알맹이 = 꾸러미.files.get('canvas.fig');
|
|
284
|
+
// **있는데 못 푼** 것은 「없다」 가 아니다 — 사람이 파일 안을 뒤지다 헛걸음한다(2.0.0 3회차).
|
|
285
|
+
const 못푼알맹이 = !알맹이 && 꾸러미.skipped.find((s) => s.name === 'canvas.fig');
|
|
286
|
+
if (못푼알맹이) {
|
|
287
|
+
return { ok: false, 끝났다: true, error: `.fig 꾸러미 안의 canvas.fig 를 풀지 못했습니다 — ${못푼알맹이.why}` };
|
|
288
|
+
}
|
|
273
289
|
if (!알맹이) {
|
|
274
290
|
return {
|
|
275
291
|
ok: false,
|
|
@@ -341,7 +357,8 @@ export function readFig(경로또는버퍼) {
|
|
|
341
357
|
const 문서 = 뿌리들.find((n) => n.type === 'DOCUMENT') ?? 뿌리들[0];
|
|
342
358
|
const 쪽들 = (자식표.get(열쇠(문서?.guid)) ?? []).filter((n) => n.type === 'CANVAS');
|
|
343
359
|
|
|
344
|
-
const
|
|
360
|
+
const 상한노드 = Math.max(1, Number(최대노드) || 노드최대);
|
|
361
|
+
const 셈 = { 남은: 상한노드 };
|
|
345
362
|
const 덩이들 = [];
|
|
346
363
|
|
|
347
364
|
const 개요 = [];
|
|
@@ -355,11 +372,24 @@ export function readFig(경로또는버퍼) {
|
|
|
355
372
|
|
|
356
373
|
if (쪽들.length) {
|
|
357
374
|
for (const 쪽 of 쪽들) {
|
|
375
|
+
const 아이들 = 자식표.get(열쇠(쪽.guid)) ?? [];
|
|
358
376
|
const 줄들 = [];
|
|
359
|
-
for (const 아이 of
|
|
377
|
+
for (const 아이 of 아이들) 가지풀기(아이, 자식표, 줄들, 0, 셈);
|
|
378
|
+
/*
|
|
379
|
+
* 상한에 걸려 못 편 쪽을 「비어 있습니다」 라고 적으면 안 된다.
|
|
380
|
+
*
|
|
381
|
+
* 앞쪽 쪽에서 상한을 다 써 버리면 뒤쪽은 한 줄도 안 나온다. 그때 「비어
|
|
382
|
+
* 있다」 고 적으면, 시안에 멀쩡히 들어 있는 화면을 사람도 모델도 없는
|
|
383
|
+
* 것으로 읽는다 — 없는 화면을 새로 그리게 된다. 안 폈다고 말한다.
|
|
384
|
+
*/
|
|
385
|
+
const 못편것 = !줄들.length && 아이들.length > 0;
|
|
360
386
|
덩이들.push({
|
|
361
387
|
이름: String(쪽.name ?? '').trim() || '이름 없는 쪽',
|
|
362
|
-
문단들: 줄들.length
|
|
388
|
+
문단들: 줄들.length
|
|
389
|
+
? 줄들
|
|
390
|
+
: [못편것
|
|
391
|
+
? `(도형 ${상한노드.toLocaleString('en-US')}개 상한에 걸려 이 쪽은 못 폈습니다 — 비어 있는 것이 아닙니다.)`
|
|
392
|
+
: '(이 쪽은 비어 있습니다.)'],
|
|
363
393
|
});
|
|
364
394
|
}
|
|
365
395
|
} else {
|
|
@@ -369,8 +399,9 @@ export function readFig(경로또는버퍼) {
|
|
|
369
399
|
덩이들.push({ 이름: '도형', 문단들: 줄들 });
|
|
370
400
|
}
|
|
371
401
|
|
|
372
|
-
|
|
373
|
-
|
|
402
|
+
// 못 편 것이 정말 있을 때만 말한다 (가지풀기 머리말).
|
|
403
|
+
if (셈.잘림) {
|
|
404
|
+
말.push(`도형이 ${상한노드.toLocaleString('en-US')}개를 넘어 거기까지만 폈습니다 — 뒷부분은 안 실렸습니다`);
|
|
374
405
|
}
|
|
375
406
|
|
|
376
407
|
return { ok: true, 갈래: 'fig', 덩이들, 말, 노드수: 노드들.length };
|
package/src/tools/fsutil.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 파일 훑기와 glob 매칭. 외부 패키지 없이 직접 구현한다.
|
|
2
2
|
import { readdirSync, statSync, readFileSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { join, relative as 경로상대, basename as 끝이름, sep as 경로가름, resolve as 경로풀기 } from 'node:path';
|
|
4
4
|
import { decode, looksBinary } from './encoding.js';
|
|
5
5
|
import { 뿌리규칙읽기, 파일규칙읽기, 걸리나 } from './ignore.js';
|
|
6
6
|
// 살림이 **어디** 있는지는 config.js 한 곳만 안다 (DEEL_HOME 으로 옮길 수 있다).
|
|
@@ -22,6 +22,21 @@ import { homeDir } from '../config.js';
|
|
|
22
22
|
* 하는 일이 뻔해서 읽으면 다 보인다. 그게 이 프로젝트가 원하는 것이다.
|
|
23
23
|
*/
|
|
24
24
|
export function copyDir(from, to, { skipped = [] } = {}) {
|
|
25
|
+
/*
|
|
26
|
+
* 담을 곳이 **원본 안**이면 시작도 안 한다.
|
|
27
|
+
*
|
|
28
|
+
* 담을 곳을 먼저 만들고 원본을 훑으니, 담을 곳이 원본 안이면 방금 담은 것을 또 훑어 또
|
|
29
|
+
* 담는다. 끝이 없다. `cd ~` 에서 `/plugin install .` 한 번이면 그렇게 된다 — 임시 자리
|
|
30
|
+
* `~/.deel/plugins/.tmp-local` 이 원본 안이라서다. 재어 보니 2분 만에 3,341단이었고
|
|
31
|
+
* 디스크가 찰 때까지 안 멈춘다(2.0.0 6회차 사냥). 윈도우는 대소문자를 안 가르므로 낮춰 견준다.
|
|
32
|
+
*/
|
|
33
|
+
const 견줄꼴 = (p) => { const r = 경로풀기(p); return process.platform === 'win32' ? r.toLowerCase() : r; };
|
|
34
|
+
const 원 = 견줄꼴(from);
|
|
35
|
+
const 곳 = 견줄꼴(to);
|
|
36
|
+
const 원끝 = 원.endsWith(경로가름) ? 원 : 원 + 경로가름;
|
|
37
|
+
if (곳 === 원 || 곳.startsWith(원끝)) {
|
|
38
|
+
throw new Error(`담을 곳이 원본 폴더 안이라 복사하지 않습니다 — 끝없이 제 안으로 복사하게 됩니다: ${to}`);
|
|
39
|
+
}
|
|
25
40
|
mkdirSync(to, { recursive: true });
|
|
26
41
|
for (const e of readdirSync(from, { withFileTypes: true })) {
|
|
27
42
|
const s = join(from, e.name);
|
|
@@ -84,7 +99,20 @@ export const SKIP_DIRS = new Set([
|
|
|
84
99
|
*/
|
|
85
100
|
export function 내부살림(abs) {
|
|
86
101
|
const 편 = String(abs ?? '').replace(/\\/g, '/');
|
|
87
|
-
|
|
102
|
+
/*
|
|
103
|
+
* ── 조각마다 `:스트림` 꼬리를 벗기고 본다 (사냥6 F6-2) ───────────────────
|
|
104
|
+
*
|
|
105
|
+
* 윈도우(NTFS)는 `config.json::$DATA` 를 config.json **본체**로, `config.json:이름` 을 그
|
|
106
|
+
* 파일에 붙은 딴 스트림으로 연다. 이 자는 조각을 글자 그대로 견줘서 그 철자가 전부 비켜 갔다 —
|
|
107
|
+
*
|
|
108
|
+
* Read .deel/config.json::$DATA 열쇠가 그대로 나왔다
|
|
109
|
+
* Write .deel/config.json::$DATA **설정 본체를 덮어썼다**
|
|
110
|
+
* cat .deel/audit.jsonl::$DATA checkPaths 통과
|
|
111
|
+
*
|
|
112
|
+
* 드라이브 조각(`C:`)만 빼고 첫 `:` 뒤를 버린다. 유닉스에서 `:` 는 그냥 글자라 막는 쪽으로
|
|
113
|
+
* 넓어질 뿐이다(`history:x` 라는 살림 이름을 따로 쓸 까닭이 없다).
|
|
114
|
+
*/
|
|
115
|
+
const 조각 = 편.split('/').map((x, 몇째) => (몇째 === 0 && /^[A-Za-z]:$/.test(x) ? x : x.replace(/:.*$/s, '')));
|
|
88
116
|
const 이름 = (조각[조각.length - 1] ?? '').toLowerCase();
|
|
89
117
|
/*
|
|
90
118
|
* 살림 자리를 `.deel` 이라는 **글자**로 찾고 있었다.
|
|
@@ -120,7 +148,7 @@ export function 내부살림(abs) {
|
|
|
120
148
|
if (i < 0) {
|
|
121
149
|
try {
|
|
122
150
|
const 집 = String(homeDir()).replace(/\\/g, '/').replace(/[/]+$/, '');
|
|
123
|
-
const 낮은 =
|
|
151
|
+
const 낮은 = 조각.join('/').toLowerCase();
|
|
124
152
|
const 집낮은 = 집.toLowerCase();
|
|
125
153
|
if (집 && (낮은 === 집낮은 || 낮은.startsWith(집낮은 + '/'))) {
|
|
126
154
|
i = 집.split('/').length - 1;
|
|
@@ -185,7 +213,38 @@ export function 내부살림(abs) {
|
|
|
185
213
|
+ ' 지난 대화·명령 이력·열쇠 같은 것이라 이 작업과 상관이 없고, 읽으면 컨텍스트만 찹니다.'
|
|
186
214
|
+ ' 정말 그 안의 내용이 필요하면 사용자에게 직접 물어보세요.';
|
|
187
215
|
}
|
|
188
|
-
|
|
216
|
+
/*
|
|
217
|
+
* ── 이름만 보고 막지 않는다 ──────────────────────────────────────────
|
|
218
|
+
*
|
|
219
|
+
* 여기가 `이름 === 'audit.jsonl'` 한 줄이었다. **자리를 안 봤다.** 그래서
|
|
220
|
+
* 프로젝트가 제 감사 기록을 그 이름으로 쓰면 제 파일을 못 읽었다 —
|
|
221
|
+
*
|
|
222
|
+
* Read src/audit.jsonl 「deel 자신의 기록입니다」
|
|
223
|
+
*
|
|
224
|
+
* 거짓 경고도 결함이다. 모델은 그 파일이 없는 것으로 치고 일하거나, 있지도
|
|
225
|
+
* 않은 권한 문제를 찾아 헤맨다. 막는 것과 숨기는 것이 다르다고 적어 둔
|
|
226
|
+
* 이 자가, 남의 파일을 제 것이라며 숨기고 있었다.
|
|
227
|
+
*
|
|
228
|
+
* deel 이 이 이름을 쓰는 자리는 **제 살림 폴더 안 하나뿐이다**
|
|
229
|
+
* (safety/audit.js 의 join(dir, 'audit.jsonl') · stats.js 의
|
|
230
|
+
* `.deel/audit.jsonl`). 그 자리는 위 `.deel` 갈래와 집 폴더 갈래가 이미
|
|
231
|
+
* 잡는다 — DEEL_HOME 으로 옮겨도 그렇다. 이 줄은 그 둘이 다 빗나간 자리
|
|
232
|
+
* (집을 못 물어본 판)를 위한 마지막 그물이다.
|
|
233
|
+
*
|
|
234
|
+
* 그 그물을 「숨은 폴더 안이면 살림」 으로 쳤더니 **또 남의 파일을 막았다.**
|
|
235
|
+
* 「프로젝트의 소스·로그 폴더는 점으로 시작하지 않는다」 고 적어 뒀는데 그게
|
|
236
|
+
* 틀렸다 — `.github/` · `.ci/` · `.circleci/` 는 프로젝트 제 폴더다.
|
|
237
|
+
*
|
|
238
|
+
* Read .github/audit.jsonl 「deel 자신의 기록입니다」
|
|
239
|
+
*
|
|
240
|
+
* 이름만 보던 것을 자리까지 보게 고치면서, 자리를 「점으로 시작하나」 로만
|
|
241
|
+
* 봐서 반만 고쳐져 있었다. 그래서 **살림 폴더 이름일 때만** 친다. 집을 못
|
|
242
|
+
* 물어본 판에서도 DEEL_HOME 은 그대로 읽히니 옮긴 살림도 놓치지 않는다.
|
|
243
|
+
*/
|
|
244
|
+
const 담긴곳 = (조각[조각.length - 2] ?? '').toLowerCase();
|
|
245
|
+
const 살림꼬리 = String(process.env.DEEL_HOME ?? '').replace(/\\/g, '/').replace(/[/]+$/, '')
|
|
246
|
+
.split('/').pop()?.toLowerCase() || null;
|
|
247
|
+
if (이름 === 'audit.jsonl' && (담긴곳 === '.deel' || (살림꼬리 && 담긴곳 === 살림꼬리))) {
|
|
189
248
|
return 'deel 자신의 기록입니다. 읽어도 지금 하는 일에 도움이 안 됩니다.';
|
|
190
249
|
}
|
|
191
250
|
return null;
|
|
@@ -195,7 +254,14 @@ export function 내부살림(abs) {
|
|
|
195
254
|
export function globToRegex(pattern) {
|
|
196
255
|
let re = '';
|
|
197
256
|
let i = 0;
|
|
198
|
-
|
|
257
|
+
/*
|
|
258
|
+
* 앞머리 `./` 는 뗀다.
|
|
259
|
+
*
|
|
260
|
+
* 맞춰 보는 대상은 `src/a.js` 꼴의 상대경로라 `./src/*.js` 는 한 파일에도
|
|
261
|
+
* 안 맞았다. 사람도 모델도 셸 버릇대로 `./` 를 붙여 적는데, 그러면
|
|
262
|
+
* 「찾은 파일 없음」 이 뜨고 모델은 그 폴더에 파일이 없다고 믿는다.
|
|
263
|
+
*/
|
|
264
|
+
const p = pattern.replace(/\\/g, '/').replace(/^(?:\.\/)+/, '');
|
|
199
265
|
while (i < p.length) {
|
|
200
266
|
const ch = p[i];
|
|
201
267
|
if (ch === '*') {
|
|
@@ -209,19 +275,86 @@ export function globToRegex(pattern) {
|
|
|
209
275
|
const end = p.indexOf('}', i);
|
|
210
276
|
if (end < 0) { re += '\\{'; i += 1; }
|
|
211
277
|
else {
|
|
212
|
-
|
|
278
|
+
/*
|
|
279
|
+
* 갈래 **안쪽도 glob 이다.**
|
|
280
|
+
*
|
|
281
|
+
* 여기가 `escapeLiteral` 한 자였다. 그래서 `{*.js,*.ts}` 가 「`*.js` 라는
|
|
282
|
+
* 이름의 파일」 이 됐다 — 그런 파일은 없으니 **늘 0건**이었다. 밖에 쓴
|
|
283
|
+
* `*.{js,ts}` 는 되고 안에 쓴 것만 안 되니, 사람도 모델도 무늬가 틀렸다고
|
|
284
|
+
* 생각하지 않고 「그 폴더에 그런 파일이 없다」 고 믿었다. 조용히 틀린 답을
|
|
285
|
+
* 주는 자리라 못 찾았다고 말하는 것보다 나쁘다.
|
|
286
|
+
*
|
|
287
|
+
* 그래서 갈래마다 이 자를 다시 태운다. `*` · `?` · `[…]` · 겹친 중괄호가
|
|
288
|
+
* 한 벌로 같이 풀린다. 앞뒤 닻(`^`·`$`)만 떼고 알맹이를 쓴다.
|
|
289
|
+
*/
|
|
290
|
+
const parts = p.slice(i + 1, end).split(',')
|
|
291
|
+
.map((조각) => globToRegex(조각).source.replace(/^\^/, '').replace(/\$$/, ''));
|
|
213
292
|
re += `(?:${parts.join('|')})`;
|
|
214
293
|
i = end + 1;
|
|
215
294
|
}
|
|
216
295
|
} else if (ch === '[') {
|
|
217
296
|
const end = p.indexOf(']', i);
|
|
218
297
|
if (end < 0) { re += '\\['; i += 1; }
|
|
219
|
-
else {
|
|
298
|
+
else {
|
|
299
|
+
/*
|
|
300
|
+
* glob 의 빼기는 `[!…]` 다. 정규식은 `[^…]` 다.
|
|
301
|
+
*
|
|
302
|
+
* 대괄호를 통째로 옮겨 적고 있어서 `file[!0-9].txt` 가 `[!0-9]` —
|
|
303
|
+
* 「느낌표나 숫자」 — 가 됐다. 빼라고 한 file1.txt 가 걸리고 filea.txt
|
|
304
|
+
* 는 빠졌다. 정반대 답이다. .gitignore 쪽(tools/ignore.js)은 이미
|
|
305
|
+
* 바꿔 적고 있었고, rg 도 `[!…]` 를 빼기로 읽어서 Grep 은 엔진에
|
|
306
|
+
* 따라 답이 뒤집혔다.
|
|
307
|
+
*/
|
|
308
|
+
let 안 = p.slice(i + 1, end);
|
|
309
|
+
if (안.startsWith('!')) 안 = `^${안.slice(1)}`;
|
|
310
|
+
re += `[${안}]`;
|
|
311
|
+
i = end + 1;
|
|
312
|
+
}
|
|
220
313
|
} else { re += escapeLiteral(ch); i += 1; }
|
|
221
314
|
}
|
|
222
315
|
return new RegExp(`^${re}$`, process.platform === 'win32' ? 'i' : '');
|
|
223
316
|
}
|
|
224
317
|
|
|
318
|
+
/*
|
|
319
|
+
* ── glob 하나로 파일을 거르는 **한 벌뿐인** 자 ────────────────────────────
|
|
320
|
+
*
|
|
321
|
+
* Grep 의 glob 을 rg 는 제 작업 폴더 기준으로, 자바스크립트 길은 찾는 폴더
|
|
322
|
+
* 기준 상대경로 · 파일 이름으로 맞췄다. 그래서 같은 명령이 이렇게 갈렸다:
|
|
323
|
+
*
|
|
324
|
+
* glob=src/*.js rg 가 있는 PC → (0건) 없는 PC → src/a.js
|
|
325
|
+
* path=src glob=src/**\/*.js 두 PC 다 → (0건) — 파일은 있다
|
|
326
|
+
* glob=!src/** rg → src 밖 파일들 JS → (0건, `!` 를 글자로)
|
|
327
|
+
*
|
|
328
|
+
* 규칙을 하나로 정한다. rg 쪽도 같은 규칙으로 인자를 만든다(fastgrep.js 의 rg글로브들).
|
|
329
|
+
*
|
|
330
|
+
* · `!` 로 시작하면 **빼기**다 (rg · .gitignore 와 같다)
|
|
331
|
+
* · `/` 로 시작하면 작업 폴더(뿌리)에 묶는다 — 뿌리 기준 상대경로와만 견준다
|
|
332
|
+
* · 그 밖에는 찾는 폴더 기준 상대경로 · 파일 이름 · (빗금이 있으면) 뿌리 기준
|
|
333
|
+
* 상대경로 중 하나라도 맞으면 맞는다
|
|
334
|
+
*
|
|
335
|
+
* @returns {(abs:string) => boolean}
|
|
336
|
+
*/
|
|
337
|
+
export function glob거르개(glob, { 뿌리 = null, 자리 = null } = {}) {
|
|
338
|
+
if (!glob) return () => true;
|
|
339
|
+
const 빼기 = glob.startsWith('!');
|
|
340
|
+
let 몸 = 빼기 ? glob.slice(1) : glob;
|
|
341
|
+
const 뿌리에묶임 = 몸.startsWith('/');
|
|
342
|
+
if (뿌리에묶임) 몸 = 몸.replace(/^\/+/, '');
|
|
343
|
+
const re = globToRegex(몸);
|
|
344
|
+
const 빗금 = 몸.includes('/');
|
|
345
|
+
const 상대 = (기준, p) => 경로상대(기준, p).split(경로가름).join('/');
|
|
346
|
+
return (abs) => {
|
|
347
|
+
let 맞음;
|
|
348
|
+
if (뿌리에묶임) 맞음 = !!뿌리 && re.test(상대(뿌리, abs));
|
|
349
|
+
else {
|
|
350
|
+
맞음 = re.test(끝이름(abs))
|
|
351
|
+
|| (!!자리 && re.test(상대(자리, abs)))
|
|
352
|
+
|| (빗금 && !!뿌리 && re.test(상대(뿌리, abs)));
|
|
353
|
+
}
|
|
354
|
+
return 빼기 ? !맞음 : 맞음;
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
225
358
|
function escapeLiteral(s) {
|
|
226
359
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
227
360
|
}
|
|
@@ -231,8 +364,9 @@ function escapeLiteral(s) {
|
|
|
231
364
|
*
|
|
232
365
|
* `ignore` 가 켜져 있으면(기본) .gitignore 가 건너뛰라는 것은 건너뛴다 (tools/ignore.js).
|
|
233
366
|
* skipDirs 는 그 아래의 바닥이다 — .gitignore 가 없어도 node_modules 는 늘 건너뛴다.
|
|
234
|
-
* 돌려주는 배열에는 `건너뜀 = { 폴더,
|
|
235
|
-
*
|
|
367
|
+
* 돌려주는 배열에는 `건너뜀 = { 폴더, 파일, 못연폴더, 못연파일 }` 이 (열거되지 않게) 붙어 있다.
|
|
368
|
+
* 앞의 둘은 .gitignore 가 빼라고 해서 뺀 수고, 뒤의 둘은 **열려다 실패한** 수다. 부르는 쪽이 그
|
|
369
|
+
* 수를 화면에 적는다 — 조용히 빼면 "그 파일이 없다" 로 읽힌다.
|
|
236
370
|
* 폴더를 통째로 옮기거나 복사할 때는 `ignore: false` 로 — 그때는 다 있어야 한다.
|
|
237
371
|
*
|
|
238
372
|
* `잘림` 도 같이 붙는다 — **상한에서 멈췄다는 뜻이다.**
|
|
@@ -296,17 +430,64 @@ const 한숨 = () => new Promise((풀기) => setImmediate(풀기));
|
|
|
296
430
|
*/
|
|
297
431
|
export async function walk(root, { limit = 훑기상한(), skipDirs = SKIP_DIRS, ignore = true, signal = null } = {}) {
|
|
298
432
|
const out = [];
|
|
299
|
-
|
|
433
|
+
/*
|
|
434
|
+
* ── 안 본 것은 셋이다: 규칙으로 뺀 것 · 살림 폴더 · **못 연 것** ───────
|
|
435
|
+
*
|
|
436
|
+
* `폴더`·`파일` 은 .gitignore 가 빼라고 해서 뺀 수다. 우리가 보고 뺀 것이라
|
|
437
|
+
* 셀 수 있었다. 그런데 **열다가 실패한 것**은 아무 데도 안 셌다 — 아래 두
|
|
438
|
+
* catch 가 그냥 `continue` 였다.
|
|
439
|
+
*
|
|
440
|
+
* 그래서 이런 일이 있었다. 윈도우 ACL 이 막아 놓은 폴더 하나가 섞인
|
|
441
|
+
* 저장소에서 Grep 을 돌리면 —
|
|
442
|
+
*
|
|
443
|
+
* 일치 없음: TODO
|
|
444
|
+
*
|
|
445
|
+
* 꼬리말도 없다. 건너뜀이 0이고 잘림도 거짓이니 `건너뜀말` 이 낼 줄이
|
|
446
|
+
* 하나도 없어서다. 그 폴더 밑의 200개 파일은 **한 번도 안 열어 봤는데**
|
|
447
|
+
* 화면에는 다 찾아본 것과 똑같이 뜬다. 사람도 모델도 "이 프로젝트엔
|
|
448
|
+
* 그런 게 없다" 로 읽는다.
|
|
449
|
+
*
|
|
450
|
+
* 막는 자리는 흔하다 — ACL 로 막힌 폴더, 아직 안 내려받은 OneDrive
|
|
451
|
+
* 자리표, 대상이 사라진 정션, 다른 프로그램이 잡고 있는 폴더.
|
|
452
|
+
*
|
|
453
|
+
* 이제 그 수를 따로 센다. **못 찾은 것과 못 본 것은 다르다**, 이 파일이
|
|
454
|
+
* 잘림에 대고 이미 세 번 적어 둔 그 규칙이 여기에도 그대로 적용된다.
|
|
455
|
+
* 같은 통에 담아 보내는 까닭은 부르는 쪽(Glob·Grep·Outline·Verify)이
|
|
456
|
+
* 전부 이 통 하나를 `건너뜀말` 에 넘기기 때문이다 — 통을 새로 만들면
|
|
457
|
+
* 넘기는 자리를 하나 빠뜨리는 날이 오고, 그날 이 말은 또 조용해진다.
|
|
458
|
+
*/
|
|
459
|
+
const 건너뜀 = { 폴더: 0, 파일: 0, 못연폴더: 0, 못연파일: 0 };
|
|
300
460
|
// .gitignore 로 건너뛴 것과 다르다 — 이쪽은 우리가 늘 안 보는 살림 폴더다.
|
|
301
461
|
const 건너뛴살림 = [];
|
|
302
462
|
let 끊김 = false;
|
|
303
463
|
let 본것 = 0;
|
|
304
|
-
|
|
464
|
+
/*
|
|
465
|
+
* ── 살림은 **훑다가 흘러 들어와도** 안 내놓는다 (사냥6 F6-1) ──────────────
|
|
466
|
+
*
|
|
467
|
+
* 살림 폴더는 이름(skipDirs)으로만 건너뛰었다. 그래서 훑기를 `.deel` **안에서** 시작하면
|
|
468
|
+
* (Glob·Grep·Outline 의 path) config.json·mcp.json 이 목록과 내용으로 그대로 나왔고,
|
|
469
|
+
* 폴더가 아니라 파일로 흘리는 남의 도구 기록(`.aider.chat.history.md`)은 어디서 시작하든
|
|
470
|
+
* 걸러지지 않았다. Read 는 같은 자리를 내부살림() 으로 막는데 훑는 쪽만 그 자를 안 썼다 —
|
|
471
|
+
* 이 파일 머리말이 「목록은 한 곳에만」 이라고 적어 둔 까닭 그대로다.
|
|
472
|
+
*
|
|
473
|
+
* 그래서 시작 자리와 폴더·파일 하나하나를 **같은 자**로 본다. 건너뛴 것은 건너뛴살림 에
|
|
474
|
+
* 넣는다 — Move 가 「안 뜬 것」 을 말할 때 이 목록을 쓴다.
|
|
475
|
+
*
|
|
476
|
+
* **시작 자리도 그 목록에 넣는다.** 여태 시작 자리만 빼고 있었다 — 안 훑기만 하고
|
|
477
|
+
* 아무것도 안 남겼다. 그러면 살림 폴더를 통째로 옮길 때 Move 는 「안 뜬 것」 을 말할
|
|
478
|
+
* 근거가 하나도 없어서 `폴더 0개 파일` 이라고 적고 넘어간다. 바로 윗줄이 이 목록의
|
|
479
|
+
* 쓰임을 적어 놓고, 정작 제일 큰 한 덩이에서 비어 있었다.
|
|
480
|
+
*/
|
|
481
|
+
const stack = 내부살림(root) ? [] : [{ dir: root, rel: '', 규칙: ignore ? 뿌리규칙읽기(root) : [] }];
|
|
482
|
+
// 시작 자리가 살림이라 담을 것이 없으면 **그 사실도 남긴다** (바로 위 머리말).
|
|
483
|
+
if (!stack.length) 건너뛴살림.push(끝이름(root));
|
|
305
484
|
while (stack.length && out.length < limit) {
|
|
306
485
|
if (signal?.aborted) { 끊김 = true; break; }
|
|
307
486
|
const { dir, rel, 규칙 } = stack.pop();
|
|
308
487
|
let entries;
|
|
309
|
-
|
|
488
|
+
// 못 열면 그 폴더 밑은 통째로 안 본 것이다. 세어 두지 않으면 그 사실이
|
|
489
|
+
// 아무 데도 안 남는다 — 위 건너뜀 선언에 왜 그게 나쁜지 적어 두었다.
|
|
490
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { 건너뜀.못연폴더 += 1; continue; }
|
|
310
491
|
// 이 폴더에 .gitignore 가 있으면 그 아래에만 더한다 (뿌리 것은 위에서 읽었다).
|
|
311
492
|
let 여기규칙 = 규칙;
|
|
312
493
|
if (ignore && rel && entries.some((e) => e.name === '.gitignore' && e.isFile())) {
|
|
@@ -314,27 +495,58 @@ export async function walk(root, { limit = 훑기상한(), skipDirs = SKIP_DIRS,
|
|
|
314
495
|
if (추가.length) 여기규칙 = [...규칙, ...추가];
|
|
315
496
|
}
|
|
316
497
|
for (const e of entries) {
|
|
498
|
+
/*
|
|
499
|
+
* 본 것을 **거르기 앞에서** 센다 — 걸러낸 것도 readdirSync·걸리나 값을 이미 치렀다.
|
|
500
|
+
*
|
|
501
|
+
* 세기가 이 반복의 맨 끝에 있었다. 그런데 .gitignore·살림으로 거른 항목은 그 앞에서
|
|
502
|
+
* continue 로 빠져나가 한 번도 안 셌다. 그래서 로그·빌드 찌꺼기가 수만 개 쌓인 폴더는
|
|
503
|
+
* 숨 한 번 안 쉬고 끝까지 돌았고, 그동안 누른 ESC 는 다 돈 뒤에야 들렸다(2.0.0 6회차 사냥).
|
|
504
|
+
*/
|
|
505
|
+
본것 += 1;
|
|
506
|
+
if (본것 % 숨쉴간격 === 0) {
|
|
507
|
+
await 한숨();
|
|
508
|
+
if (signal?.aborted) { 끊김 = true; break; }
|
|
509
|
+
}
|
|
317
510
|
const full = join(dir, e.name);
|
|
318
511
|
const erel = rel ? `${rel}/${e.name}` : e.name;
|
|
319
512
|
if (e.isDirectory()) {
|
|
320
513
|
// 살림 폴더(node_modules·.git·dist…)는 안 훑는다. 다만 **몇 개를
|
|
321
514
|
// 안 봤는지는 센다** — Move 가 폴더를 통째로 옮길 때 이 수를 모르면
|
|
322
515
|
// 「폴더 12개 파일」 이라고 말해 놓고 3만 개를 옮기게 된다.
|
|
323
|
-
if (skipDirs.has(e.name)) { 건너뛴살림.push(erel); continue; }
|
|
516
|
+
if (skipDirs.has(e.name) || 내부살림(full)) { 건너뛴살림.push(erel); continue; }
|
|
324
517
|
if (여기규칙.length && 걸리나(erel, true, 여기규칙)) { 건너뜀.폴더 += 1; continue; }
|
|
325
518
|
stack.push({ dir: full, rel: erel, 규칙: 여기규칙 });
|
|
326
519
|
} else if (e.isFile()) {
|
|
520
|
+
if (내부살림(full)) { 건너뛴살림.push(erel); continue; }
|
|
327
521
|
if (여기규칙.length && 걸리나(erel, false, 여기규칙)) { 건너뜀.파일 += 1; continue; }
|
|
328
522
|
let st;
|
|
329
|
-
|
|
523
|
+
// 목록에는 있는데 물어보면 없다고 하는 파일이 있다 — 잠겨 있거나,
|
|
524
|
+
// 아직 안 내려받은 자리표거나, 방금 사라진 것이다. 여기서 조용히
|
|
525
|
+
// 빼면 그 파일은 처음부터 없던 것이 된다.
|
|
526
|
+
try { st = statSync(full); } catch { 건너뜀.못연파일 += 1; continue; }
|
|
330
527
|
out.push({ path: full, rel: erel, mtime: st.mtimeMs, size: st.size });
|
|
331
528
|
if (out.length >= limit) break;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
529
|
+
} else {
|
|
530
|
+
/*
|
|
531
|
+
* ── 폴더도 파일도 아닌 것 — **여기서 조용히 사라지고 있었다** ────────
|
|
532
|
+
*
|
|
533
|
+
* readdir 은 lstat 으로 본다. 그래서 심볼릭 링크와 윈도우 정션은
|
|
534
|
+
* isDirectory·isFile 이 **둘 다 false** 다. 위 두 갈래 어디에도 안 들어가니
|
|
535
|
+
* 걸러지지도 세어지지도 않고 그냥 없어졌다 — 바로 위 건너뜀 선언이
|
|
536
|
+
* 「대상이 사라진 정션」 을 못 본 자리로 꼽아 놓고, 정작 그것만 한 번도
|
|
537
|
+
* 안 셌다. 재 보니 정션 하나가 끊긴 폴더에서도 `{"폴더":0,"파일":0,
|
|
538
|
+
* "못연폴더":0,"못연파일":0}` 이라 꼬리말이 한 줄도 안 붙었다.
|
|
539
|
+
*
|
|
540
|
+
* **따라 들어가지는 않는다.** 바깥을 가리키는 링크 하나가 훑는 범위를
|
|
541
|
+
* 통째로 넓히고, 제 안을 가리키면 끝없이 돈다 (copyDir 머리말과 같은
|
|
542
|
+
* 자세다). 대신 **안 본 것으로 센다** — 못 찾은 것과 못 본 것은 다르다.
|
|
543
|
+
* 무엇으로 셀지만 대상에게 물어본다. 대상이 사라졌으면 폴더 쪽으로 센다
|
|
544
|
+
* (끊긴 정션은 거의 다 폴더를 가리키던 것이다).
|
|
545
|
+
*/
|
|
546
|
+
let 딸린것;
|
|
547
|
+
try { 딸린것 = statSync(full); } catch { 딸린것 = null; }
|
|
548
|
+
if (딸린것?.isFile()) 건너뜀.못연파일 += 1;
|
|
549
|
+
else 건너뜀.못연폴더 += 1;
|
|
338
550
|
}
|
|
339
551
|
}
|
|
340
552
|
if (끊김) break;
|
|
@@ -365,7 +577,9 @@ export function readTextFull(path) {
|
|
|
365
577
|
throw err;
|
|
366
578
|
}
|
|
367
579
|
const r = decode(buf);
|
|
368
|
-
|
|
580
|
+
// 읽은 바이트도 같이 준다. 되돌려 쓸 때 안 바뀐 자리는 **이 바이트 그대로** 둬야
|
|
581
|
+
// 한다 — 옛 인코딩은 한 글자에 바이트 자리가 둘인 것이 있다 (encoding.js 의 바꾼데만쓰기).
|
|
582
|
+
return { text: r.text, encoding: r.encoding, sure: r.sure, bom: r.bom ?? 0, buf };
|
|
369
583
|
}
|
|
370
584
|
|
|
371
585
|
/** 글만 필요할 때. 예전 부르던 자리를 그대로 두기 위해 남긴다. */
|
package/src/tools/hwpxwrite.js
CHANGED
|
@@ -49,20 +49,34 @@ export const 문단최대 = 20000;
|
|
|
49
49
|
/** 문단 수 상한. 넘으면 자르고 **잘랐다고 말한다.** */
|
|
50
50
|
export const 문단수최대 = 5000;
|
|
51
51
|
|
|
52
|
-
const 갈래 = { 제목1: 1, 제목2: 2, 제목3: 3, 본문: 0, 글머리: 0 };
|
|
52
|
+
const 갈래 = { 제목1: 1, 제목2: 2, 제목3: 3, 본문: 0, 글머리: 0, 빈줄: 0 };
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* XML 1.0 이 아예 못 담는 글자.
|
|
56
|
+
*
|
|
57
|
+
* 모델이 낸 글에 이런 것이 섞이는 일이 실제로 있다 — 터미널 출력을 그대로
|
|
58
|
+
* 옮겨 적으면 ESC(0x1B)가 딸려 온다. 남겨 두면 한글이 **파일 전체**를 안 여는데,
|
|
59
|
+
* 화면에는 「손상된 문서」 한 줄만 뜬다. 글자 몇 개를 잃는 쪽이 문서를 통째로
|
|
60
|
+
* 잃는 쪽보다 낫다.
|
|
61
|
+
*
|
|
62
|
+
* 처음에는 제어문자(0x00~0x1F)만 걷었다. 그런데 XML 1.0 의 Char 규칙은
|
|
63
|
+
* [#xE000-#xFFFD] 에서 끝난다 — U+FFFE·U+FFFF 도 들어가면 잘 짜인 XML 이 아니다
|
|
64
|
+
* (2.0.0 4회차 사냥). 본문도 제목(content.hpf)도 이 esc 하나를 지나므로 여기서 걷는다.
|
|
65
|
+
*
|
|
66
|
+
* 두 글자는 소스에 `\u` 로 적지 않고 번호로 만든다. 보이지 않는 글자는 소스를
|
|
67
|
+
* 옮기는 도구가 날것으로 바꿔 넣거나 지워 버리는 일이 있다 — 그러면 이 줄이
|
|
68
|
+
* 소리 없이 아무것도 안 걷는다.
|
|
69
|
+
*
|
|
70
|
+
* 짝 없는 서로게이트(0xD800~0xDFFF 낱개)는 여기서 안 건드린다. 짝 맞은 이모지까지
|
|
71
|
+
* 같이 걸리지 않게 하려는 것이고, 낱개는 UTF-8 로 담는 순간 U+FFFD 로 바뀌어
|
|
72
|
+
* 규칙 안 글자가 된다. 0x7F 는 규칙 안이지만 전부터 걷어 왔고 글에 쓸 데가 없다.
|
|
73
|
+
*/
|
|
74
|
+
const XML밖글자 = new RegExp(`[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F${String.fromCharCode(0xfffe, 0xffff)}]`, 'g');
|
|
53
75
|
|
|
54
76
|
const esc = (s) => String(s ?? '')
|
|
55
77
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
56
78
|
.replace(/"/g, '"')
|
|
57
|
-
|
|
58
|
-
* XML 1.0 이 아예 못 담는 제어문자를 걷어낸다.
|
|
59
|
-
*
|
|
60
|
-
* 모델이 낸 글에 이런 것이 섞이는 일이 실제로 있다 — 터미널 출력을 그대로
|
|
61
|
-
* 옮겨 적으면 ESC(0x1B)가 딸려 온다. 남겨 두면 한글이 **파일 전체**를 안 여는데,
|
|
62
|
-
* 화면에는 「손상된 문서」 한 줄만 뜼다. 글자 몇 개를 잃는 쪽이 문서를 통째로
|
|
63
|
-
* 잃는 쪽보다 낫다.
|
|
64
|
-
*/
|
|
65
|
-
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
79
|
+
.replace(XML밖글자, '');
|
|
66
80
|
|
|
67
81
|
/**
|
|
68
82
|
* 글을 문단 목록으로 읽는다.
|
|
@@ -78,21 +92,44 @@ export function 글읽기(글) {
|
|
|
78
92
|
const 줄들 = String(글 ?? '').replace(/\r\n?/g, '\n').split('\n');
|
|
79
93
|
const 문단들 = [];
|
|
80
94
|
let 표몇개 = 0;
|
|
95
|
+
let 표안 = false;
|
|
96
|
+
let 빈줄대기 = false;
|
|
81
97
|
let 이어붙일것 = null;
|
|
82
98
|
|
|
99
|
+
/*
|
|
100
|
+
* 빈 줄도 **옮긴다** (머리말 「옮긴다 … 빈 줄」).
|
|
101
|
+
*
|
|
102
|
+
* 본문 문단 모양은 위아래 여백이 0 이라, 빈 줄을 버리면 md 에서 사이가
|
|
103
|
+
* 벌어져 있던 글이 한글에서는 줄줄이 붙은 한 덩어리로 보인다 — 옮긴다고
|
|
104
|
+
* 적어 두고 하나도 안 넣고 있던 자리다 (8회차).
|
|
105
|
+
*
|
|
106
|
+
* 다만 **문단 사이**의 것만 넣는다. 넣을 자리가 올 때까지 미뤄 두었다가 다음
|
|
107
|
+
* 문단 앞에 한 줄만 세운다. 그래야 문서 맨 앞·맨 뒤나 여러 번 이어진 빈 줄에
|
|
108
|
+
* 빈 문단이 쌓여 한글에서 빈 쪽이 생기는 일이 없다.
|
|
109
|
+
*/
|
|
110
|
+
const 넣기 = (p) => {
|
|
111
|
+
if (빈줄대기) {
|
|
112
|
+
if (문단들.length) 문단들.push({ 갈래: '빈줄', 글: '' });
|
|
113
|
+
빈줄대기 = false;
|
|
114
|
+
}
|
|
115
|
+
문단들.push(p);
|
|
116
|
+
};
|
|
117
|
+
|
|
83
118
|
const 닫기 = () => {
|
|
84
|
-
if (이어붙일것 && 이어붙일것.글.trim())
|
|
119
|
+
if (이어붙일것 && 이어붙일것.글.trim()) 넣기(이어붙일것);
|
|
85
120
|
이어붙일것 = null;
|
|
86
121
|
};
|
|
87
122
|
|
|
88
123
|
for (const 날것 of 줄들) {
|
|
89
124
|
const 줄 = 날것.replace(/\s+$/, '');
|
|
90
|
-
|
|
125
|
+
const 표줄 = /^\s*\|.*\|\s*$/.test(줄);
|
|
126
|
+
if (!표줄) 표안 = false; // 표 아닌 줄을 만나면 그 표는 끝난 것이다
|
|
127
|
+
if (!줄.trim()) { 닫기(); 빈줄대기 = true; continue; }
|
|
91
128
|
|
|
92
129
|
const 제목 = /^(#{1,3})\s+(.*)$/.exec(줄);
|
|
93
130
|
if (제목) {
|
|
94
131
|
닫기();
|
|
95
|
-
|
|
132
|
+
넣기({ 갈래: `제목${제목[1].length}`, 글: 제목[2].trim() });
|
|
96
133
|
continue;
|
|
97
134
|
}
|
|
98
135
|
|
|
@@ -100,12 +137,21 @@ export function 글읽기(글) {
|
|
|
100
137
|
* 표는 줄 글로 내보낸다. 그리고 **몇 개였는지 센다** — 부르는 쪽이
|
|
101
138
|
* 그 수를 사람에게 말한다. 조용히 바꿔치기하면 사람은 표가 글이 된 것을
|
|
102
139
|
* 문서를 열어 보고서야 안다.
|
|
140
|
+
*
|
|
141
|
+
* 세는 것은 구분선이 아니라 **표 덩이**다. 구분선을 세던 때 두 가지가 같이
|
|
142
|
+
* 틀렸다 (8회차) — 구분선 없이 적은 표는 0개가 되어 「표를 줄 글로 폈다」 는
|
|
143
|
+
* 말이 아예 안 나갔고, 칸이 다 빈 줄(`| | |`)은 구분선 규칙에 걸려 표 수를
|
|
144
|
+
* 부풀리면서 그 행까지 통째로 버렸다.
|
|
103
145
|
*/
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
146
|
+
if (표줄) {
|
|
147
|
+
if (!표안) { 표몇개 += 1; 표안 = true; }
|
|
148
|
+
// 구분선(|---|---|)은 글로 옮길 것이 없다. 줄표가 **있어야** 구분선이다.
|
|
149
|
+
if (/^\s*\|[\s:|-]*-[\s:|-]*\|\s*$/.test(줄)) continue;
|
|
107
150
|
닫기();
|
|
108
|
-
|
|
151
|
+
const 칸들 = 줄.replace(/^\s*\|/, '').replace(/\|\s*$/, '').split('|').map((x) => x.trim());
|
|
152
|
+
// 칸이 다 빈 행은 옮길 글이 없다 — 빈 줄로 남긴다.
|
|
153
|
+
if (칸들.some((x) => x)) 넣기({ 갈래: '본문', 글: 칸들.join(' · ') });
|
|
154
|
+
else 빈줄대기 = true;
|
|
109
155
|
continue;
|
|
110
156
|
}
|
|
111
157
|
|
|
@@ -115,7 +161,10 @@ export function 글읽기(글) {
|
|
|
115
161
|
// 한글의 글머리표 규격을 안 쓴다. 문자 하나로 적으면 어느 한글 판에서도
|
|
116
162
|
// 똑같이 보이고, 서식이 어긋날 자리가 없다.
|
|
117
163
|
const 표시 = /^\d/.test(글머리[1]) ? `${글머리[1]} ` : '· ';
|
|
118
|
-
|
|
164
|
+
// 바로 문단에 넣지 않고 **열어 둔다** — 아래 「이어지는 줄은 한 문단으로」 가 글머리 뒤에서도
|
|
165
|
+
// 지켜지게. 바로 넣던 때는 `- 긴 항목\n 이어지는 줄` 이 글머리 하나와 본문 하나로 쪼개졌다
|
|
166
|
+
// (6회차 한글쓰기6ba-b HW2). 빈 줄·제목·표·다음 글머리가 닫는다.
|
|
167
|
+
이어붙일것 = { 갈래: '글머리', 글: `${표시}${글머리[2].trim()}` };
|
|
119
168
|
continue;
|
|
120
169
|
}
|
|
121
170
|
|
|
@@ -127,7 +176,19 @@ export function 글읽기(글) {
|
|
|
127
176
|
닫기();
|
|
128
177
|
|
|
129
178
|
let 잘림 = 0;
|
|
130
|
-
|
|
179
|
+
/*
|
|
180
|
+
* 상한은 **알맹이 문단**으로 센다. 빈 줄은 모델이 쏟아붓는 것이 아니라 사이를
|
|
181
|
+
* 벌리는 것이라, 그것까지 세면 같은 글이 절반만 실리고 「잘랐다」 는 수도 사람이
|
|
182
|
+
* 쓴 문단 수와 안 맞는다.
|
|
183
|
+
*/
|
|
184
|
+
const 알맹이수 = 문단들.filter((p) => p.갈래 !== '빈줄').length;
|
|
185
|
+
if (알맹이수 > 문단수최대) {
|
|
186
|
+
잘림 = 알맹이수 - 문단수최대;
|
|
187
|
+
let 셈 = 0;
|
|
188
|
+
const 끝 = 문단들.findIndex((p) => p.갈래 !== '빈줄' && ++셈 > 문단수최대);
|
|
189
|
+
if (끝 >= 0) 문단들.length = 끝;
|
|
190
|
+
while (문단들.length && 문단들[문단들.length - 1].갈래 === '빈줄') 문단들.pop();
|
|
191
|
+
}
|
|
131
192
|
for (const p of 문단들) if (p.글.length > 문단최대) p.글 = `${p.글.slice(0, 문단최대)} …(잘림)`;
|
|
132
193
|
return { 문단들, 잘림, 표몇개 };
|
|
133
194
|
}
|