deel-local-cli 1.20.10 → 1.20.12
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 +13 -3
- package/package.json +1 -1
- package/src/acp/serve.js +5 -3
- package/src/agent/commit.js +105 -21
- package/src/agent/loop.js +39 -4
- package/src/agent/session.js +279 -33
- package/src/agent/sessionui.js +14 -4
- package/src/agent/store.js +133 -33
- package/src/agent/threads.js +52 -1
- package/src/commands.js +146 -13
- package/src/config.js +87 -9
- package/src/i18n/en.js +7 -5
- package/src/i18n/ja.js +4 -2
- package/src/i18n/ko.js +7 -5
- package/src/i18n/zh.js +4 -2
- package/src/lsp/client.js +82 -19
- package/src/oneshot.js +3 -2
- package/src/plugins/manage.js +15 -6
- package/src/repl.js +55 -2
- package/src/reset.js +37 -6
- package/src/safety/hooks.js +4 -1
- package/src/safety/runmode.js +15 -0
- package/src/safety/undo.js +98 -8
- package/src/setup.js +26 -5
- package/src/skills/discover.js +10 -1
- package/src/tools/fsutil.js +7 -1
- package/src/tools/index.js +427 -30
- package/src/tools/webfetch.js +84 -20
- package/src/ui/intro.js +2 -1
- package/src/ui/md.js +123 -25
- package/src/ui/motion.js +40 -9
- package/src/ui/office.js +22 -1
- package/src/ui/screen.js +60 -4
- package/src/ui/working.js +4 -1
package/src/tools/webfetch.js
CHANGED
|
@@ -65,8 +65,13 @@ const 집간격 = 400; // 같은 집을 다시 두드리기 전에
|
|
|
65
65
|
* 안 먹은 것과 똑같이 보인다 — 제보받은 그 증상이다.
|
|
66
66
|
*/
|
|
67
67
|
const 잠깐 = (ms, signal = null) => new Promise((풀기) => {
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// 이미 멈춘 신호를 들고 오면 abort 는 **다시 안 터진다.** 그 판에서
|
|
69
|
+
// 그냥 잠들면 ESC 를 누른 뒤에도 이 잠을 끝까지 잔다 — 고치려던 그 증상이
|
|
70
|
+
// 그대로 돌아온다.
|
|
71
|
+
if (signal?.aborted) return 풀기();
|
|
72
|
+
const 깨우기 = () => { clearTimeout(t); 풀기(); };
|
|
73
|
+
const t = setTimeout(() => { signal?.removeEventListener?.('abort', 깨우기); 풀기(); }, ms);
|
|
74
|
+
signal?.addEventListener?.('abort', 깨우기, { once: true });
|
|
70
75
|
});
|
|
71
76
|
|
|
72
77
|
function 한집씩(origin, 일, signal = null) {
|
|
@@ -79,8 +84,15 @@ function 한집씩(origin, 일, signal = null) {
|
|
|
79
84
|
if (signal?.aborted) throw new Error('중단했습니다');
|
|
80
85
|
return 일();
|
|
81
86
|
});
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
/*
|
|
88
|
+
* 다음 사람이 기다리는 것은 '내가 끝났다' 뿐이다. 내가 실패해도 줄은 넘어간다.
|
|
89
|
+
*
|
|
90
|
+
* 끝나면 **줄에서 뺀다.** 안 빼면 한참 뒤에 혼자 부르는 요청도 앞사람이
|
|
91
|
+
* 있는 것으로 보여 400ms 를 그냥 잔다 — 줄이 비어 있는데 서 있는 셈이다.
|
|
92
|
+
*/
|
|
93
|
+
const 끝나면 = 내차례.then(() => {}, () => {});
|
|
94
|
+
집줄.set(origin, 끝나면);
|
|
95
|
+
끝나면.then(() => { if (집줄.get(origin) === 끝나면) 집줄.delete(origin); });
|
|
84
96
|
return 내차례;
|
|
85
97
|
}
|
|
86
98
|
|
|
@@ -133,15 +145,26 @@ function 태그벗기기(html) {
|
|
|
133
145
|
if (글 === 전) break;
|
|
134
146
|
}
|
|
135
147
|
return 글
|
|
136
|
-
|
|
137
|
-
|
|
148
|
+
// 닫는 태그만 줄로 바꾸면, 닫는 태그를 안 적은 페이지가 통째로 한 줄이
|
|
149
|
+
// 된다 — `<p>첫째<p>둘째` 나 `<ul><li>하나<li>둘</ul>` 이 흔하다.
|
|
150
|
+
// HTML 이 그것을 허락하므로 옛 사내 페이지에는 정말로 그렇게 적혀 있다.
|
|
151
|
+
// 여는 태그도 같이 줄로 바꾼다.
|
|
152
|
+
.replace(/<\/?(p|div|section|article|li|tr|h[1-6]|br)\b[^>]*>/gi, '\n')
|
|
138
153
|
.replace(/<[^>]+>/g, ' ')
|
|
139
154
|
// 한 번에 찾아서 한 번에 바꾼다 — 차례로 바꾸면(`&` 를 먼저 `&` 로
|
|
140
155
|
// 풀고 나서 `<` 를 다시 찾는 식) `&lt;` 처럼 두 겹 씌운 것이
|
|
141
156
|
// 두 번 풀려서 `<` 로 튀어나온다(글자로 남아야 하는데 태그처럼 보이게 됨).
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}[
|
|
157
|
+
// 숫자 꼴(`빈` · `'`)과 `'` 도 같은 한 판에서 푼다.
|
|
158
|
+
// 안 풀면 한글 페이지 하나가 통째로 숫자 나열로 모델에게 간다.
|
|
159
|
+
.replace(/&(nbsp|amp|lt|gt|quot|apos|#\d{1,7}|#x[0-9a-f]{1,6});/gi, (온것, 이름) => {
|
|
160
|
+
const 표 = { nbsp: ' ', amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
|
|
161
|
+
const 키 = 이름.toLowerCase();
|
|
162
|
+
if (표[키] !== undefined) return 표[키];
|
|
163
|
+
const 번호 = 키.startsWith('#x') ? parseInt(키.slice(2), 16) : parseInt(키.slice(1), 10);
|
|
164
|
+
// 못 읽을 번호면 **손대지 않는다.** 지어낸 글자를 넣느니 원문이 낫다.
|
|
165
|
+
if (!Number.isFinite(번호) || 번호 < 32 || 번호 > 0x10ffff) return 온것;
|
|
166
|
+
try { return String.fromCodePoint(번호); } catch { return 온것; }
|
|
167
|
+
})
|
|
145
168
|
.replace(/[ \t]+/g, ' ')
|
|
146
169
|
.replace(/\n\s*\n\s*\n+/g, '\n\n')
|
|
147
170
|
.trim();
|
|
@@ -243,6 +266,15 @@ export async function webFetch(args, { allowPrivate = false, 모델컨텍스트
|
|
|
243
266
|
// 되돌림(redirect)으로 옮겨 간 집도 그 한 번만 연다. 한 홉마다 문지기를 지나므로
|
|
244
267
|
// 여기서 열어 주지 않으면 막힌다 — 그리고 사내망으로 되돌리는 것은 열지 않는다.
|
|
245
268
|
const 열어둔 = [];
|
|
269
|
+
/*
|
|
270
|
+
* 되돌림을 따라가면 **정말 답한 곳**은 처음 주소가 아니다.
|
|
271
|
+
*
|
|
272
|
+
* 여태 기록에도 오류 문구에도 처음 주소만 적혔다. 그래서 짧은주소 하나가
|
|
273
|
+
* 302 로 딴 집에 보내 놓고 429 를 뱉으면, 화면에는 짧은주소가 힘들어한다고
|
|
274
|
+
* 뜬다 — 사람도 모델도 엉뚱한 쪽을 붙잡는다. 심사서에 남는 「어디로 나갔나」
|
|
275
|
+
* 도 마찬가지로 처음 주소만 남아서, 실제로 통신한 집이 빠진다.
|
|
276
|
+
*/
|
|
277
|
+
let 닿은곳 = u;
|
|
246
278
|
try {
|
|
247
279
|
// 같은 집이면 줄을 서고, 잠시 뒤 되는 오류면 쉬었다 다시 부른다.
|
|
248
280
|
let res = null;
|
|
@@ -264,10 +296,19 @@ export async function webFetch(args, { allowPrivate = false, 모델컨텍스트
|
|
|
264
296
|
stream: true, // 상한까지만 받는다 — 다 받아 놓고 버리지 않는다
|
|
265
297
|
되돌림: async (다음) => {
|
|
266
298
|
await 웹되돌림검사(다음, { allowPrivate });
|
|
299
|
+
닿은곳 = 다음;
|
|
300
|
+
방문기록.push({ url: 다음.href, status: 0, at: new Date().toISOString(), 되돌림: true });
|
|
267
301
|
열어둔.push(allowTemporarily(다음.origin));
|
|
268
302
|
},
|
|
269
303
|
}), signal);
|
|
270
|
-
방문기록.push({
|
|
304
|
+
방문기록.push({
|
|
305
|
+
url: u.href,
|
|
306
|
+
status: res.status,
|
|
307
|
+
at: new Date().toISOString(),
|
|
308
|
+
// 되돌림을 탔으면 **정말 답한 곳**도 같이 적는다. 처음 주소만 남기면
|
|
309
|
+
// 심사서의 「어디로 나갔나」 에서 실제로 통신한 집이 빠진다.
|
|
310
|
+
...(닿은곳.href === u.href ? {} : { 닿은곳: 닿은곳.href }),
|
|
311
|
+
});
|
|
271
312
|
if (res.ok || !다시할것.has(res.status) || 회차 >= 다시횟수) break;
|
|
272
313
|
await res.버리기?.();
|
|
273
314
|
const 초 = 얼마나쉬라나(res.headers.get('retry-after'), 회차);
|
|
@@ -282,14 +323,14 @@ export async function webFetch(args, { allowPrivate = false, 모델컨텍스트
|
|
|
282
323
|
* 오류를 그냥 번호로만 던지면 모델은 할 수 있는 게 없다. 실제로 그랬다 —
|
|
283
324
|
* `✗ HTTP 429` 만 보고 그 자료를 포기했다. 무엇을 하면 되는지 같이 준다.
|
|
284
325
|
*/
|
|
285
|
-
const 집 =
|
|
326
|
+
const 집 = 닿은곳.hostname;
|
|
286
327
|
if (res.status === 429) {
|
|
287
328
|
return { error: `HTTP 429 — ${집} 가 "너무 자주 부른다" 고 합니다.`
|
|
288
329
|
+ `\n ${다시횟수}번 쉬었다 다시 불러 봤습니다(${쉰시간}초). 그래도 같습니다.`
|
|
289
330
|
+ '\n 한꺼번에 여러 개를 부르지 말고 하나씩 부르거나, 잠시 뒤에 다시 해 보세요.'
|
|
290
331
|
+ '\n 키가 있는 API 면 키를 붙인 주소를 쓰면 한도가 늘어납니다.' };
|
|
291
332
|
}
|
|
292
|
-
if (res.status === 404) return { error: `HTTP 404 — 그런 쪽이 없습니다: ${
|
|
333
|
+
if (res.status === 404) return { error: `HTTP 404 — 그런 쪽이 없습니다: ${닿은곳.href}\n 주소를 다시 확인하세요.` };
|
|
293
334
|
if (res.status === 401 || res.status === 403) {
|
|
294
335
|
return { error: `HTTP ${res.status} — ${집} 가 접근을 막았습니다.\n 로그인이나 키가 있어야 하는 쪽입니다. 이 도구로는 못 읽습니다.` };
|
|
295
336
|
}
|
|
@@ -297,11 +338,23 @@ export async function webFetch(args, { allowPrivate = false, 모델컨텍스트
|
|
|
297
338
|
return { error: `HTTP ${res.status} — ${집} 가 지금 힘들어합니다.`
|
|
298
339
|
+ `\n ${다시횟수}번 다시 불러 봤습니다(${쉰시간}초). 잠시 뒤에 다시 해 보세요.` };
|
|
299
340
|
}
|
|
300
|
-
return { error: `HTTP ${res.status} — ${
|
|
341
|
+
return { error: `HTTP ${res.status} — ${닿은곳.href}` };
|
|
301
342
|
}
|
|
302
343
|
|
|
303
344
|
const type = (res.headers.get('content-type') ?? '').toLowerCase();
|
|
304
|
-
|
|
345
|
+
/*
|
|
346
|
+
* 갈래는 **낱말로** 본다.
|
|
347
|
+
*
|
|
348
|
+
* 그냥 `/text|json|xml|javascript/` 로 보면 두 쪽에서 틀린다.
|
|
349
|
+
* `application/octet-stream; name="context.bin"` 은 'context' 안의 text 에
|
|
350
|
+
* 걸려 통과하고(내려받은 바이너리가 모델에게 간다), `application/yaml` 이나
|
|
351
|
+
* `application/toml` 같은 멀쩡한 글은 낱말이 없어서 거절당한다.
|
|
352
|
+
*/
|
|
353
|
+
const 갈래 = type.split(';')[0].trim();
|
|
354
|
+
const 뒷말 = 갈래.split('/')[1] ?? '';
|
|
355
|
+
const 글인가 = /^text\//.test(갈래)
|
|
356
|
+
|| /(^|[-+.])(json|xml|javascript|ecmascript|yaml|toml|csv|ndjson|graphql)$/.test(뒷말);
|
|
357
|
+
if (!글인가) {
|
|
305
358
|
await res.버리기?.();
|
|
306
359
|
return { error: `글이 아닌 내용입니다 (${type || '알 수 없음'}). 이 도구는 글만 읽습니다.` };
|
|
307
360
|
}
|
|
@@ -323,12 +376,23 @@ export async function webFetch(args, { allowPrivate = false, 모델컨텍스트
|
|
|
323
376
|
const 머리글 = /charset=["']?([\w-]+)/i.exec(type)?.[1]?.toLowerCase() ?? null;
|
|
324
377
|
let text = 웹글읽기(buf, 머리글);
|
|
325
378
|
if (/html/.test(type)) text = 태그벗기기(text);
|
|
326
|
-
|
|
327
|
-
|
|
379
|
+
/*
|
|
380
|
+
* `<meta charset>` 이 머리글과 **다르게** 적혀 있는 페이지가 있다.
|
|
381
|
+
*
|
|
382
|
+
* 여기 `!머리글` 이 붙어 있어서, 정작 그 판 — 머리글이 있는데 틀린 판 —
|
|
383
|
+
* 에서는 한 번도 안 돌았다. 서버가 기본값으로 `charset=utf-8` 을 붙이고
|
|
384
|
+
* 본문은 EUC-KR 인 옛 사내 위키가 딱 그렇다. 깨진 글이 그대로 모델에게
|
|
385
|
+
* 가고, 모델은 깨진 채로 요약한다.
|
|
386
|
+
*
|
|
387
|
+
* 다시 읽어서 **덜 깨진 쪽**을 쓴다. 세어 보고 고르니 더 나빠질 일이 없다.
|
|
388
|
+
*/
|
|
389
|
+
const 깨진수 = (그것) => (그것.match(/�/g) ?? []).length;
|
|
390
|
+
if (깨진수(text)) {
|
|
328
391
|
const meta = /<meta[^>]+charset=["']?([\w-]+)/i.exec(buf.toString('latin1').slice(0, 2000))?.[1]?.toLowerCase();
|
|
329
|
-
if (meta) {
|
|
330
|
-
|
|
331
|
-
if (/html/.test(type))
|
|
392
|
+
if (meta && meta !== 머리글) {
|
|
393
|
+
let 다시 = 웹글읽기(buf, meta);
|
|
394
|
+
if (/html/.test(type)) 다시 = 태그벗기기(다시);
|
|
395
|
+
if (깨진수(다시) < 깨진수(text)) text = 다시;
|
|
332
396
|
}
|
|
333
397
|
}
|
|
334
398
|
/*
|
|
@@ -365,7 +429,7 @@ export async function webFetch(args, { allowPrivate = false, 모델컨텍스트
|
|
|
365
429
|
? `\n\n(여기서 잘렸습니다 — ${남은것}자가 더 있습니다.`
|
|
366
430
|
+ '\n 잘린 JSON 은 그대로 읽을 수 없습니다. 다음 중 하나를 하세요:'
|
|
367
431
|
+ '\n · 범위를 좁혀 다시 부른다 (per_page·ids·days 같은 조건을 붙인다)'
|
|
368
|
-
+ `\n · 같은 주소를 max_chars 를 올려 다시 부른다 (지금 ${max.toLocaleString()}, 최대
|
|
432
|
+
+ `\n · 같은 주소를 max_chars 를 올려 다시 부른다 (지금 ${max.toLocaleString()}, 최대 120,000)`
|
|
369
433
|
: `\n\n(뒤쪽 ${남은것}자는 잘렸습니다. 더 필요하면 max_chars 를 올려 다시 부르세요.)`;
|
|
370
434
|
}
|
|
371
435
|
|
package/src/ui/intro.js
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
import { c, cursor, width } from './ansi.js';
|
|
36
36
|
import { 말 } from '../i18n/index.js';
|
|
37
37
|
import * as 큰이름 from './banner.js';
|
|
38
|
+
import { 끄라는값인가 } from './office.js';
|
|
38
39
|
|
|
39
40
|
const 앞 = 'deel';
|
|
40
41
|
const 꼬리 = '-local';
|
|
@@ -42,7 +43,7 @@ const 이름폭 = 앞.length + 꼬리.length; // 10
|
|
|
42
43
|
const 들여 = ' ';
|
|
43
44
|
|
|
44
45
|
/** 모션을 끄는 스위치. 그림 돌림표(motion.js)와 같은 스위치를 함께 본다. */
|
|
45
|
-
export const 끔 = () =>
|
|
46
|
+
export const 끔 = () => 끄라는값인가(process.env.DEEL_NO_INTRO) || 끄라는값인가();
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* 한 틀에 얼마나 머무나.
|
package/src/ui/md.js
CHANGED
|
@@ -57,13 +57,22 @@ export function 인라인(글) {
|
|
|
57
57
|
// 자리표시자로 읽혀 엉뚱한 글자로 바뀐다 — 모델이 한 말과 화면이 달라진다.
|
|
58
58
|
let s = String(글 ?? '').replace(/\x00/g, '');
|
|
59
59
|
const 뺀것 = [];
|
|
60
|
-
|
|
60
|
+
// 백틱은 **같은 수만큼** 짝을 짓는다. 하나만 보면 ``a`` 나 ```x``` 처럼 겹쳐
|
|
61
|
+
// 적은 코드가 안쪽만 걸려서 바깥 백틱이 화면에 그대로 남는다.
|
|
62
|
+
s = s.replace(/(`+)([\s\S]+?)\1/g, (_, __, 안) => {
|
|
61
63
|
뺀것.push(코드(안));
|
|
62
64
|
return `\x00${뺀것.length - 1}\x00`;
|
|
63
65
|
});
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
// 별표 안쪽이 공백으로 시작하거나 끝나면 굵게가 아니다. 안 막으면 파이썬의
|
|
68
|
+
// 거듭제곱을 적은 `2 ** 3 ** 4` 가 화면에서 `2 3 4` 가 된다 — 별표가
|
|
69
|
+
// 소리 없이 사라지고 사람은 모델이 그렇게 말한 줄 안다.
|
|
70
|
+
s = s.replace(/\*\*([^*\s](?:[^*]*[^*\s])?)\*\*/g, (_, 안) => 굵게(안));
|
|
71
|
+
// 닫는 별표 뒤는 별표만 아니면 된다. `[\s.,)]` 로 좁혀 두면 조사가 붙는
|
|
72
|
+
// 한국어(`*중요*입니다`)와 `*주의*!` 에서 별표가 화면에 그대로 남는다.
|
|
73
|
+
// 굵게와 같이, 안쪽이 공백으로 끝나면 기울임이 아니다 — `5 *x * y` 의 별표를
|
|
74
|
+
// 먹으면 화면에서 별표가 사라진다.
|
|
75
|
+
s = s.replace(/(^|[\s(])\*([^*\s](?:[^*]*[^*\s])?)\*(?=$|[^*])/g, (_, 앞, 안) => `${앞}${c.italic(안)}`);
|
|
67
76
|
s = s.replace(/~~([^~]+)~~/g, (_, 안) => c.gray(안));
|
|
68
77
|
// 링크는 글만 보이고 주소는 흐리게. 주소를 지우면 따라갈 수가 없다.
|
|
69
78
|
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, 글자, 주소) => `${c.under(글자)} ${c.gray(주소)}`);
|
|
@@ -86,13 +95,30 @@ export function 한줄(줄, 상태 = { 펜스: false }, 폭 = 80) {
|
|
|
86
95
|
const s = String(줄 ?? '').replace(/\r+$/, '');
|
|
87
96
|
|
|
88
97
|
// 코드 울타리. 여는 줄에 언어가 붙어 있으면 그걸 알려 준다.
|
|
89
|
-
|
|
98
|
+
//
|
|
99
|
+
// 언어 자리에 백틱이 또 들어 있으면 울타리가 아니다 — 마크다운 규칙이 그렇고,
|
|
100
|
+
// 안 막으면 ```` ```js console.log(1)``` ```` 처럼 한 줄에 다 적은 코드가
|
|
101
|
+
// 울타리를 **열기만** 한다. 그 뒤로 오는 답 전체가 코드로 그려진다 — 제목도
|
|
102
|
+
// 굵기도 다 사라지고, 화면은 멀쩡해 보인다.
|
|
103
|
+
const 울타리 = s.match(/^\s*(`{3,})[ \t]*([^`]*?)[ \t]*$/);
|
|
90
104
|
if (울타리) {
|
|
91
|
-
const
|
|
105
|
+
const 길이 = 울타리[1].length;
|
|
106
|
+
const 안쪽 = 울타리[2];
|
|
107
|
+
const 줄긋기 = (앞) => c.gray(앞 + '─'.repeat(Math.max(2, Math.min(30, 폭 - 6))));
|
|
108
|
+
if (상태.펜스) {
|
|
109
|
+
/*
|
|
110
|
+
* 닫는 울타리는 언어를 못 갖고, 연 것보다 짧을 수도 없다.
|
|
111
|
+
*
|
|
112
|
+
* 마크다운 문서를 보여 주려고 백틱 넷으로 열고 그 안에 ```js 를 적는 일이
|
|
113
|
+
* 흔하다. 안 막으면 그 ```js 가 상자를 일찍 닫아 버린다 — 나머지 코드가
|
|
114
|
+
* 코드가 아닌 것으로 그려지고, 상자는 열자마자 닫힌 빈 상자가 된다.
|
|
115
|
+
*/
|
|
116
|
+
if (안쪽 || 길이 < (상태.울타리길이 ?? 길이)) return { 글: `${c.gray('│')} ${c.white(s)}`, 상태 };
|
|
117
|
+
return { 글: 줄긋기('└'), 상태: { ...상태, 펜스: false, 울타리길이: 0 } };
|
|
118
|
+
}
|
|
92
119
|
return {
|
|
93
|
-
글:
|
|
94
|
-
|
|
95
|
-
상태: { ...상태, 펜스: !상태.펜스 },
|
|
120
|
+
글: 줄긋기('┌') + (안쪽 ? c.gray(` ${안쪽}`) : ''),
|
|
121
|
+
상태: { ...상태, 펜스: true, 울타리길이: 길이 },
|
|
96
122
|
};
|
|
97
123
|
}
|
|
98
124
|
|
|
@@ -108,9 +134,11 @@ export function 한줄(줄, 상태 = { 펜스: false }, 폭 = 80) {
|
|
|
108
134
|
const 제목 = s.match(/^(#{1,6})\s+(.*)$/);
|
|
109
135
|
if (제목) {
|
|
110
136
|
const 깊이 = 제목[1].length;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
137
|
+
// 이어굵게 로 감싼다. c.bold 로 감싸면 제목 안의 코드가 제 색을 끝내면서
|
|
138
|
+
// 굵기까지 꺼 버려서, 코드 뒷부분만 가늘어진다.
|
|
139
|
+
const 글자 = 이어굵게(인라인(제목[2]));
|
|
140
|
+
if (깊이 <= 2) return { 글: `${c.hcyan('▍')} ${글자}`, 상태 };
|
|
141
|
+
return { 글: 글자, 상태 };
|
|
114
142
|
}
|
|
115
143
|
|
|
116
144
|
// 인용
|
|
@@ -118,10 +146,14 @@ export function 한줄(줄, 상태 = { 펜스: false }, 폭 = 80) {
|
|
|
118
146
|
if (인용) return { 글: `${c.gray('│')} ${이어흐리게(인라인(인용[1]))}`, 상태 };
|
|
119
147
|
|
|
120
148
|
// 할 일 목록은 목록보다 먼저 본다 — `- [ ]` 가 그냥 목록으로 먹히면 안 된다.
|
|
121
|
-
|
|
149
|
+
// 뒤에 글이 없는 빈 칸(`- [ ]`)도 할 일이다. 뒷글을 요구하면 그것만 글머리표로
|
|
150
|
+
// 빠져서 화면에 `• [ ]` 라고 대괄호가 그대로 뜬다. 다만 대괄호에 글이 바로 붙은
|
|
151
|
+
// `- [ ]foo` 는 할 일이 아니다 — 마크다운도 그렇게 안 읽는다.
|
|
152
|
+
const 할일 = s.match(/^(\s*)[-*+]\s+\[([ xX])\](\s+.*)?$/);
|
|
122
153
|
if (할일) {
|
|
123
154
|
const 표 = 할일[2] === ' ' ? c.gray('☐') : c.green('☑');
|
|
124
|
-
|
|
155
|
+
const 뒤 = 할일[3]?.trim() ? ` ${인라인(할일[3].trim())}` : '';
|
|
156
|
+
return { 글: `${할일[1]}${표}${뒤}`, 상태 };
|
|
125
157
|
}
|
|
126
158
|
|
|
127
159
|
// 글머리표
|
|
@@ -132,8 +164,9 @@ export function 한줄(줄, 상태 = { 펜스: false }, 폭 = 80) {
|
|
|
132
164
|
const 번호 = s.match(/^(\s*)(\d+)([.)])\s+(.*)$/);
|
|
133
165
|
if (번호) return { 글: `${번호[1]}${c.hcyan(번호[2] + 번호[3])} ${인라인(번호[4])}`, 상태 };
|
|
134
166
|
|
|
135
|
-
//
|
|
136
|
-
//
|
|
167
|
+
// 여기는 **표가 아니었던 것**이 오는 자리다. 진짜 표는 마크다운 클래스가
|
|
168
|
+
// 모아서 표그리기() 로 칸을 맞춰 그린다. 여기서는 칸 너비를 모르므로
|
|
169
|
+
// 세로줄만 흐리게 해서 내보낸다.
|
|
137
170
|
if (/^\s*\|.*\|\s*$/.test(s)) {
|
|
138
171
|
// 가름줄(`|---|---|`). 그대로 두면 화면에 작대기가 그대로 뜬다.
|
|
139
172
|
// 바로 앞 머리줄의 칸 너비를 기억해 뒀다가 그 자리에 맞춰 긋는다 —
|
|
@@ -182,6 +215,10 @@ const 표줄인가 = (s) => /^\s*\|.*\|\s*$/.test(String(s ?? '').replace(/\r+$/
|
|
|
182
215
|
/** `---` `:---` `---:` `:---:` — 가름줄의 한 칸인가. */
|
|
183
216
|
const 가름칸인가 = (칸) => /^:?-+:?$/.test(String(칸).trim());
|
|
184
217
|
|
|
218
|
+
/** 가름줄이 몇 번째 줄인가. 머리줄이 위에 있어야 하므로 1 보다 작으면 표가 아니다. */
|
|
219
|
+
const 가름찾기 = (행들) =>
|
|
220
|
+
행들.findIndex((행) => 행.length > 0 && 행.every(가름칸인가));
|
|
221
|
+
|
|
185
222
|
/**
|
|
186
223
|
* 한 줄을 칸으로 가른다.
|
|
187
224
|
*
|
|
@@ -232,10 +269,27 @@ function 채우기(글, 폭, 쪽) {
|
|
|
232
269
|
* 설명 칸이다. 줄인 칸은 잘라 버리지 않고 접는다(접어쓰기). 표에서 글자가
|
|
233
270
|
* 사라지면 그 줄은 아예 못 읽는다.
|
|
234
271
|
*/
|
|
235
|
-
export function 표그리기(줄들, 폭 = 80) {
|
|
272
|
+
export function 표그리기(줄들, 폭 = 80, 어떻게 = {}) {
|
|
273
|
+
return 짜기(줄들, 폭, 어떻게)?.화면 ?? null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 그 표를 그렸다면 칸 너비가 얼마였을까. 표가 아니면 null.
|
|
278
|
+
*
|
|
279
|
+
* 상한에 걸려 표를 나눠 그릴 때, 뒤 묶음에 앞 묶음의 너비를 그대로 물려 주려고
|
|
280
|
+
* 쓴다. 묶음마다 제 폭을 다시 재면 같은 표인데 위아래의 세로줄이 안 맞는다.
|
|
281
|
+
*/
|
|
282
|
+
export function 표칸너비(줄들, 폭 = 80) {
|
|
283
|
+
return 짜기(줄들, 폭)?.너비 ?? null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function 짜기(줄들, 폭 = 80, { 너비고정 = null } = {}) {
|
|
236
287
|
const 행들 = (줄들 ?? []).map(칸가르기);
|
|
237
288
|
// 가름줄이 있어야 표다. 첫 줄일 수는 없다 — 머리줄이 그 위에 있어야 한다.
|
|
238
|
-
|
|
289
|
+
//
|
|
290
|
+
// 칸이 하나뿐인 표(`| 항목 |`)도 표다. 칸 둘을 요구하면 그것만 표로 안 읽혀
|
|
291
|
+
// 테두리 없이 세로줄만 그어진 채 나가고, 가름줄의 작대기도 화면에 남는다.
|
|
292
|
+
const 가름 = 가름찾기(행들);
|
|
239
293
|
if (가름 < 1) return null;
|
|
240
294
|
|
|
241
295
|
const 열수 = Math.max(...행들.map((행) => 행.length));
|
|
@@ -253,9 +307,14 @@ export function 표그리기(줄들, 폭 = 80) {
|
|
|
253
307
|
*/
|
|
254
308
|
const 치레 = 3 * 열수 + 1;
|
|
255
309
|
const 쓸수있는 = Math.max(열수, 폭 - 치레);
|
|
256
|
-
|
|
310
|
+
// 앞 묶음이 쓰던 너비가 있으면 그대로 쓴다. 다시 재면 같은 표의 위아래가
|
|
311
|
+
// 서로 다른 폭으로 그려진다 — 세로줄이 이음매에서 어긋난다.
|
|
312
|
+
const 물려받나 = Array.isArray(너비고정) && 너비고정.length === 열수;
|
|
313
|
+
const 너비 = 물려받나
|
|
314
|
+
? 너비고정.map((w) => Math.max(1, w))
|
|
315
|
+
: Array.from({ length: 열수 }, (_, i) => Math.max(1, ...몸.map((행) => width(행[i]))));
|
|
257
316
|
let 합 = 너비.reduce((a, b) => a + b, 0);
|
|
258
|
-
while (합 > 쓸수있는) {
|
|
317
|
+
while (!물려받나 && 합 > 쓸수있는) {
|
|
259
318
|
let 큰 = 0;
|
|
260
319
|
for (let i = 1; i < 너비.length; i += 1) if (너비[i] > 너비[큰]) 큰 = i;
|
|
261
320
|
if (너비[큰] <= 1) break;
|
|
@@ -285,7 +344,7 @@ export function 표그리기(줄들, 폭 = 80) {
|
|
|
285
344
|
화면.push(...한행(i === 0 ? 행.map(이어굵게) : 행));
|
|
286
345
|
화면.push(i === 몸.length - 1 ? 가로('└', '┴', '┘') : 가로('├', '┼', '┤'));
|
|
287
346
|
});
|
|
288
|
-
return
|
|
347
|
+
return { 화면, 너비 };
|
|
289
348
|
}
|
|
290
349
|
|
|
291
350
|
/**
|
|
@@ -308,6 +367,10 @@ export class 마크다운 {
|
|
|
308
367
|
this.날것중 = false;
|
|
309
368
|
// 그리려고 붙들고 있는 표 줄들. 표가 끝나는 순간 칸을 맞춰 한꺼번에 나간다.
|
|
310
369
|
this.표줄들 = [];
|
|
370
|
+
// 상한에 걸려 끊은 표의 머리줄(가름줄까지)과 그때 쓰던 칸 너비.
|
|
371
|
+
// 다음 묶음이 같은 표로 이어 그려지도록 물려준다.
|
|
372
|
+
this.이어갈머리 = null;
|
|
373
|
+
this.표너비 = null;
|
|
311
374
|
}
|
|
312
375
|
|
|
313
376
|
/** 붙들고 있던 표를 내보낸다. 표가 아니었으면 옛 방식으로 한 줄씩 내보낸다. */
|
|
@@ -315,8 +378,23 @@ export class 마크다운 {
|
|
|
315
378
|
const 줄들 = this.표줄들;
|
|
316
379
|
this.표줄들 = [];
|
|
317
380
|
if (!줄들.length) return [];
|
|
318
|
-
|
|
319
|
-
|
|
381
|
+
let 쓴것 = 줄들;
|
|
382
|
+
let 그린것 = 표그리기(줄들, this.폭, { 너비고정: this.표너비 });
|
|
383
|
+
/*
|
|
384
|
+
* 제 가름줄이 없으면 상한에 걸려 끊긴 표의 뒷동강이다. 그때만 앞 묶음의
|
|
385
|
+
* 머리줄을 얹어 다시 그린다. 무조건 얹으면, 상한 직후에 진짜 새 표가
|
|
386
|
+
* 시작될 때 남의 머리줄이 그 표에 섞여 들어간다.
|
|
387
|
+
*/
|
|
388
|
+
if (!그린것 && this.이어갈머리) {
|
|
389
|
+
쓴것 = [...this.이어갈머리, ...줄들];
|
|
390
|
+
그린것 = 표그리기(쓴것, this.폭, { 너비고정: this.표너비 });
|
|
391
|
+
}
|
|
392
|
+
if (그린것) {
|
|
393
|
+
// 상한에 걸려 끊기는 묶음일 때만 너비를 기억해 둔다. 여느 표는 한 번에
|
|
394
|
+
// 다 나가므로 물려줄 데가 없다 — 그때까지 재면 헛일을 한 번 더 하는 것이다.
|
|
395
|
+
if (this.표너비 === null && 줄들.length >= 표붙들상한) this.표너비 = 표칸너비(쓴것, this.폭);
|
|
396
|
+
return 그린것;
|
|
397
|
+
}
|
|
320
398
|
const 나갈것 = [];
|
|
321
399
|
for (const 줄 of 줄들) {
|
|
322
400
|
const r = 한줄(줄, this.상태, this.폭);
|
|
@@ -343,9 +421,22 @@ export class 마크다운 {
|
|
|
343
421
|
*/
|
|
344
422
|
if (!this.상태.펜스 && 표줄인가(줄)) {
|
|
345
423
|
this.표줄들.push(줄);
|
|
346
|
-
|
|
424
|
+
if (this.표줄들.length < 표붙들상한) return [];
|
|
425
|
+
/*
|
|
426
|
+
* 여기서 끊는다. 다음 묶음이 표로 안 읽히지 않도록 머리줄을 기억해 둔다 —
|
|
427
|
+
* 세 번째 묶음까지 갈 수 있으므로, 이 묶음에 제 머리줄이 없으면 앞서
|
|
428
|
+
* 기억해 둔 것을 그대로 들고 간다.
|
|
429
|
+
*/
|
|
430
|
+
const 가름 = 가름찾기(this.표줄들.map(칸가르기));
|
|
431
|
+
const 머리 = 가름 >= 1 ? this.표줄들.slice(0, 가름 + 1) : this.이어갈머리;
|
|
432
|
+
const 나갈것 = this.#표비우기();
|
|
433
|
+
this.이어갈머리 = 머리;
|
|
434
|
+
return 나갈것;
|
|
347
435
|
}
|
|
348
436
|
|
|
437
|
+
// 표가 끝났다. 다음 표는 제 폭을 처음부터 다시 잰다.
|
|
438
|
+
this.이어갈머리 = null;
|
|
439
|
+
this.표너비 = null;
|
|
349
440
|
const 앞 = this.#표비우기();
|
|
350
441
|
const r = 한줄(줄, this.상태, this.폭);
|
|
351
442
|
this.상태 = r.상태;
|
|
@@ -377,8 +468,11 @@ export class 마크다운 {
|
|
|
377
468
|
* 그래도 상한은 둔다. 표가 아닌데 세로줄로 시작하는 긴 글(로그 한 덩이
|
|
378
469
|
* 같은 것)이 오면 화면이 멈춘 것처럼 보이면 안 된다.
|
|
379
470
|
*/
|
|
471
|
+
// 지금 담긴 것이 세로줄로 시작할 때만 기다린다. 「표를 붙들고 있으면
|
|
472
|
+
// 무조건 기다린다」 로 두면, 표 뒤에 이어지는 긴 산문이 줄이 끝날 때까지
|
|
473
|
+
// 화면에 한 글자도 안 나온다 — 사람 눈에는 답이 멈춘 것으로 보인다.
|
|
380
474
|
const 표기다림 = !this.상태.펜스
|
|
381
|
-
&&
|
|
475
|
+
&& /^\s*\|/.test(this.담긴것)
|
|
382
476
|
&& width(this.담긴것) <= this.폭 * 표줄최대배;
|
|
383
477
|
if (표기다림) break;
|
|
384
478
|
|
|
@@ -415,9 +509,13 @@ export class 마크다운 {
|
|
|
415
509
|
|
|
416
510
|
/** 통째로 있는 글을 한 번에 그린다 (스트리밍이 아닌 자리). */
|
|
417
511
|
export function 그리기(글, 폭 = 80) {
|
|
512
|
+
const s = String(글 ?? '');
|
|
418
513
|
const md = new 마크다운({ 폭 });
|
|
419
514
|
const 줄들 = [];
|
|
420
|
-
|
|
515
|
+
// 마지막 줄에 개행을 얹어 준다. 흘려보내는 자리가 아니므로 줄이 안 끝날 일이
|
|
516
|
+
// 없는데, 안 얹으면 마지막 줄이 폭을 넘는 순간 「아직 안 끝난 줄」 로 보여
|
|
517
|
+
// 그 줄만 날것으로 나간다 — 별표와 우물정자가 거기서만 화면에 남는다.
|
|
518
|
+
for (const x of md.넣기(s && !s.endsWith('\n') ? `${s}\n` : s)) 줄들.push(typeof x === 'string' ? x : x.이어붙임);
|
|
421
519
|
for (const x of md.끝()) 줄들.push(typeof x === 'string' ? x : x.이어붙임);
|
|
422
520
|
return 줄들;
|
|
423
521
|
}
|
package/src/ui/motion.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* 그림을 점자 코드로 직접 적으면 나중에 아무도 못 고친다. 그래서 눈에 보이는
|
|
29
29
|
* 대로 '#' 과 '.' 으로 적고, 켜질 때 한 번 점자로 바꾼다.
|
|
30
30
|
*/
|
|
31
|
-
import { 켜달라했나 as
|
|
31
|
+
import { 켜달라했나 as 사무실켜달라했나, 끄라는값인가 } from './office.js';
|
|
32
32
|
|
|
33
33
|
// 점 번호와 자리. 점자는 이렇게 번호가 매겨져 있다 —
|
|
34
34
|
// 1 4
|
|
@@ -183,7 +183,11 @@ export const 그림들 = {
|
|
|
183
183
|
* 한쪽으로만 지나가는 것과 왔다 갔다 하는 것은 느낌이 다르다. 앞의 것은
|
|
184
184
|
* '진행 중' 으로 읽히고, 뒤의 것은 '기다리는 중' 으로 읽힌다. 45초가 넘은
|
|
185
185
|
* 자리에서 진행 중인 척하면 곧 끝날 것처럼 보여서 오히려 답답해진다.
|
|
186
|
-
*
|
|
186
|
+
*
|
|
187
|
+
* 여덟 장이라 한 바퀴가 0.72초 — 이 테마에서 제일 길다(다음이 다섯 장).
|
|
188
|
+
* 기사·동물 쪽 느긋은 열 장(0.9초)인데, 그쪽은 넘어졌다 일어나는 그림이라
|
|
189
|
+
* 한 동작에 장이 더 든다. 세 테마가 같을 이유는 없고, 검사가 재는 것도
|
|
190
|
+
* 「제 테마 안에서 느긋이 제일 길다」 이지 장수 자체가 아니다.
|
|
187
191
|
*/
|
|
188
192
|
느긋: [
|
|
189
193
|
['......', '##....', '##....', '......'],
|
|
@@ -510,13 +514,31 @@ const 이름표 = {
|
|
|
510
514
|
*/
|
|
511
515
|
let 고른그림 = null;
|
|
512
516
|
export function 그림고르기설정(값) {
|
|
513
|
-
|
|
517
|
+
// 소문자로 한 번만 찾는다. 이름표에는 대문자 열쇠가 하나도 없어서, 원본
|
|
518
|
+
// 그대로 한 번 더 찾던 자리는 어떤 입력에도 안 걸리는 죽은 규칙이었다.
|
|
519
|
+
고른그림 = 이름표[String(값 ?? '').trim().toLowerCase()] ?? null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* `DEEL_MOTION` 이 **실제로 이기고 있나.**
|
|
524
|
+
*
|
|
525
|
+
* `/motion` 은 「환경변수가 켜져 있어 그쪽이 이깁니다」 를 `process.env.DEEL_MOTION`
|
|
526
|
+
* 이 비어 있지 않다는 것만 보고 띄우고 있었다. 그런데 `DEEL_MOTION=cat` 은
|
|
527
|
+
* 우리가 아는 이름이 아니라 **그냥 무시되고 설정 파일이 그대로 쓰인다.**
|
|
528
|
+
* 그러면 화면이 하는 말이 사실과 반대다 — 사람은 설정을 바꿔도 소용없다고
|
|
529
|
+
* 믿고 손을 떼는데, 정작 이기고 있는 것은 그 설정이다.
|
|
530
|
+
*
|
|
531
|
+
* @returns {null|{값: string, 이름: string|null, 이김: boolean}}
|
|
532
|
+
* 안 적혀 있으면 null. 적혀 있으면 그 값과, 우리가 아는 이름인지.
|
|
533
|
+
*/
|
|
534
|
+
export function 환경그림() {
|
|
535
|
+
const 값 = String(process.env.DEEL_MOTION ?? '').trim();
|
|
536
|
+
if (!값) return null;
|
|
537
|
+
const 이름 = 이름표[값.toLowerCase()] ?? null;
|
|
538
|
+
return { 값, 이름, 이김: 이름 !== null };
|
|
514
539
|
}
|
|
515
540
|
|
|
516
|
-
export const 테마이름 = () =>
|
|
517
|
-
?? 이름표[String(process.env.DEEL_MOTION ?? '').trim()]
|
|
518
|
-
?? 고른그림
|
|
519
|
-
?? '기본';
|
|
541
|
+
export const 테마이름 = () => 환경그림()?.이름 ?? 고른그림 ?? '기본';
|
|
520
542
|
export const 테마 = () => 테마들[테마이름()] ?? 테마들.기본;
|
|
521
543
|
|
|
522
544
|
/*
|
|
@@ -529,7 +551,15 @@ export const 테마 = () => 테마들[테마이름()] ?? 테마들.기본;
|
|
|
529
551
|
let 설정으로껐나 = false;
|
|
530
552
|
/** `/motion 끔` 이 여기에 넣는다. 환경변수는 따로 살아 있고, 둘 중 하나면 꺼진다. */
|
|
531
553
|
export function 끔설정(값) { 설정으로껐나 = !!값; }
|
|
532
|
-
|
|
554
|
+
/**
|
|
555
|
+
* 환경변수 쪽이 꺼 놨나. `/motion` 이 「바꿨습니다」 라고 말해도 되는지 여기서 본다.
|
|
556
|
+
*
|
|
557
|
+
* 끔() 하나로는 모자란다 — 그건 「설정으로 껐다」 와 「환경변수가 껐다」 를
|
|
558
|
+
* 뭉쳐서 준다. 앞엣것은 방금 사람이 고른 것이고, 뒤엣것은 사람이 무엇을 골라도
|
|
559
|
+
* 안 바뀌는 것이다. 둘을 뭉치면 「고른 것이 안 먹힌다」 를 말해 줄 수가 없다.
|
|
560
|
+
*/
|
|
561
|
+
export const 환경으로껐나 = () => 끄라는값인가();
|
|
562
|
+
export const 끔 = () => 환경으로껐나() || 설정으로껐나;
|
|
533
563
|
|
|
534
564
|
// 못 끄는 자리에서 쓸 한 칸짜리. working.js 의 돌림틀과 같은 것이다.
|
|
535
565
|
const 한칸 = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
@@ -542,7 +572,8 @@ const 한칸 = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '
|
|
|
542
572
|
*
|
|
543
573
|
* @param {string} 갈래 working.js 의 갈래 이름
|
|
544
574
|
* @param {number} 틱 90ms 마다 하나씩 오르는 수
|
|
545
|
-
* @returns {string} 점자
|
|
575
|
+
* @returns {string} 그 테마의 칸수만큼 점자 — 기본은 세 글자, 기사·동물은
|
|
576
|
+
* 여섯 글자다(넓은칸수). 끈 상태·사무실이면 한 글자.
|
|
546
577
|
*/
|
|
547
578
|
export function 그림고르기(갈래, 틱 = 0) {
|
|
548
579
|
const n = Math.trunc(틱) || 0;
|
package/src/ui/office.js
CHANGED
|
@@ -555,9 +555,30 @@ export function 켜달라했나() {
|
|
|
555
555
|
return 설정으로켰나;
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
+
/*
|
|
559
|
+
* `DEEL_NO_MOTION` 을 「끄라는 말」 로 읽어도 되나.
|
|
560
|
+
*
|
|
561
|
+
* `!!process.env.DEEL_NO_MOTION` 로 두고 있었다. 그러면 `DEEL_NO_MOTION=0` 도
|
|
562
|
+
* 끈다 — **끄지 말라고 적은 값이 끄는 것이다.** 바로 위 켜달라했나() 는 값을
|
|
563
|
+
* 읽어서 `DEEL_OFFICE=0` 을 껐다로 보므로, 같은 .env 에 둘을 나란히 적으면
|
|
564
|
+
* 하나만 말을 듣고 다른 하나는 반대로 간다. 그런데 화면에는 아무 말도 안 뜬다.
|
|
565
|
+
*
|
|
566
|
+
* 그래서 여기도 값을 읽는다. 안 적혀 있거나 0·false·off·no·빈 값이면 안 끈다.
|
|
567
|
+
* 그 밖의 값(1·true·yes·아무 글자)은 다 끄는 것으로 본다 — 끄려고 적었는데
|
|
568
|
+
* 철자가 달라서 안 꺼지는 쪽이 더 나쁘다.
|
|
569
|
+
*
|
|
570
|
+
* 이 자리에 두는 까닭: 이 값을 읽는 데가 셋(여기·motion.js·intro.js)인데,
|
|
571
|
+
* motion.js 가 이 파일을 가져다 쓰므로 반대로 가져오면 고리가 생긴다.
|
|
572
|
+
*/
|
|
573
|
+
const 안끄는값 = new Set(['', '0', 'false', 'off', 'no']);
|
|
574
|
+
export function 끄라는값인가(raw = process.env.DEEL_NO_MOTION) {
|
|
575
|
+
if (raw === undefined || raw === null) return false;
|
|
576
|
+
return !안끄는값.has(String(raw).trim().toLowerCase());
|
|
577
|
+
}
|
|
578
|
+
|
|
558
579
|
export function 켜나(rows = process.stdout.rows, cols = process.stdout.columns) {
|
|
559
580
|
if (!켜달라했나()) return false;
|
|
560
|
-
if (
|
|
581
|
+
if (끄라는값인가()) return false;
|
|
561
582
|
if (!색충분한가()) return false;
|
|
562
583
|
if ((cols ?? 0) < 최소폭) return false;
|
|
563
584
|
return (rows ?? 0) >= 최소높이;
|