deel-local-cli 1.20.7 → 1.20.9
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 +4 -2
- package/README.md +4 -2
- package/bin/deel.js +27 -0
- package/package.json +1 -1
- package/src/agent/compact.js +103 -8
- package/src/agent/evolve.js +18 -1
- package/src/agent/models.js +22 -0
- package/src/agent/modes.js +67 -7
- package/src/agent/session.js +12 -1
- package/src/backend/adapter.js +93 -9
- package/src/backend/mcp.js +108 -13
- package/src/backend/probe.js +108 -14
- package/src/backend/wire.js +18 -0
- package/src/commands.js +203 -20
- package/src/i18n/en.js +3 -1
- package/src/i18n/ko.js +3 -1
- package/src/repl.js +13 -2
- package/src/tools/jobs.js +85 -7
- package/src/tools/pdf.js +168 -44
- package/src/ui/ansi.js +30 -8
- package/src/ui/inputbox.js +105 -25
- package/src/ui/level.js +9 -1
- package/src/ui/office.js +16 -2
- package/src/ui/status.js +26 -3
package/src/tools/pdf.js
CHANGED
|
@@ -87,8 +87,10 @@ function 이름읽기(b, i) {
|
|
|
87
87
|
const 조각 = [];
|
|
88
88
|
while (i < b.length && !끝인가(b[i])) {
|
|
89
89
|
if (b[i] === 0x23 && i + 2 < b.length) {
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
// 두 자리 다 16진수여야 한다. `parseInt('1/',16)` 은 1 을 돌려주므로,
|
|
91
|
+
// 그것만 보면 뒤따라온 구분자 `/` 까지 삼켜 다음 낱말이 사라진다.
|
|
92
|
+
const 두자리 = String.fromCharCode(b[i + 1], b[i + 2]);
|
|
93
|
+
if (/^[0-9A-Fa-f]{2}$/.test(두자리)) { 조각.push(parseInt(두자리, 16)); i += 3; continue; }
|
|
92
94
|
}
|
|
93
95
|
조각.push(b[i]); i += 1;
|
|
94
96
|
}
|
|
@@ -423,6 +425,7 @@ class 문서 {
|
|
|
423
425
|
this.자리 = new Map(); // 객체번호 → 파일 위치
|
|
424
426
|
this.푼것 = new Map(); // 객체번호 → 읽어 둔 값
|
|
425
427
|
this.묶음속 = new Map(); // 객체번호 → ObjStm 안에서 읽은 값
|
|
428
|
+
this.xref정함 = new Set(); // 최신 xref 가 자리를 정해 준 객체 (옛 판이 못 덮는다)
|
|
426
429
|
this.trailer = {};
|
|
427
430
|
this.훑기();
|
|
428
431
|
this.xref읽기();
|
|
@@ -435,11 +438,39 @@ class 문서 {
|
|
|
435
438
|
const 무늬 = /(?:^|[\s>\]])(\d+)\s+(\d+)\s+obj\b/g;
|
|
436
439
|
let m;
|
|
437
440
|
while ((m = 무늬.exec(s)) !== null) {
|
|
438
|
-
|
|
439
|
-
|
|
441
|
+
/*
|
|
442
|
+
* 무늬가 앞 글자 한 칸을 같이 물었는지만 보면 된다.
|
|
443
|
+
*
|
|
444
|
+
* 여기서 `${m[1]} ${m[2]} obj`.length 를 빼는 셈을 하면 **공백이 한 칸일
|
|
445
|
+
* 때만** 맞는다. `4 0\r\nobj` 처럼 줄바꿈이 끼면 자리가 한 칸 밀려
|
|
446
|
+
* 「0\r\nobj」 를 가리키고, 그러면 그 객체를 못 읽는다. xref 까지 틀린
|
|
447
|
+
* 파일에서는 이 훑기가 마지막 안전망인데 그 안전망에 구멍이 났다.
|
|
448
|
+
*/
|
|
449
|
+
this.자리.set(Number(m[1]), m.index + (/^\d/.test(m[0]) ? 0 : 1));
|
|
440
450
|
}
|
|
441
451
|
}
|
|
442
452
|
|
|
453
|
+
/**
|
|
454
|
+
* xref 가 말하는 자리를 받아 적는다.
|
|
455
|
+
*
|
|
456
|
+
* xref 는 **최신 판부터** 거슬러 읽는다(startxref → /Prev). 그래서 한 번
|
|
457
|
+
* 정해진 객체를 나중에(= 더 옛 판에서) 다시 적으면 안 된다. 증분 저장한
|
|
458
|
+
* PDF 는 고친 객체가 파일 뒤에 새로 붙고 옛것이 그대로 남아 있는데, 옛
|
|
459
|
+
* 자리로 덮으면 **고치기 전 값을 읽는다** — 폼에 채운 값이 빈칸으로,
|
|
460
|
+
* 고친 금액이 옛 금액으로 돌아간다. 빈 글은 보이지만 옛 글은 안 보인다.
|
|
461
|
+
*/
|
|
462
|
+
자리정하기(번호, 자리) {
|
|
463
|
+
if (this.xref정함.has(번호)) return;
|
|
464
|
+
// 훑기가 찾은 자리를 못 믿을 때만 xref 를 쓴다. 둘 다 있으면 그 자리에
|
|
465
|
+
// 진짜 `N obj` 가 있는 쪽을 고른다.
|
|
466
|
+
if (this.진짜객체인가(자리, 번호)) {
|
|
467
|
+
this.자리.set(번호, 자리);
|
|
468
|
+
this.xref정함.add(번호);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (!this.자리.has(번호)) this.자리.set(번호, 자리);
|
|
472
|
+
}
|
|
473
|
+
|
|
443
474
|
/** ② xref 를 따라가며 trailer 를 모은다. */
|
|
444
475
|
xref읽기() {
|
|
445
476
|
const s = this.b.toString('latin1');
|
|
@@ -460,7 +491,10 @@ class 문서 {
|
|
|
460
491
|
}
|
|
461
492
|
if (!this.trailer.Root) {
|
|
462
493
|
// trailer 를 못 찾았으면 카탈로그를 직접 찾는다.
|
|
463
|
-
|
|
494
|
+
// `[^>]` 로 막으면 카탈로그 앞에 속사전(`<< … >>`)이 하나라도 있으면
|
|
495
|
+
// 못 찾는다. endobj 만 넘지 않도록 해서 그 안쪽까지 본다.
|
|
496
|
+
const m = /(\d+)\s+\d+\s+obj\s*<<(?:(?!endobj)[\s\S]){0,400}?\/Type\s*\/Catalog/
|
|
497
|
+
.exec(this.b.toString('latin1'));
|
|
464
498
|
if (m) this.trailer.Root = { 참조: Number(m[1]), 세대: 0 };
|
|
465
499
|
}
|
|
466
500
|
}
|
|
@@ -482,23 +516,25 @@ class 문서 {
|
|
|
482
516
|
let 번호 = Number(m[1]);
|
|
483
517
|
const 수 = Number(m[2]);
|
|
484
518
|
i = 건너뛰기(this.b, i + m[0].length);
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
519
|
+
/*
|
|
520
|
+
* 한 줄은 20자라고 규격에 적혀 있지만 19자로 쓰는 프로그램이 있다
|
|
521
|
+
* (줄 끝을 `\n` 하나로 적는다). 그래서 20 씩 더하지 않고 **줄을 읽은
|
|
522
|
+
* 만큼** 나아간다. 앞서 20 씩 더하던 코드는 19자 파일에서 둘째 줄부터
|
|
523
|
+
* 한 자씩 밀려 표 전체를 잃었고, 뒤에 붙어 있던 「줄을 맞춘다」 는
|
|
524
|
+
* 그 자리에서 곧바로 빠져나가 한 번도 맞춰 준 적이 없다.
|
|
525
|
+
*/
|
|
526
|
+
for (let k = 0; k < 수 && i < this.b.length; k += 1) {
|
|
527
|
+
while (i < this.b.length && 공백인가(this.b[i])) i += 1;
|
|
528
|
+
const mm = /^(\d{10})\s(\d{5})\s([nf])/.exec(this.b.toString('latin1', i, i + 20));
|
|
529
|
+
if (!mm) {
|
|
530
|
+
// 규격에서 벗어난 줄 — 다음 줄머리로 넘긴다.
|
|
531
|
+
while (i < this.b.length && this.b[i] !== 0x0a && this.b[i] !== 0x0d) i += 1;
|
|
532
|
+
번호 += 1;
|
|
533
|
+
continue;
|
|
494
534
|
}
|
|
535
|
+
if (mm[3] === 'n') this.자리정하기(번호, Number(mm[1]));
|
|
495
536
|
번호 += 1;
|
|
496
|
-
i +=
|
|
497
|
-
// 줄이 19자인 파일도 있다. 다음 줄 머리를 보고 맞춘다.
|
|
498
|
-
while (i < this.b.length && 공백인가(this.b[i]) && !/\d/.test(String.fromCharCode(this.b[i]))) {
|
|
499
|
-
if (this.b[i] === 0x0a || this.b[i] === 0x0d || this.b[i] === 0x20) break;
|
|
500
|
-
i += 1;
|
|
501
|
-
}
|
|
537
|
+
i += mm[0].length;
|
|
502
538
|
}
|
|
503
539
|
}
|
|
504
540
|
}
|
|
@@ -539,9 +575,12 @@ class 문서 {
|
|
|
539
575
|
밭.push(w === 0 ? null : v);
|
|
540
576
|
}
|
|
541
577
|
const 갈래 = 밭[0] === null ? 1 : 밭[0];
|
|
542
|
-
if (갈래 === 1
|
|
543
|
-
else if (갈래 ===
|
|
544
|
-
|
|
578
|
+
if (갈래 === 1) this.자리정하기(번호, 밭[1]);
|
|
579
|
+
else if (갈래 === 2 && !this.xref정함.has(번호)) {
|
|
580
|
+
// 여기도 최신 판이 이긴다 — 옛 ObjStm 이 고친 객체를 덮으면 안 된다.
|
|
581
|
+
this.묶음자리 = this.묶음자리 ?? new Map();
|
|
582
|
+
if (!this.묶음자리.has(번호)) this.묶음자리.set(번호, 밭[1]);
|
|
583
|
+
}
|
|
545
584
|
번호 += 1;
|
|
546
585
|
}
|
|
547
586
|
}
|
|
@@ -578,6 +617,16 @@ class 문서 {
|
|
|
578
617
|
const 안자리 = 수들[k * 2 + 1];
|
|
579
618
|
if (!Number.isInteger(객체번호) || !Number.isInteger(안자리)) continue;
|
|
580
619
|
if (this.묶음속.has(객체번호)) continue;
|
|
620
|
+
/*
|
|
621
|
+
* 최신 xref 가 「이 객체는 파일에 낱개로 있다」 고 정해 준 것은
|
|
622
|
+
* 묶음이 덮지 못한다.
|
|
623
|
+
*
|
|
624
|
+
* 눌린 PDF(1.5+)를 고쳐 저장하면 고친 객체만 **낱개로** 뒤에
|
|
625
|
+
* 덧붙는다. 그때 옛 묶음도 파일에 그대로 남아 있어서, 묶음을 펼쳐
|
|
626
|
+
* 덮어 버리면 아래 풀기() 가 묶음 쪽을 먼저 보므로 **고치기 전 값을
|
|
627
|
+
* 읽는다.** 자리만 지켜 놓고 여기를 안 지키면 반쪽만 고친 것이다.
|
|
628
|
+
*/
|
|
629
|
+
if (this.xref정함.has(객체번호)) continue;
|
|
581
630
|
const [값] = 값읽기(r.자료, 첫 + 안자리, (x) => this.풀기(x));
|
|
582
631
|
this.묶음속.set(객체번호, 값);
|
|
583
632
|
}
|
|
@@ -708,13 +757,29 @@ function 열여섯값(글) {
|
|
|
708
757
|
return s ? parseInt(s, 16) : null;
|
|
709
758
|
}
|
|
710
759
|
|
|
711
|
-
/**
|
|
760
|
+
/**
|
|
761
|
+
* `<0041>` 을 유니코드 글자로 (UTF-16BE 여러 글자일 수 있다).
|
|
762
|
+
*
|
|
763
|
+
* 넷씩 끊어 읽고, 모자란 자리는 **맨 앞**을 0 으로 메운다. 뒤를 메우면
|
|
764
|
+
* `<41>`('A') 가 `4100` — 곧 「䄀」 이라는 엉뚱한 한자가 된다. 두 자리로 적는
|
|
765
|
+
* 파일이 규격 위반이긴 하지만, 위반을 만났을 때 없는 한자를 만들어 내는 것과
|
|
766
|
+
* 「A」 를 돌려주는 것 중에 무엇이 나은지는 분명하다.
|
|
767
|
+
*
|
|
768
|
+
* 메우는 자리가 **맨 앞 한 곳**이어야 한다. 뒤쪽 토막만 메우면 `<410042>` 가
|
|
769
|
+
* `4100`·`0042` 로 끊겨 첫 글자가 다시 「䄀」 이 된다 — 고친 자리가 도로
|
|
770
|
+
* 살아난다.
|
|
771
|
+
*/
|
|
712
772
|
function 열여섯글자(글) {
|
|
713
773
|
const s = String(글 ?? '').replace(/[^0-9A-Fa-f]/g, '');
|
|
714
774
|
if (!s) return '';
|
|
715
|
-
const
|
|
775
|
+
const 남 = s.length % 4;
|
|
776
|
+
const 짝 = 남 ? s.padStart(s.length + (4 - 남), '0') : s;
|
|
716
777
|
let 나온것 = '';
|
|
717
|
-
for (let i = 0; i < 짝.length; i += 4)
|
|
778
|
+
for (let i = 0; i < 짝.length; i += 4) {
|
|
779
|
+
const v = parseInt(짝.slice(i, i + 4), 16);
|
|
780
|
+
// `<0000>` 은 「글자 없음」 이다. 글에 NUL 을 섞으면 화면과 모델 쪽이 같이 깨진다.
|
|
781
|
+
if (v) 나온것 += String.fromCharCode(v);
|
|
782
|
+
}
|
|
718
783
|
return 나온것;
|
|
719
784
|
}
|
|
720
785
|
|
|
@@ -742,14 +807,24 @@ export function 유니코드표읽기(글) {
|
|
|
742
807
|
}
|
|
743
808
|
}
|
|
744
809
|
for (const m of s.matchAll(/beginbfrange([\s\S]*?)endbfrange/g)) {
|
|
745
|
-
const
|
|
810
|
+
const 배열꼴 = /<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>\s*\[([\s\S]*?)\]/g;
|
|
746
811
|
// `<시작> <끝> [<a> <b> …]` 꼴
|
|
747
|
-
for (const p of
|
|
812
|
+
for (const p of m[1].matchAll(배열꼴)) {
|
|
748
813
|
const 시작 = 열여섯값(p[1]);
|
|
749
814
|
코드폭.add(Math.ceil(p[1].length / 2));
|
|
750
815
|
const 것들 = [...p[3].matchAll(/<([0-9A-Fa-f]+)>/g)].map((x) => 열여섯글자(x[1]));
|
|
751
816
|
것들.forEach((글자, k) => 표.set(시작 + k, 글자));
|
|
752
817
|
}
|
|
818
|
+
/*
|
|
819
|
+
* 배열 꼴을 읽은 자리는 지우고 넘긴다.
|
|
820
|
+
*
|
|
821
|
+
* 남겨 두면 아래 무늬가 **대괄호 안의 글자 셋을 또 하나의 범위로** 읽는다.
|
|
822
|
+
* `<0020> <0022> [<0041> <0042> <0043>]` 에서 `<0041> <0042> <0043>` 이
|
|
823
|
+
* 다시 걸려, 코드 0x41·0x42 에 「C」「D」 라는 없는 매핑이 생긴다.
|
|
824
|
+
* Identity-H 문서에서 0x41 은 글리프 번호이므로 — 못 읽은 글자가 되는
|
|
825
|
+
* 대신 **엉뚱한 글자로 읽힌다.** 빠진 글자는 보이지만 틀린 글자는 안 보인다.
|
|
826
|
+
*/
|
|
827
|
+
const 몸 = m[1].replace(배열꼴, ' ');
|
|
753
828
|
// `<시작> <끝> <첫글자>` 꼴
|
|
754
829
|
for (const p of 몸.matchAll(/<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>/g)) {
|
|
755
830
|
const 시작 = 열여섯값(p[1]);
|
|
@@ -757,6 +832,7 @@ export function 유니코드표읽기(글) {
|
|
|
757
832
|
const 첫 = 열여섯글자(p[3]);
|
|
758
833
|
코드폭.add(Math.ceil(p[1].length / 2));
|
|
759
834
|
if (시작 === null || 끝 === null || 끝 < 시작 || 끝 - 시작 > 65535) continue;
|
|
835
|
+
if (!첫) continue; // `<0000>` 에서 시작하는 범위 — 셀 밑자리가 없다
|
|
760
836
|
const 밑 = 첫.charCodeAt(첫.length - 1);
|
|
761
837
|
for (let k = 0; k <= 끝 - 시작; k += 1) {
|
|
762
838
|
if (표.has(시작 + k)) continue;
|
|
@@ -906,11 +982,8 @@ function 글자로(글꼴, 바이트들) {
|
|
|
906
982
|
let 글 = '';
|
|
907
983
|
let 못읽음 = 0;
|
|
908
984
|
let 나아감 = 0;
|
|
909
|
-
for (let i = 0; i + 폭 <= 바이트들.length
|
|
910
|
-
const 코드 = 폭 === 2
|
|
911
|
-
? ((바이트들[i] << 8) | (바이트들[i + 1] ?? 0))
|
|
912
|
-
: 바이트들[i];
|
|
913
|
-
if (코드 === undefined) break;
|
|
985
|
+
for (let i = 0; i + 폭 <= 바이트들.length; i += 폭) {
|
|
986
|
+
const 코드 = 폭 === 2 ? ((바이트들[i] << 8) | 바이트들[i + 1]) : 바이트들[i];
|
|
914
987
|
const g = 글꼴?.표?.get(코드);
|
|
915
988
|
let 찍은것 = null;
|
|
916
989
|
if (g !== undefined && g !== null && g !== '') { 글 += g; 찍은것 = g; }
|
|
@@ -922,6 +995,14 @@ function 글자로(글꼴, 바이트들) {
|
|
|
922
995
|
else if (typeof 너비기본 === 'number') 나아감 += 너비기본;
|
|
923
996
|
else 나아감 += 찍은것 ? 글자너비비율(찍은것) : 넓은글자;
|
|
924
997
|
}
|
|
998
|
+
/*
|
|
999
|
+
* 반 토막 난 마지막 코드.
|
|
1000
|
+
*
|
|
1001
|
+
* 두 바이트 글꼴에 홀수 길이 글자열이 오면 규격 위반이다. 여기에 0 을 붙여
|
|
1002
|
+
* 두 바이트로 만들면 **없는 글리프 번호**가 생기고, 그것이 어쩌다 표에
|
|
1003
|
+
* 있으면 엉뚱한 글자가 글에 섞인다. 못 읽은 것으로 세는 쪽이 정직하다.
|
|
1004
|
+
*/
|
|
1005
|
+
if (바이트들.length % 폭) 못읽음 += 1;
|
|
925
1006
|
return { 글, 못읽음, 나아감 };
|
|
926
1007
|
}
|
|
927
1008
|
|
|
@@ -960,6 +1041,27 @@ function 곱하기(m, n) {
|
|
|
960
1041
|
];
|
|
961
1042
|
}
|
|
962
1043
|
|
|
1044
|
+
/**
|
|
1045
|
+
* 붙박이 그림(BI … ID … EI)이 끝나는 자리를 찾는다.
|
|
1046
|
+
*
|
|
1047
|
+
* 그림 자료는 눌린 바이트라 그 안에 우연히 `EI` 두 글자가 들어 있을 수 있다.
|
|
1048
|
+
* 그 자리에서 끊으면 그림 한가운데부터 낱말을 읽기 시작하고, 자료 안의 `(`
|
|
1049
|
+
* 를 글자 시작으로 잘못 읽어 **그 쪽에 남은 글을 통째로 삼킨다.** 그러면서
|
|
1050
|
+
* 앞부분은 읽혔으니 「못 읽은 쪽」에도 안 들어간다 — 아무 말 없이 사라진다.
|
|
1051
|
+
*
|
|
1052
|
+
* 규격대로 앞이 공백이고 뒤가 공백·구분자인 자리만 끝으로 본다. 그런 자리가
|
|
1053
|
+
* 없으면 여태처럼 첫 `EI` 로 물러선다(끊긴 그림이 있는 파일).
|
|
1054
|
+
*/
|
|
1055
|
+
function 그림끝(b, i) {
|
|
1056
|
+
let k = i;
|
|
1057
|
+
for (;;) {
|
|
1058
|
+
k = b.indexOf('EI', k, 'latin1');
|
|
1059
|
+
if (k < 0) return -1;
|
|
1060
|
+
if (공백인가(b[k - 1]) && 끝인가(b[k + 2])) return k + 2;
|
|
1061
|
+
k += 2;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
963
1065
|
/**
|
|
964
1066
|
* 내용 흐름 하나에서 글을 뽑는다.
|
|
965
1067
|
*
|
|
@@ -975,6 +1077,7 @@ function 흐름에서글(문, 자료, 자원, 깊이 = 0) {
|
|
|
975
1077
|
let 펜x = null; // 붓이 지금 어디까지 갔다고 보는가 (자리바뀜 참고)
|
|
976
1078
|
let 못읽은코드 = 0;
|
|
977
1079
|
let 글낸적 = false;
|
|
1080
|
+
let 그림낸적 = false;
|
|
978
1081
|
const 글꼴통 = new Map();
|
|
979
1082
|
|
|
980
1083
|
let Tm = [1, 0, 0, 1, 0, 0];
|
|
@@ -1029,9 +1132,10 @@ function 흐름에서글(문, 자료, 자원, 깊이 = 0) {
|
|
|
1029
1132
|
|
|
1030
1133
|
// 붙박이 그림은 통째로 건너뛴다. 안 그러면 그림 자료를 낱말로 읽는다.
|
|
1031
1134
|
if (b[i] === 0x42 && b[i + 1] === 0x49 && 끝인가(b[i + 2])) { // 'BI'
|
|
1032
|
-
const 끝 = b
|
|
1033
|
-
|
|
1034
|
-
|
|
1135
|
+
const 끝 = 그림끝(b, i);
|
|
1136
|
+
const 물러선것 = 끝 < 0 ? b.indexOf('EI', i, 'latin1') : -1;
|
|
1137
|
+
i = 끝 >= 0 ? 끝 : (물러선것 < 0 ? b.length : 물러선것 + 2);
|
|
1138
|
+
그림낸적 = true;
|
|
1035
1139
|
continue;
|
|
1036
1140
|
}
|
|
1037
1141
|
|
|
@@ -1046,8 +1150,17 @@ function 흐름에서글(문, 자료, 자원, 깊이 = 0) {
|
|
|
1046
1150
|
쌓임.length = 0;
|
|
1047
1151
|
|
|
1048
1152
|
switch (op) {
|
|
1049
|
-
|
|
1050
|
-
|
|
1153
|
+
/*
|
|
1154
|
+
* BT·ET 는 줄이 아니다.
|
|
1155
|
+
*
|
|
1156
|
+
* 한 줄 안에서 낱말 하나만 굵게 하거나 색을 바꾸려면 그 낱말만 BT…ET
|
|
1157
|
+
* 로 따로 싣는 조판이 흔하다. 그때 ET 마다 줄을 맺으면
|
|
1158
|
+
* 「Total 1,000,000 won」 이 세 줄로 쪼개진다. 줄이 바뀌었는지는 ET 가
|
|
1159
|
+
* 아니라 **세로로 움직였는지**가 말해 준다 — 그래서 앞자리를 그대로
|
|
1160
|
+
* 들고 다음 덩이의 첫 자리와 견준다.
|
|
1161
|
+
*/
|
|
1162
|
+
case 'BT': Tm = [1, 0, 0, 1, 0, 0]; Tlm = Tm.slice(); break;
|
|
1163
|
+
case 'ET': break;
|
|
1051
1164
|
case 'TL': TL = Number(인자[0]) || 0; break;
|
|
1052
1165
|
case 'Td': {
|
|
1053
1166
|
const tx = Number(인자[0]) || 0; const ty = Number(인자[1]) || 0;
|
|
@@ -1092,14 +1205,17 @@ function 흐름에서글(문, 자료, 자원, 깊이 = 0) {
|
|
|
1092
1205
|
break;
|
|
1093
1206
|
}
|
|
1094
1207
|
case 'Do': {
|
|
1095
|
-
// 폼 XObject 안에도 글이 있다 (머리말·표·도장). 깊이는 막아 둔다.
|
|
1096
|
-
if (깊이 >= 4) break;
|
|
1097
1208
|
const 이름 = 인자[0]?.이름;
|
|
1098
1209
|
if (!이름) break;
|
|
1099
1210
|
const 것들 = 문.꺼내기(자원, 'XObject');
|
|
1100
1211
|
const 하나 = 문.풀기((것들?.사전 ?? 것들)?.[이름]);
|
|
1212
|
+
// 폼이 아니면 그림(또는 알 수 없는 것)을 그린 것이다. 그리기를 본
|
|
1213
|
+
// 이 자리가 「스캔본인가」 를 아는 유일한 자리다 — 자원에 /XObject 가
|
|
1214
|
+
// 있다는 것만으로는 **그렸는지**를 알 수 없다.
|
|
1215
|
+
if (문.풀기(하나?.사전?.Subtype)?.이름 !== 'Form') { 그림낸적 = true; break; }
|
|
1216
|
+
// 폼 XObject 안에도 글이 있다 (머리말·표·도장). 깊이는 막아 둔다.
|
|
1217
|
+
if (깊이 >= 4) break;
|
|
1101
1218
|
if (!하나?.날것) break;
|
|
1102
|
-
if (문.풀기(하나.사전?.Subtype)?.이름 !== 'Form') break;
|
|
1103
1219
|
const r = 흐름풀기(하나, (x) => 문.풀기(x));
|
|
1104
1220
|
if (!r.ok) break;
|
|
1105
1221
|
const 속자원 = 문.풀기(하나.사전?.Resources) ?? 자원;
|
|
@@ -1108,13 +1224,14 @@ function 흐름에서글(문, 자료, 자원, 깊이 = 0) {
|
|
|
1108
1224
|
줄들.push(...안것.줄들);
|
|
1109
1225
|
못읽은코드 += 안것.못읽은코드;
|
|
1110
1226
|
글낸적 = 글낸적 || 안것.글낸적;
|
|
1227
|
+
그림낸적 = 그림낸적 || 안것.그림낸적;
|
|
1111
1228
|
break;
|
|
1112
1229
|
}
|
|
1113
1230
|
default: break;
|
|
1114
1231
|
}
|
|
1115
1232
|
}
|
|
1116
1233
|
줄맺기();
|
|
1117
|
-
return { 줄들, 못읽은코드,
|
|
1234
|
+
return { 줄들, 못읽은코드, 글낸적, 그림낸적 };
|
|
1118
1235
|
}
|
|
1119
1236
|
|
|
1120
1237
|
/* ────────────────────────────────────────────────────────────────────────
|
|
@@ -1228,8 +1345,15 @@ export function readPdf(경로또는버퍼) {
|
|
|
1228
1345
|
* 글이 하나도 안 나온 쪽. 왜인지 갈라서 말한다 — 스캔본과 글꼴 문제는
|
|
1229
1346
|
* 사람이 할 일이 다르다(전자는 OCR, 후자는 다시 뽑기).
|
|
1230
1347
|
*/
|
|
1231
|
-
|
|
1232
|
-
|
|
1348
|
+
/*
|
|
1349
|
+
* 「그림은 그렸고 글은 없다」 = 스캔본일 수 있다 → OCR.
|
|
1350
|
+
* 「아무것도 안 그렸다」 = 그냥 빈 쪽이다 → OCR 은 할 일이 없다.
|
|
1351
|
+
*
|
|
1352
|
+
* 앞서는 자원 사전에 /XObject 가 **있기만 해도** 스캔본으로 적었다.
|
|
1353
|
+
* 머리말 로고가 위쪽 자원에 물려 있으면 정말 빈 쪽도 「OCR 이 필요」
|
|
1354
|
+
* 가 됐다. 그린 것을 본 쪽은 파서다.
|
|
1355
|
+
*/
|
|
1356
|
+
const 그림있나 = 뽑은것.그림낸적;
|
|
1233
1357
|
못읽은쪽.push({
|
|
1234
1358
|
번호,
|
|
1235
1359
|
왜: 뽑은것.못읽은코드
|
package/src/ui/ansi.js
CHANGED
|
@@ -126,19 +126,41 @@ export function pad(str, target, align = 'left') {
|
|
|
126
126
|
return str + ' '.repeat(gap);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
/**
|
|
130
|
+
* 색 코드를 건드리지 않고 보이는 폭 기준으로 자른다.
|
|
131
|
+
*
|
|
132
|
+
* 색 코드는 화면에서 자리를 차지하지 않으므로 폭에서 빼야 하고, 그 가운데를
|
|
133
|
+
* 자르면 안 된다. 한 글자씩 셈에 넣으면 둘 다 어긋난다 — 회색 한 줄
|
|
134
|
+
* `\x1b[38;5;245m…\x1b[0m` 은 색 코드만으로 열두 칸을 먹어서, 폭 12 로
|
|
135
|
+
* 자르면 보이는 글자가 한 칸만 남고 잘린 자리가 `\x1b[38;5;` 라는 토막이
|
|
136
|
+
* 된다. 터미널은 그 토막을 명령의 시작으로 보고 뒤따르는 글자를 먹는다.
|
|
137
|
+
*
|
|
138
|
+
* 그래서 색 코드는 통째로 넘기고 폭 0 으로 센다. 자른 자리에서 색이 켜져
|
|
139
|
+
* 있으면 되돌림을 붙인다 — 안 붙이면 그 색이 뒤에 찍는 모든 줄로 번진다.
|
|
140
|
+
*/
|
|
130
141
|
export function clip(str, max, tail = '…') {
|
|
131
142
|
if (width(str) <= max) return str;
|
|
143
|
+
const budget = max - width(tail);
|
|
132
144
|
let out = '';
|
|
133
145
|
let w = 0;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
let 색켜짐 = false;
|
|
147
|
+
let 찼다 = false;
|
|
148
|
+
for (const 조각 of String(str).split(/(\x1b\[[0-9;]*m)/)) {
|
|
149
|
+
if (!조각) continue;
|
|
150
|
+
if (찼다) break;
|
|
151
|
+
if (조각.charCodeAt(0) === 0x1b) {
|
|
152
|
+
out += 조각;
|
|
153
|
+
색켜짐 = !/^\x1b\[0?m$/.test(조각);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
for (const ch of 조각) {
|
|
157
|
+
const cw = width(ch);
|
|
158
|
+
if (w + cw > budget) { 찼다 = true; break; }
|
|
159
|
+
out += ch;
|
|
160
|
+
w += cw;
|
|
161
|
+
}
|
|
140
162
|
}
|
|
141
|
-
return out + tail;
|
|
163
|
+
return out + (색켜짐 ? '\x1b[0m' : '') + tail;
|
|
142
164
|
}
|
|
143
165
|
|
|
144
166
|
// 터미널 가로 폭. 파이프로 넘어가면 알 수 없으니 넉넉히 잡는다.
|
package/src/ui/inputbox.js
CHANGED
|
@@ -72,6 +72,8 @@ const 자가치유주기 = 4000;
|
|
|
72
72
|
const 앞머리 = 5;
|
|
73
73
|
// 상자가 차지할 최대 줄 수. 긴 글을 붙여넣어도 화면을 다 먹지 않게.
|
|
74
74
|
export const 안쪽최대 = 8;
|
|
75
|
+
// 일하는 중에 미리 쳐 두는 글은 이보다 좁게 보인다 — 위쪽 '일하는 중' 줄을 안 밀어내게.
|
|
76
|
+
export const 대기최대 = 3;
|
|
75
77
|
|
|
76
78
|
/**
|
|
77
79
|
* 상자를 이룰 줄들과 커서 자리를 값으로 내놓는다.
|
|
@@ -117,8 +119,23 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
|
|
|
117
119
|
*/
|
|
118
120
|
if (사무실?.length) 줄들.push(...사무실);
|
|
119
121
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
/*
|
|
123
|
+
* ── 여기 얹는 줄은 **반드시 폭 안에 넣는다** ─────────────────────────
|
|
124
|
+
*
|
|
125
|
+
* 상자는 아래 몇 줄을 붙박아 두고 그 자리에서 다시 그린다 — 그 셈이
|
|
126
|
+
* `줄들.length` 다. 한 줄이 터미널보다 넓으면 터미널은 그것을 두 줄로
|
|
127
|
+
* 접는데 상자는 한 줄로 세고, 그만큼 커서를 덜 올린다. 그러면 키를 칠
|
|
128
|
+
* 때마다 위쪽 대화가 한 줄씩 갉여 나가고 테두리 잔상이 쌓인다.
|
|
129
|
+
*
|
|
130
|
+
* 실제로 재 본 자리: 폭 80 에서 할당량 경고가 101칸이었다. 상자는 5줄로
|
|
131
|
+
* 셌고 터미널은 7줄을 썼다.
|
|
132
|
+
*
|
|
133
|
+
* 상태줄은 자기 자리에서 이미 줄이지만(ui/status.js), 덩이 하나가 폭보다
|
|
134
|
+
* 넓으면 그대로 나온다 — 그래서 여기서 한 번 더 자른다. 경고는 뜻이
|
|
135
|
+
* 뒤쪽에 있는 문장이라 자르지 않고 **접는다.**
|
|
136
|
+
*/
|
|
137
|
+
if (상태) 줄들.push(clip(상태, 칸));
|
|
138
|
+
if (경고) for (const 줄 of 접어쓰기(` ${경고}`, 칸)) 줄들.push(줄);
|
|
122
139
|
|
|
123
140
|
const 가로 = '─'.repeat(칸 - 3);
|
|
124
141
|
줄들.push(테(` ╭${가로}╮`));
|
|
@@ -144,8 +161,16 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
|
|
|
144
161
|
const 왼쪽 = `${c.hcyan(일감.돌림 ?? '⠋')} ${c.white(일감.말 ?? '')}${c.gray('…')}`;
|
|
145
162
|
const 오른쪽 = c.gray(일감.곁 ?? '');
|
|
146
163
|
const 남 = 일안쪽 - width(왼쪽) - width(오른쪽);
|
|
147
|
-
|
|
148
|
-
|
|
164
|
+
/*
|
|
165
|
+
* 자리가 모자라면 오른쪽(걸린 시간·중단 안내)을 버린다. 왼쪽이 본문이다.
|
|
166
|
+
*
|
|
167
|
+
* 오른쪽을 버려도 **왼쪽 하나가 폭보다 넓을 수 있다.** 일감의 말은 부르는
|
|
168
|
+
* 쪽에서 오는 글이라(하위 작업 목적·도구 이름·파일 경로) 길이를 여기서
|
|
169
|
+
* 정하지 못한다. 안 자르고 내보내면 위 머리글이 적어 둔 그 손해가 그대로
|
|
170
|
+
* 난다 — 터미널은 두 줄로 접고 상자는 한 줄로 세서, 키를 칠 때마다
|
|
171
|
+
* 위쪽 대화가 한 줄씩 갉여 나간다.
|
|
172
|
+
*/
|
|
173
|
+
const 안 = 남 >= 2 ? `${왼쪽}${' '.repeat(남)}${오른쪽}` : clip(왼쪽, 일안쪽);
|
|
149
174
|
줄들.push(` ${테('│')} ${안}${' '.repeat(Math.max(0, 일안쪽 - width(안)))} ${테('│')}`);
|
|
150
175
|
|
|
151
176
|
/*
|
|
@@ -157,22 +182,31 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
|
|
|
157
182
|
* 아무것도 안 보여서, 쳐도 안 먹는 줄 알았을 뿐이다.
|
|
158
183
|
*
|
|
159
184
|
* 그래서 보여만 준다. 새로 받는 게 아니라 **이미 받고 있던 것**이다.
|
|
185
|
+
*
|
|
186
|
+
* 세 줄을 넘으면 **뒤에서 세 줄**을 보인다.
|
|
187
|
+
*
|
|
188
|
+
* 앞서는 앞에서 세 줄을 보였다. 사람은 끝에서 치고 있는데 화면에는 처음
|
|
189
|
+
* 세 줄이 붙어 있으니, 네 줄째부터는 친 글자가 아무 데도 안 나타났다 —
|
|
190
|
+
* 그리고 커서는 셋째 줄 끝에 서 있었다. 키가 안 먹는 것처럼 보이는데
|
|
191
|
+
* 실은 다 받고 있었다. 가린 줄이 있으면 ❯ 를 … 로 바꿔 알린다
|
|
192
|
+
* (아래 본 상자가 쓰는 것과 같은 표시다).
|
|
160
193
|
*/
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
194
|
+
const 대기줄들 = 일감.대기 ? 접어쓰기(일감.대기, 안쪽) : [];
|
|
195
|
+
const 대기보일 = 대기줄들.slice(-대기최대);
|
|
196
|
+
const 대기넘침 = 대기줄들.length > 대기최대;
|
|
197
|
+
for (const [i, 한줄] of 대기보일.entries()) {
|
|
198
|
+
const 표 = i === 0 ? (대기넘침 ? c.gray('…') : c.hcyan('❯')) : ' ';
|
|
199
|
+
줄들.push(` ${테('│')} ${표} ${한줄}${' '.repeat(Math.max(0, 안쪽 - width(한줄)))} ${테('│')}`);
|
|
167
200
|
}
|
|
168
201
|
|
|
169
202
|
줄들.push(테(` ╰${가로}╯`));
|
|
170
|
-
|
|
171
|
-
|
|
203
|
+
// 이 아래 줄도 폭 안에 넣는다 — 안내 문구는 43칸이라 좁은 창에서 접힌다.
|
|
204
|
+
if (일감.대기) 줄들.push(clip(` ${c.gray('Enter 를 치면 지금 일이 끝난 뒤에 보냅니다')}`, 칸));
|
|
205
|
+
else if (곁말) 줄들.push(clip(` ${c.gray(곁말)}`, 칸));
|
|
172
206
|
|
|
173
207
|
// 커서는 미리 치는 중일 때만 보여 준다. 아니면 칠 자리가 아니다.
|
|
174
208
|
if (일감.대기) {
|
|
175
|
-
const 마지막 =
|
|
209
|
+
const 마지막 = 대기보일.at(-1) ?? '';
|
|
176
210
|
return { 줄들, 커서: { 위: 2, 열: Math.min(앞머리 + 1 + width(마지막), 칸 - 1) } };
|
|
177
211
|
}
|
|
178
212
|
return { 줄들, 커서: { 위: (곁말 ? 1 : 0), 열: 1 } };
|
|
@@ -184,7 +218,24 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
|
|
|
184
218
|
*/
|
|
185
219
|
const 안쪽줄 = 글 ? 접어쓰기(글, 안쪽) : [''];
|
|
186
220
|
const 넘침 = 안쪽줄.length > 안쪽최대;
|
|
187
|
-
|
|
221
|
+
/*
|
|
222
|
+
* 보일 자리는 **커서를 따라간다.**
|
|
223
|
+
*
|
|
224
|
+
* 앞서는 언제나 마지막 여덟 줄만 보여 줬다. 그래서 열두 줄짜리 글을
|
|
225
|
+
* 붙여넣고 Ctrl+A 로 맨 앞에 가면, 커서가 든 줄이 화면에 없었다 — 그때
|
|
226
|
+
* 커서 줄 번호는 0 으로 깎이므로 커서는 **보이는 첫 줄**에 그려지고,
|
|
227
|
+
* 치는 글자는 안 보이는 줄에 들어간다. 커서 자리에 아무것도 안 나타나니
|
|
228
|
+
* 키가 안 먹는 것처럼 보인다.
|
|
229
|
+
*
|
|
230
|
+
* 끝에서 치는 보통 경우에는 예전과 똑같이 마지막 여덟 줄이 보인다.
|
|
231
|
+
*/
|
|
232
|
+
const 커서앞줄수 = (커서자리 == null ? 글 : String(글).slice(0, 커서자리))
|
|
233
|
+
? 접어쓰기(커서자리 == null ? 글 : String(글).slice(0, 커서자리), 안쪽).length
|
|
234
|
+
: 1;
|
|
235
|
+
const 첫줄 = 넘침
|
|
236
|
+
? Math.min(Math.max(0, 커서앞줄수 - 안쪽최대), 안쪽줄.length - 안쪽최대)
|
|
237
|
+
: 0;
|
|
238
|
+
const 보일줄 = 안쪽줄.slice(첫줄, 첫줄 + 안쪽최대);
|
|
188
239
|
|
|
189
240
|
for (const [i, 한줄] of 보일줄.entries()) {
|
|
190
241
|
const 표 = i === 0 ? (넘침 ? c.gray('…') : c.hcyan('❯')) : ' ';
|
|
@@ -227,7 +278,8 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
|
|
|
227
278
|
}
|
|
228
279
|
}
|
|
229
280
|
|
|
230
|
-
|
|
281
|
+
// 곁말도 폭 안에 넣는다 — 위 머리글의 그 셈(줄 수)이 어긋난다.
|
|
282
|
+
if (곁말) 줄들.push(clip(` ${c.gray(곁말)}`, 칸));
|
|
231
283
|
|
|
232
284
|
/*
|
|
233
285
|
* 커서 자리.
|
|
@@ -239,9 +291,8 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
|
|
|
239
291
|
const 앞글 = 커서자리 == null ? 글 : String(글).slice(0, 커서자리);
|
|
240
292
|
const 앞줄들 = 앞글 ? 접어쓰기(앞글, 안쪽) : [''];
|
|
241
293
|
|
|
242
|
-
//
|
|
243
|
-
const
|
|
244
|
-
const 커서줄 = Math.min(Math.max(0, 앞줄들.length - 1 - 숨긴줄), 보일줄.length - 1);
|
|
294
|
+
// 위로 접어 둔 줄만큼 줄 번호가 밀린다 (보일 자리는 커서를 따라 옮긴다).
|
|
295
|
+
const 커서줄 = Math.min(Math.max(0, 앞줄들.length - 1 - 첫줄), 보일줄.length - 1);
|
|
245
296
|
|
|
246
297
|
// 커서 아래에 몇 줄이 더 있나 — 닫는 테두리 + 추천 목록 + 곁말.
|
|
247
298
|
const 아래 = 1 + 추천줄수 + (곁말 ? 1 : 0);
|
|
@@ -400,7 +451,7 @@ export class InputBox {
|
|
|
400
451
|
일시작(session, 갈래 = '기본') {
|
|
401
452
|
this.session = session ?? this.session;
|
|
402
453
|
// 대기 = 일하는 도중에 미리 치고 있는 글, 예약수 = 이미 Enter 를 친 것들.
|
|
403
|
-
this.일감 = { 갈래, 시작: Date.now(), 곁정보: '', 대기: '', 예약수: 0 };
|
|
454
|
+
this.일감 = { 갈래, 시작: Date.now(), 바뀐때: Date.now(), 곁정보: '', 대기: '', 예약수: 0 };
|
|
404
455
|
this.틱 = 0;
|
|
405
456
|
this.일그리기();
|
|
406
457
|
if (this.박자) clearInterval(this.박자);
|
|
@@ -414,7 +465,7 @@ export class InputBox {
|
|
|
414
465
|
if (!this.일감) return;
|
|
415
466
|
// 갈래가 바뀌면 문구를 그 자리에서 갈아 준다. 4초를 기다리면 이미 다음
|
|
416
467
|
// 일로 넘어가 있어서, 화면이 늘 한 걸음 늦은 말을 하게 된다.
|
|
417
|
-
if (갈래 && 갈래 !== this.일감.갈래) { this.일감.갈래 = 갈래; this.틱 = 0; }
|
|
468
|
+
if (갈래 && 갈래 !== this.일감.갈래) { this.일감.갈래 = 갈래; this.일감.바뀐때 = Date.now(); this.틱 = 0; }
|
|
418
469
|
if (곁정보 !== null) this.일감.곁정보 = 곁정보;
|
|
419
470
|
this.일그리기();
|
|
420
471
|
}
|
|
@@ -452,7 +503,18 @@ export class InputBox {
|
|
|
452
503
|
* 「멈추는 중」이 화면에 영영 안 뜨고, 사람은 안 먹었다고 여겨 또 누른다.
|
|
453
504
|
*/
|
|
454
505
|
const 멈추나 = this.일감.갈래 === '멈춤';
|
|
455
|
-
|
|
506
|
+
/*
|
|
507
|
+
* 느긋으로 넘어가는 셈은 **턴이 시작된 뒤**가 아니라 **갈래가 바뀐 뒤**로
|
|
508
|
+
* 잰다.
|
|
509
|
+
*
|
|
510
|
+
* 시작부터 재면 45초가 지난 순간부터 턴이 끝날 때까지 갈래가 영영
|
|
511
|
+
* 느긋에 붙박인다 — 그 뒤로 파일을 다섯 개 읽고 세 군데를 고쳐도 화면은
|
|
512
|
+
* 계속 '아직 하는 중' 만 돈다. 로컬 모델은 한 턴이 몇 분이라 사실상 늘
|
|
513
|
+
* 그 상태다. 이 문구를 넣은 뜻이 「아까 하던 말을 계속 돌리면 붙박이처럼
|
|
514
|
+
* 보인다」 였는데, 시작부터 재면 그 붙박이를 이 문구가 만든다.
|
|
515
|
+
*/
|
|
516
|
+
const 멎은지 = Date.now() - (this.일감.바뀐때 ?? this.일감.시작);
|
|
517
|
+
const 갈래 = (!멈추나 && 멎은지 >= 느긋해질때) ? '느긋' : this.일감.갈래;
|
|
456
518
|
const 회차 = Math.floor((this.틱 * 돌림주기) / 문구주기);
|
|
457
519
|
this.그리기(this.session, '', 0, {
|
|
458
520
|
// 갈래에 맞는 작은 그림. 무엇을 하는 중인지 글자와 그림이 같이 말한다.
|
|
@@ -551,16 +613,34 @@ export class InputBox {
|
|
|
551
613
|
*
|
|
552
614
|
* 높이만 바뀌었다 — 줄 접힘은 폭이 정한다. 폭이 그대로면 접힘도 그대로라
|
|
553
615
|
* 그린줄·커서위 가 아직 맞다. 평소처럼 제대로 지운다.
|
|
554
|
-
* 폭이 바뀌었다 — 옛 수는 다 틀어졌다. 대신
|
|
555
|
-
* 상자는 늘 맨 아래에
|
|
556
|
-
*
|
|
616
|
+
* 폭이 바뀌었다 — 옛 수는 다 틀어졌다. 대신 상자 맨 윗줄로 올라가
|
|
617
|
+
* 거기서 아래를 통째로 쓸어낸다. 상자는 늘 맨 아래에
|
|
618
|
+
* 있어서 그 아래에는 아무것도 없고, 맨 윗줄보다 위로는
|
|
619
|
+
* 안 올라가니 대화를 건드릴 일이 없다.
|
|
557
620
|
*/
|
|
558
621
|
const 높이 = process.stdout.rows ?? null;
|
|
559
622
|
if (this.그린폭 !== 폭 || this.그린높이 !== 높이) {
|
|
560
623
|
if (this.그린폭 === 폭) {
|
|
561
624
|
this.지우기();
|
|
562
625
|
} else {
|
|
563
|
-
|
|
626
|
+
/*
|
|
627
|
+
* 쓸어내기 전에 **상자 맨 윗줄로 올라간다.**
|
|
628
|
+
*
|
|
629
|
+
* 커서는 상자 한가운데(아래에서 커서위 번째 줄)에 서 있다. 거기서
|
|
630
|
+
* 곧바로 쓸어내면 커서 아래만 지워지고 **위쪽 절반이 그대로 남는다** —
|
|
631
|
+
* 사무실·상태줄·윗테두리·그 위 입력 줄이 유령으로 붙어 있고, 새 상자는
|
|
632
|
+
* 그 아래에 또 그려진다. 상자가 두 개로 보이고 유령은 대화와 함께 위로
|
|
633
|
+
* 밀려 올라가 영영 안 없어진다. 흉내 화면으로 재 보니 80→60 에서 네 줄
|
|
634
|
+
* (윗테두리·입력 줄이 접힌 것)이 남았다.
|
|
635
|
+
*
|
|
636
|
+
* 올라가는 거리는 **옛 폭에서 세던 그 거리**를 쓴다. 좁아지면 터미널이
|
|
637
|
+
* 다시 접어 상자가 더 여러 줄을 차지하므로 이 거리는 모자랄 수 있는데,
|
|
638
|
+
* 모자라면 유령이 몇 줄 남을 뿐이다. 넘치면 대화를 지운다 — 이 저장소가
|
|
639
|
+
* 제일 나쁜 것으로 꼽는 그 손해다. 그래서 모자라는 쪽으로 잡는다.
|
|
640
|
+
*/
|
|
641
|
+
if (this.그린줄) {
|
|
642
|
+
process.stdout.write('\r' + 위로(Math.max(0, this.그린줄 - 1 - this.커서위)) + '\x1b[J');
|
|
643
|
+
}
|
|
564
644
|
this.앞줄들 = null;
|
|
565
645
|
this.그린줄 = 0;
|
|
566
646
|
this.커서위 = 0;
|
package/src/ui/level.js
CHANGED
|
@@ -58,7 +58,15 @@ export function normalize(v) {
|
|
|
58
58
|
'쉬움': '쉬움', '초보': '쉬움', '초보자': '쉬움', 'beginner': '쉬움', 'easy': '쉬움', 'b': '쉬움',
|
|
59
59
|
'개발자': '개발자', '고급': '개발자', 'developer': '개발자', 'dev': '개발자', 'advanced': '개발자', 'd': '개발자',
|
|
60
60
|
};
|
|
61
|
-
|
|
61
|
+
/*
|
|
62
|
+
* 표에 **제 것으로 있는** 열쇠만 본다.
|
|
63
|
+
*
|
|
64
|
+
* `별명[s]` 는 물려받은 열쇠에도 값을 준다. `/level constructor` 면 함수
|
|
65
|
+
* 하나가 수준 이름으로 통과해서, 그 뒤 shows() 가 `lv.show` 를 읽다가
|
|
66
|
+
* 터진다 — 명령 목록을 그리는 자리라 대화가 통째로 끝난다.
|
|
67
|
+
*/
|
|
68
|
+
if (Object.hasOwn(별명, s)) return 별명[s];
|
|
69
|
+
return Object.hasOwn(LEVELS, v) ? v : null;
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
export function get(id) {
|