@milcho0604/velog-mcp 0.4.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/LICENSE +21 -0
- package/README.ko.md +366 -0
- package/README.md +381 -0
- package/dist/auth.d.ts +57 -0
- package/dist/auth.js +124 -0
- package/dist/auth.js.map +1 -0
- package/dist/capabilities.d.ts +50 -0
- package/dist/capabilities.js +60 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/client.d.ts +113 -0
- package/dist/client.js +322 -0
- package/dist/client.js.map +1 -0
- package/dist/format.d.ts +31 -0
- package/dist/format.js +66 -0
- package/dist/format.js.map +1 -0
- package/dist/graphql.d.ts +29 -0
- package/dist/graphql.js +82 -0
- package/dist/graphql.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +149 -0
- package/dist/index.js.map +1 -0
- package/dist/me.d.ts +23 -0
- package/dist/me.js +35 -0
- package/dist/me.js.map +1 -0
- package/dist/ownership.d.ts +42 -0
- package/dist/ownership.js +62 -0
- package/dist/ownership.js.map +1 -0
- package/dist/plugin-env.d.ts +67 -0
- package/dist/plugin-env.js +102 -0
- package/dist/plugin-env.js.map +1 -0
- package/dist/ratelimit.d.ts +48 -0
- package/dist/ratelimit.js +79 -0
- package/dist/ratelimit.js.map +1 -0
- package/dist/render/chrome.d.ts +77 -0
- package/dist/render/chrome.js +287 -0
- package/dist/render/chrome.js.map +1 -0
- package/dist/render/cover.d.ts +29 -0
- package/dist/render/cover.js +195 -0
- package/dist/render/cover.js.map +1 -0
- package/dist/render/icons.d.ts +22 -0
- package/dist/render/icons.js +158 -0
- package/dist/render/icons.js.map +1 -0
- package/dist/render/index.d.ts +32 -0
- package/dist/render/index.js +137 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/page.d.ts +89 -0
- package/dist/render/page.js +761 -0
- package/dist/render/page.js.map +1 -0
- package/dist/render/tones.d.ts +30 -0
- package/dist/render/tones.js +46 -0
- package/dist/render/tones.js.map +1 -0
- package/dist/slug.d.ts +42 -0
- package/dist/slug.js +79 -0
- package/dist/slug.js.map +1 -0
- package/dist/tools/discover.d.ts +6 -0
- package/dist/tools/discover.js +106 -0
- package/dist/tools/discover.js.map +1 -0
- package/dist/tools/drafts.d.ts +23 -0
- package/dist/tools/drafts.js +227 -0
- package/dist/tools/drafts.js.map +1 -0
- package/dist/tools/export.d.ts +21 -0
- package/dist/tools/export.js +132 -0
- package/dist/tools/export.js.map +1 -0
- package/dist/tools/images.d.ts +34 -0
- package/dist/tools/images.js +556 -0
- package/dist/tools/images.js.map +1 -0
- package/dist/tools/posts.d.ts +14 -0
- package/dist/tools/posts.js +82 -0
- package/dist/tools/posts.js.map +1 -0
- package/dist/tools/profile-edit.d.ts +15 -0
- package/dist/tools/profile-edit.js +216 -0
- package/dist/tools/profile-edit.js.map +1 -0
- package/dist/tools/profile.d.ts +9 -0
- package/dist/tools/profile.js +133 -0
- package/dist/tools/profile.js.map +1 -0
- package/dist/tools/publish.d.ts +16 -0
- package/dist/tools/publish.js +424 -0
- package/dist/tools/publish.js.map +1 -0
- package/dist/tools/stats.d.ts +32 -0
- package/dist/tools/stats.js +154 -0
- package/dist/tools/stats.js.map +1 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/docs/PRD.md +146 -0
- package/docs/api-reference.md +329 -0
- package/docs/architecture.md +112 -0
- package/docs/decisions/0001-why-build-our-own.md +89 -0
- package/docs/decisions/0002-draft-only-write.md +84 -0
- package/docs/decisions/0003-token-env-only.md +109 -0
- package/docs/decisions/0004-capability-model.md +123 -0
- package/docs/decisions/0005-render-in-server.md +117 -0
- package/docs/decisions/0006-ship-as-plugin.md +532 -0
- package/docs/security.md +384 -0
- package/docs/tools.md +404 -0
- package/npm-shrinkwrap.json +2345 -0
- package/package.json +61 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 글 표지(썸네일) 카드.
|
|
3
|
+
*
|
|
4
|
+
* 벨로그는 글 목록·SNS 공유에서 `thumbnail` 을 쓴다. 없으면 본문 첫 이미지를 쓰거나
|
|
5
|
+
* 그냥 비어 보인다. 제목만 있는 글이라도 표지 한 장이 있으면 목록에서 확실히 다르다.
|
|
6
|
+
*
|
|
7
|
+
* 크기는 1200×630 — OG 이미지 표준 비율이라 트위터·슬랙 미리보기에서 잘리지 않는다.
|
|
8
|
+
* 줄바꿈은 여기서도 브라우저 실측으로 한다. 글자수로 자르면 한글에서 반드시 틀린다.
|
|
9
|
+
*/
|
|
10
|
+
import { tone } from "./tones.js";
|
|
11
|
+
const STYLE = `
|
|
12
|
+
html,body { margin:0; padding:0; }
|
|
13
|
+
#wrap { position:relative; }
|
|
14
|
+
svg { display:block; }
|
|
15
|
+
.kicker { font-weight:700; letter-spacing:1.6px; }
|
|
16
|
+
.title { font-weight:800; }
|
|
17
|
+
.sub { font-weight:400; }
|
|
18
|
+
.tag { font-weight:600; }
|
|
19
|
+
.foot { font-weight:600; }
|
|
20
|
+
text { font-family:-apple-system,BlinkMacSystemFont,"Apple SD Gothic Neo","Pretendard","Noto Sans KR","Malgun Gothic",sans-serif;
|
|
21
|
+
text-rendering:geometricPrecision; }
|
|
22
|
+
`;
|
|
23
|
+
// ★ page.ts 와 같은 규칙: 이 문자열 안에서는 백틱과 ${ 를 쓰지 않는다.
|
|
24
|
+
const SCRIPT = `
|
|
25
|
+
(function(){
|
|
26
|
+
'use strict';
|
|
27
|
+
var S = JSON.parse(document.getElementById('spec').textContent);
|
|
28
|
+
var NS = 'http://www.w3.org/2000/svg';
|
|
29
|
+
var svg = document.getElementById('cv');
|
|
30
|
+
var W = 1200, H = 630, M = 84;
|
|
31
|
+
svg.setAttribute('width', W); svg.setAttribute('height', H);
|
|
32
|
+
svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
|
|
33
|
+
document.getElementById('wrap').style.width = W + 'px';
|
|
34
|
+
document.getElementById('wrap').style.height = H + 'px';
|
|
35
|
+
|
|
36
|
+
function el(n, a, p){
|
|
37
|
+
var e = document.createElementNS(NS, n);
|
|
38
|
+
for (var k in a) { if (a[k] !== null && a[k] !== undefined) e.setAttribute(k, a[k]); }
|
|
39
|
+
(p || svg).appendChild(e);
|
|
40
|
+
return e;
|
|
41
|
+
}
|
|
42
|
+
function txt(x, y, s, size, weightCls, fill, anchor){
|
|
43
|
+
var t = el('text', {x:x, y:y, 'class':weightCls, 'font-size':size, fill:fill,
|
|
44
|
+
'text-anchor':anchor || 'start'});
|
|
45
|
+
t.textContent = s;
|
|
46
|
+
return t;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var defs = el('defs', {});
|
|
50
|
+
var grad = el('linearGradient', {id:'bg', x1:'0', y1:'0', x2:'1', y2:'1'}, defs);
|
|
51
|
+
el('stop', {offset:'0%', 'stop-color':'#ffffff'}, grad);
|
|
52
|
+
el('stop', {offset:'100%', 'stop-color':S.fill}, grad);
|
|
53
|
+
el('rect', {x:0, y:0, width:W, height:H, fill:'url(#bg)'});
|
|
54
|
+
el('rect', {x:0, y:0, width:14, height:H, fill:S.solid});
|
|
55
|
+
// 오른쪽 아래 은은한 원 — 빈 공간이 허전하지 않게
|
|
56
|
+
el('circle', {cx:W - 90, cy:H - 60, r:190, fill:S.solid, opacity:'0.055'});
|
|
57
|
+
el('circle', {cx:W - 40, cy:H + 10, r:110, fill:S.solid, opacity:'0.06'});
|
|
58
|
+
|
|
59
|
+
var scratch = el('g', {visibility:'hidden'});
|
|
60
|
+
function widthOf(s, size, cls){
|
|
61
|
+
var t = el('text', {'font-size':size, 'class':cls}, scratch);
|
|
62
|
+
t.textContent = s;
|
|
63
|
+
var w = t.getBBox().width;
|
|
64
|
+
scratch.removeChild(t);
|
|
65
|
+
return w;
|
|
66
|
+
}
|
|
67
|
+
// 실측 기반 줄바꿈. 공백이 없으면(한글 긴 제목) 글자 단위로 끊는다.
|
|
68
|
+
function wrap(s, size, cls, maxW, maxLines){
|
|
69
|
+
var words = s.split(' ');
|
|
70
|
+
var lines = [], cur = '';
|
|
71
|
+
for (var i = 0; i < words.length; i++) {
|
|
72
|
+
var probe = cur ? cur + ' ' + words[i] : words[i];
|
|
73
|
+
if (widthOf(probe, size, cls) <= maxW || !cur) {
|
|
74
|
+
if (widthOf(probe, size, cls) > maxW && !cur) {
|
|
75
|
+
// 단어 하나가 이미 넘친다 — 글자 단위로 끊는다
|
|
76
|
+
var piece = '';
|
|
77
|
+
for (var c = 0; c < probe.length; c++) {
|
|
78
|
+
if (widthOf(piece + probe[c], size, cls) > maxW && piece) {
|
|
79
|
+
lines.push(piece); piece = probe[c];
|
|
80
|
+
if (lines.length >= maxLines) break;
|
|
81
|
+
} else piece += probe[c];
|
|
82
|
+
}
|
|
83
|
+
cur = piece;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
cur = probe;
|
|
87
|
+
} else { lines.push(cur); cur = words[i]; }
|
|
88
|
+
if (lines.length >= maxLines) break;
|
|
89
|
+
}
|
|
90
|
+
if (cur && lines.length < maxLines) lines.push(cur);
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
var truncated = [];
|
|
95
|
+
var bare = function(v){ return v.replace(/\\s/g, ''); };
|
|
96
|
+
|
|
97
|
+
if (S.kicker) txt(M, M + 8, S.kicker, 20, 'kicker', S.solid);
|
|
98
|
+
|
|
99
|
+
// 제목: 3줄에 안 들어가면 글자 크기를 줄여 다시 시도한다
|
|
100
|
+
var size = 62, lines = [];
|
|
101
|
+
var MAXW = W - M * 2 - 40;
|
|
102
|
+
for (;;) {
|
|
103
|
+
lines = wrap(S.title, size, 'title', MAXW, 3);
|
|
104
|
+
if (bare(lines.join('')).length >= bare(S.title).length || size <= 40) break;
|
|
105
|
+
size -= 4;
|
|
106
|
+
}
|
|
107
|
+
if (bare(lines.join('')).length < bare(S.title).length) truncated.push('제목');
|
|
108
|
+
|
|
109
|
+
var subLines = [];
|
|
110
|
+
if (S.subtitle) {
|
|
111
|
+
subLines = wrap(S.subtitle, 25, 'sub', MAXW, 2);
|
|
112
|
+
if (bare(subLines.join('')).length < bare(S.subtitle).length) truncated.push('부제');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ★ 위에서부터 쌓기만 하면 제목이 짧을 때 아래가 휑하다.
|
|
116
|
+
// 케커 아래 ~ 태그 위 사이 공간에 글 덩어리를 세로 중앙으로 놓는다.
|
|
117
|
+
var blockH = lines.length * (size + 6) + (subLines.length ? 30 + subLines.length * 34 : 0);
|
|
118
|
+
var areaTop = M + (S.kicker ? 54 : 10);
|
|
119
|
+
var areaBottom = H - M - (S.tags && S.tags.length ? 54 : 10);
|
|
120
|
+
var y = areaTop + Math.max(0, (areaBottom - areaTop - blockH) / 2);
|
|
121
|
+
|
|
122
|
+
for (var li = 0; li < lines.length; li++) {
|
|
123
|
+
y += size + 6;
|
|
124
|
+
txt(M, y, lines[li], size, 'title', '#0f172a');
|
|
125
|
+
}
|
|
126
|
+
if (subLines.length) {
|
|
127
|
+
y += 30;
|
|
128
|
+
for (var si = 0; si < subLines.length; si++) {
|
|
129
|
+
y += 34;
|
|
130
|
+
txt(M, y, subLines[si], 25, 'sub', '#475569');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (S.tags && S.tags.length) {
|
|
135
|
+
var tx = M, ty = H - M + 4;
|
|
136
|
+
for (var ti = 0; ti < S.tags.length; ti++) {
|
|
137
|
+
var label = '#' + S.tags[ti];
|
|
138
|
+
var tw = widthOf(label, 19, 'tag') + 30;
|
|
139
|
+
if (tx + tw > W - M) { truncated.push('태그 ' + label); continue; }
|
|
140
|
+
el('rect', {x:tx, y:ty - 25, width:tw, height:38, rx:19, fill:'#ffffff',
|
|
141
|
+
stroke:S.stroke, 'stroke-width':1.4});
|
|
142
|
+
var tt = txt(tx + tw/2, ty, label, 19, 'tag', S.solid, 'middle');
|
|
143
|
+
tx += tw + 12;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (S.footer) txt(W - M, M + 8, S.footer, 19, 'foot', '#94a3b8', 'end');
|
|
148
|
+
|
|
149
|
+
// ★ 상단 라벨(왼쪽)과 서명(오른쪽)은 같은 높이에 고정 배치된다. 둘 다 길면 가운데서
|
|
150
|
+
// 부딪히는데 여기엔 줄바꿈도 축소도 없다 — 감사만이라도 해야 조용히 안 나간다.
|
|
151
|
+
if (S.kicker || S.footer) {
|
|
152
|
+
var kw = S.kicker ? widthOf(S.kicker, 20, 'kicker') : 0;
|
|
153
|
+
var fw = S.footer ? widthOf(S.footer, 19, 'foot') : 0;
|
|
154
|
+
if (M + kw > W - M - fw - 20) truncated.push('상단 라벨과 서명이 겹침');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
svg.removeChild(scratch);
|
|
158
|
+
document.title = 'AUDIT ' + JSON.stringify({w:W, h:H, truncated:truncated, titleSize:size});
|
|
159
|
+
})();
|
|
160
|
+
`;
|
|
161
|
+
function safeJson(value) {
|
|
162
|
+
return JSON.stringify(value).replace(/</g, '\\u003c');
|
|
163
|
+
}
|
|
164
|
+
export function buildCoverHtml(spec) {
|
|
165
|
+
const t = tone(spec.tone, 'blue');
|
|
166
|
+
const payload = {
|
|
167
|
+
title: spec.title,
|
|
168
|
+
subtitle: spec.subtitle ?? '',
|
|
169
|
+
kicker: spec.kicker ?? '',
|
|
170
|
+
tags: spec.tags ?? [],
|
|
171
|
+
footer: spec.footer ?? '',
|
|
172
|
+
fill: t.fill,
|
|
173
|
+
stroke: t.stroke,
|
|
174
|
+
solid: t.solid,
|
|
175
|
+
};
|
|
176
|
+
return [
|
|
177
|
+
'<!DOCTYPE html>',
|
|
178
|
+
'<html lang="ko"><head><meta charset="utf-8"><title>rendering…</title>',
|
|
179
|
+
`<style>${STYLE}</style>`,
|
|
180
|
+
'</head><body>',
|
|
181
|
+
'<div id="wrap"><svg id="cv" xmlns="http://www.w3.org/2000/svg"></svg></div>',
|
|
182
|
+
`<script type="application/json" id="spec">${safeJson(payload)}</script>`,
|
|
183
|
+
`<script>${SCRIPT}</script>`,
|
|
184
|
+
'</body></html>',
|
|
185
|
+
].join('\n');
|
|
186
|
+
}
|
|
187
|
+
export function formatCoverAudit(a) {
|
|
188
|
+
const notes = [];
|
|
189
|
+
if (a.truncated.length)
|
|
190
|
+
notes.push(`넘쳐서 잘린 것: ${a.truncated.join(', ')}`);
|
|
191
|
+
if (a.titleSize < 62)
|
|
192
|
+
notes.push(`제목이 길어 글자 크기를 ${a.titleSize}px 로 줄였습니다`);
|
|
193
|
+
return notes.length ? `⚠️ ${notes.join(' / ')}` : '표지 생성 완료 — 잘린 곳 없음';
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=cover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cover.js","sourceRoot":"","sources":["../../src/render/cover.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAsBlC,MAAM,KAAK,GAAG;;;;;;;;;;;CAWb,CAAC;AAEF,iDAAiD;AACjD,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwId,CAAC;AAEF,SAAS,QAAQ,CAAC,KAAc;IAC/B,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAe;IAC7C,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAClC,MAAM,OAAO,GAAG;QACf,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;QAC7B,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,EAAE;QACzB,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE;QACrB,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,EAAE;QACzB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,KAAK,EAAE,CAAC,CAAC,KAAK;KACd,CAAC;IACF,OAAO;QACN,iBAAiB;QACjB,uEAAuE;QACvE,UAAU,KAAK,UAAU;QACzB,eAAe;QACf,6EAA6E;QAC7E,6CAA6C,QAAQ,CAAC,OAAO,CAAC,WAAW;QACzE,WAAW,MAAM,WAAW;QAC5B,gBAAgB;KAChB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,CAAa;IAC7C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1E,IAAI,CAAC,CAAC,SAAS,GAAG,EAAE;QAAE,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,SAAS,YAAY,CAAC,CAAC;IAC3E,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,oBAAoB,CAAC;AACxE,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 내장 아이콘.
|
|
3
|
+
*
|
|
4
|
+
* ★ 왜 simple-icons(브랜드 로고)를 안 쓰나
|
|
5
|
+
* - 네트워크: 렌더러는 DNS 를 막은 채로 돈다(render/chrome.ts). 외부 아이콘을
|
|
6
|
+
* 받아올 수 없고, 받아오게 만들면 그 보장이 깨진다.
|
|
7
|
+
* - 상표: 공개 저장소에 남의 브랜드 마크를 싣는 건 별개의 문제를 만든다.
|
|
8
|
+
* - 실용: 블로그 그림에서 정작 필요한 건 '서버·큐·DB·알림' 같은 역할 표시다.
|
|
9
|
+
*
|
|
10
|
+
* ★ 왜 이모지가 아니라 흰 글리프인가
|
|
11
|
+
* 이모지는 OS 마다 다르게 그려지고 배지 안에서 유치해 보인다. 흰 선으로 그린
|
|
12
|
+
* 글리프를 색 배지 위에 올리면 어디서 렌더하든 같은 그림이 나온다.
|
|
13
|
+
*
|
|
14
|
+
* ★ 왜 path 문자열이 아니라 도형 배열인가
|
|
15
|
+
* 손으로 쓴 path 는 한 글자만 틀려도 조용히 이상하게 그려진다. 사각형·원·선으로
|
|
16
|
+
* 쪼개두면 각 조각이 눈으로 검증된다. 곡선이 꼭 필요한 것만 path 로 남겼다.
|
|
17
|
+
*
|
|
18
|
+
* 좌표계는 24×24. 선 기반이라 stroke 만 쓰고 fill 은 점(`d`)에서만 쓴다.
|
|
19
|
+
*/
|
|
20
|
+
export type Prim = readonly ['r', number, number, number, number, number] | readonly ['c', number, number, number] | readonly ['e', number, number, number, number] | readonly ['d', number, number, number] | readonly ['l', number, number, number, number] | readonly ['p', string];
|
|
21
|
+
export declare const ICONS: Record<string, readonly Prim[]>;
|
|
22
|
+
export declare const ICON_NAMES: string[];
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 내장 아이콘.
|
|
3
|
+
*
|
|
4
|
+
* ★ 왜 simple-icons(브랜드 로고)를 안 쓰나
|
|
5
|
+
* - 네트워크: 렌더러는 DNS 를 막은 채로 돈다(render/chrome.ts). 외부 아이콘을
|
|
6
|
+
* 받아올 수 없고, 받아오게 만들면 그 보장이 깨진다.
|
|
7
|
+
* - 상표: 공개 저장소에 남의 브랜드 마크를 싣는 건 별개의 문제를 만든다.
|
|
8
|
+
* - 실용: 블로그 그림에서 정작 필요한 건 '서버·큐·DB·알림' 같은 역할 표시다.
|
|
9
|
+
*
|
|
10
|
+
* ★ 왜 이모지가 아니라 흰 글리프인가
|
|
11
|
+
* 이모지는 OS 마다 다르게 그려지고 배지 안에서 유치해 보인다. 흰 선으로 그린
|
|
12
|
+
* 글리프를 색 배지 위에 올리면 어디서 렌더하든 같은 그림이 나온다.
|
|
13
|
+
*
|
|
14
|
+
* ★ 왜 path 문자열이 아니라 도형 배열인가
|
|
15
|
+
* 손으로 쓴 path 는 한 글자만 틀려도 조용히 이상하게 그려진다. 사각형·원·선으로
|
|
16
|
+
* 쪼개두면 각 조각이 눈으로 검증된다. 곡선이 꼭 필요한 것만 path 로 남겼다.
|
|
17
|
+
*
|
|
18
|
+
* 좌표계는 24×24. 선 기반이라 stroke 만 쓰고 fill 은 점(`d`)에서만 쓴다.
|
|
19
|
+
*/
|
|
20
|
+
export const ICONS = {
|
|
21
|
+
server: [
|
|
22
|
+
['r', 3, 4, 18, 7, 2],
|
|
23
|
+
['r', 3, 13, 18, 7, 2],
|
|
24
|
+
['d', 6.5, 7.5, 1],
|
|
25
|
+
['d', 6.5, 16.5, 1],
|
|
26
|
+
],
|
|
27
|
+
database: [
|
|
28
|
+
['e', 12, 6, 7, 3],
|
|
29
|
+
['p', 'M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6'],
|
|
30
|
+
['p', 'M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3'],
|
|
31
|
+
],
|
|
32
|
+
cache: [
|
|
33
|
+
['e', 12, 6, 7, 3],
|
|
34
|
+
['p', 'M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6'],
|
|
35
|
+
['p', 'M9 11.5 13.5 11.5 10.5 16.5 15 16.5'],
|
|
36
|
+
],
|
|
37
|
+
cloud: [['p', 'M7 18.5h10a4.2 4.2 0 0 0 .3-8.4 5.8 5.8 0 0 0-11-1.5A3.9 3.9 0 0 0 7 18.5z']],
|
|
38
|
+
browser: [
|
|
39
|
+
['r', 3, 4, 18, 16, 2],
|
|
40
|
+
['l', 3, 9, 21, 9],
|
|
41
|
+
['d', 6.2, 6.5, 0.9],
|
|
42
|
+
['d', 9.2, 6.5, 0.9],
|
|
43
|
+
],
|
|
44
|
+
mobile: [
|
|
45
|
+
['r', 7, 3, 10, 18, 2],
|
|
46
|
+
['l', 10.5, 18, 13.5, 18],
|
|
47
|
+
],
|
|
48
|
+
layers: [
|
|
49
|
+
['p', 'M12 3 3 7.5l9 4.5 9-4.5z'],
|
|
50
|
+
['p', 'M3 12.2l9 4.5 9-4.5'],
|
|
51
|
+
['p', 'M3 16.7l9 4.5 9-4.5'],
|
|
52
|
+
],
|
|
53
|
+
clock: [
|
|
54
|
+
['c', 12, 12, 9],
|
|
55
|
+
['p', 'M12 6.8v5.4l3.4 2'],
|
|
56
|
+
],
|
|
57
|
+
bell: [
|
|
58
|
+
['p', 'M18 16.5v-5.5a6 6 0 1 0-12 0v5.5l-2 2h16z'],
|
|
59
|
+
['p', 'M9.9 20.6a2.4 2.4 0 0 0 4.2 0'],
|
|
60
|
+
],
|
|
61
|
+
chart: [
|
|
62
|
+
['p', 'M4 20h16'],
|
|
63
|
+
['p', 'M7.5 20v-5.5'],
|
|
64
|
+
['p', 'M12 20V7.5'],
|
|
65
|
+
['p', 'M16.5 20v-9'],
|
|
66
|
+
],
|
|
67
|
+
lock: [
|
|
68
|
+
['r', 5, 11, 14, 9, 2],
|
|
69
|
+
['p', 'M8.5 11V8a3.5 3.5 0 0 1 7 0v3'],
|
|
70
|
+
],
|
|
71
|
+
key: [
|
|
72
|
+
['c', 8, 12, 3.8],
|
|
73
|
+
['l', 11.8, 12, 20.5, 12],
|
|
74
|
+
['l', 17, 12, 17, 15.5],
|
|
75
|
+
['l', 20.5, 12, 20.5, 15],
|
|
76
|
+
],
|
|
77
|
+
code: [
|
|
78
|
+
['p', 'M9 7.5 4 12l5 4.5'],
|
|
79
|
+
['p', 'M15 7.5 20 12l-5 4.5'],
|
|
80
|
+
],
|
|
81
|
+
file: [
|
|
82
|
+
['p', 'M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z'],
|
|
83
|
+
['p', 'M14 3v5h5'],
|
|
84
|
+
],
|
|
85
|
+
user: [
|
|
86
|
+
['c', 12, 8.5, 3.6],
|
|
87
|
+
['p', 'M5 20.5a7 7 0 0 1 14 0'],
|
|
88
|
+
],
|
|
89
|
+
// ★ 톱니는 몸통 원에 **붙어** 있어야 한다. 처음엔 중심에서 떨어진 위치에
|
|
90
|
+
// 짧은 선만 뒀더니 톱니바퀴가 아니라 해 모양으로 읽혔다(렌더 실물로 확인).
|
|
91
|
+
gear: [
|
|
92
|
+
['c', 12, 12, 6.2],
|
|
93
|
+
['c', 12, 12, 2.4],
|
|
94
|
+
['l', 12, 5.8, 12, 2.8],
|
|
95
|
+
['l', 12, 18.2, 12, 21.2],
|
|
96
|
+
['l', 5.8, 12, 2.8, 12],
|
|
97
|
+
['l', 18.2, 12, 21.2, 12],
|
|
98
|
+
['l', 16.4, 7.6, 18.5, 5.5],
|
|
99
|
+
['l', 7.6, 16.4, 5.5, 18.5],
|
|
100
|
+
['l', 16.4, 16.4, 18.5, 18.5],
|
|
101
|
+
['l', 7.6, 7.6, 5.5, 5.5],
|
|
102
|
+
],
|
|
103
|
+
alert: [
|
|
104
|
+
['p', 'M12 3.5 2.5 20.5h19z'],
|
|
105
|
+
['l', 12, 9.5, 12, 14.5],
|
|
106
|
+
['d', 12, 17.6, 1.1],
|
|
107
|
+
],
|
|
108
|
+
check: [['p', 'M4.5 12.5 9.5 17.5 19.5 6.5']],
|
|
109
|
+
cross: [
|
|
110
|
+
['l', 6, 6, 18, 18],
|
|
111
|
+
['l', 18, 6, 6, 18],
|
|
112
|
+
],
|
|
113
|
+
arrow: [
|
|
114
|
+
['l', 4, 12, 18.5, 12],
|
|
115
|
+
['p', 'M14 7l5 5-5 5'],
|
|
116
|
+
],
|
|
117
|
+
branch: [
|
|
118
|
+
['d', 7, 5.5, 1.7],
|
|
119
|
+
['d', 7, 18.5, 1.7],
|
|
120
|
+
['d', 17, 9, 1.7],
|
|
121
|
+
['l', 7, 7.2, 7, 16.8],
|
|
122
|
+
['p', 'M17 10.7v1.3a3 3 0 0 1-3 3H8.7'],
|
|
123
|
+
],
|
|
124
|
+
package: [
|
|
125
|
+
['p', 'M12 3 3.5 7.5v9L12 21l8.5-4.5v-9z'],
|
|
126
|
+
['p', 'M3.5 7.5 12 12l8.5-4.5'],
|
|
127
|
+
['l', 12, 12, 12, 21],
|
|
128
|
+
],
|
|
129
|
+
terminal: [
|
|
130
|
+
['r', 3, 4, 18, 16, 2],
|
|
131
|
+
['p', 'M7 10l3 2.6-3 2.6'],
|
|
132
|
+
['l', 12.5, 15.5, 17, 15.5],
|
|
133
|
+
],
|
|
134
|
+
// 셋을 서로 잇는 삼각형. 위 하나에서 아래로 갈라지기만 하면 사람 모양으로 읽힌다.
|
|
135
|
+
network: [
|
|
136
|
+
['d', 12, 5, 2.1],
|
|
137
|
+
['d', 5.5, 18.5, 2.1],
|
|
138
|
+
['d', 18.5, 18.5, 2.1],
|
|
139
|
+
['l', 10.9, 7.2, 6.6, 16.3],
|
|
140
|
+
['l', 13.1, 7.2, 17.4, 16.3],
|
|
141
|
+
['l', 7.8, 18.5, 16.2, 18.5],
|
|
142
|
+
],
|
|
143
|
+
mail: [
|
|
144
|
+
['r', 3, 5, 18, 14, 2],
|
|
145
|
+
['p', 'M3.7 6.6 12 13l8.3-6.4'],
|
|
146
|
+
],
|
|
147
|
+
search: [
|
|
148
|
+
['c', 10.5, 10.5, 5.9],
|
|
149
|
+
['l', 14.8, 14.8, 20.2, 20.2],
|
|
150
|
+
],
|
|
151
|
+
retry: [
|
|
152
|
+
['p', 'M20 12a8 8 0 1 1-2.5-5.8'],
|
|
153
|
+
['p', 'M20.5 3.8v4.6H16'],
|
|
154
|
+
],
|
|
155
|
+
bolt: [['p', 'M13.2 2.5 5.5 13.5h5.6l-1 8L18.5 10.5h-5.6z']],
|
|
156
|
+
};
|
|
157
|
+
export const ICON_NAMES = Object.keys(ICONS).sort();
|
|
158
|
+
//# sourceMappingURL=icons.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icons.js","sourceRoot":"","sources":["../../src/render/icons.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAUH,MAAM,CAAC,MAAM,KAAK,GAAoC;IACrD,MAAM,EAAE;QACP,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QACrB,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QACtB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;QAClB,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;KACnB;IACD,QAAQ,EAAE;QACT,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAClB,CAAC,GAAG,EAAE,qCAAqC,CAAC;QAC5C,CAAC,GAAG,EAAE,iCAAiC,CAAC;KACxC;IACD,KAAK,EAAE;QACN,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAClB,CAAC,GAAG,EAAE,qCAAqC,CAAC;QAC5C,CAAC,GAAG,EAAE,qCAAqC,CAAC;KAC5C;IACD,KAAK,EAAE,CAAC,CAAC,GAAG,EAAE,4EAA4E,CAAC,CAAC;IAC5F,OAAO,EAAE;QACR,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QACtB,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAClB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC;QACpB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC;KACpB;IACD,MAAM,EAAE;QACP,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QACtB,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC;KACzB;IACD,MAAM,EAAE;QACP,CAAC,GAAG,EAAE,0BAA0B,CAAC;QACjC,CAAC,GAAG,EAAE,qBAAqB,CAAC;QAC5B,CAAC,GAAG,EAAE,qBAAqB,CAAC;KAC5B;IACD,KAAK,EAAE;QACN,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QAChB,CAAC,GAAG,EAAE,mBAAmB,CAAC;KAC1B;IACD,IAAI,EAAE;QACL,CAAC,GAAG,EAAE,2CAA2C,CAAC;QAClD,CAAC,GAAG,EAAE,+BAA+B,CAAC;KACtC;IACD,KAAK,EAAE;QACN,CAAC,GAAG,EAAE,UAAU,CAAC;QACjB,CAAC,GAAG,EAAE,cAAc,CAAC;QACrB,CAAC,GAAG,EAAE,YAAY,CAAC;QACnB,CAAC,GAAG,EAAE,aAAa,CAAC;KACpB;IACD,IAAI,EAAE;QACL,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QACtB,CAAC,GAAG,EAAE,+BAA+B,CAAC;KACtC;IACD,GAAG,EAAE;QACJ,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC;QACjB,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC;QACzB,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC;QACvB,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC;KACzB;IACD,IAAI,EAAE;QACL,CAAC,GAAG,EAAE,mBAAmB,CAAC;QAC1B,CAAC,GAAG,EAAE,sBAAsB,CAAC;KAC7B;IACD,IAAI,EAAE;QACL,CAAC,GAAG,EAAE,4DAA4D,CAAC;QACnE,CAAC,GAAG,EAAE,WAAW,CAAC;KAClB;IACD,IAAI,EAAE;QACL,CAAC,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC;QACnB,CAAC,GAAG,EAAE,wBAAwB,CAAC;KAC/B;IACD,8CAA8C;IAC9C,+CAA+C;IAC/C,IAAI,EAAE;QACL,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC;QAClB,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC;QAClB,CAAC,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,CAAC;QACvB,CAAC,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC;QACzB,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC;QACvB,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC;QACzB,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC;QAC3B,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC;QAC3B,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;QAC7B,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC;KACzB;IACD,KAAK,EAAE;QACN,CAAC,GAAG,EAAE,sBAAsB,CAAC;QAC7B,CAAC,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,CAAC;QACxB,CAAC,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC;KACpB;IACD,KAAK,EAAE,CAAC,CAAC,GAAG,EAAE,6BAA6B,CAAC,CAAC;IAC7C,KAAK,EAAE;QACN,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC;QACnB,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;KACnB;IACD,KAAK,EAAE;QACN,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC;QACtB,CAAC,GAAG,EAAE,eAAe,CAAC;KACtB;IACD,MAAM,EAAE;QACP,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC;QAClB,CAAC,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC;QACnB,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,GAAG,CAAC;QACjB,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,CAAC;QACtB,CAAC,GAAG,EAAE,gCAAgC,CAAC;KACvC;IACD,OAAO,EAAE;QACR,CAAC,GAAG,EAAE,mCAAmC,CAAC;QAC1C,CAAC,GAAG,EAAE,wBAAwB,CAAC;QAC/B,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;KACrB;IACD,QAAQ,EAAE;QACT,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QACtB,CAAC,GAAG,EAAE,mBAAmB,CAAC;QAC1B,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC;KAC3B;IACD,iDAAiD;IACjD,OAAO,EAAE;QACR,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,GAAG,CAAC;QACjB,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC;QACrB,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC;QACtB,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC;QAC3B,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC;QAC5B,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;KAC5B;IACD,IAAI,EAAE;QACL,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QACtB,CAAC,GAAG,EAAE,wBAAwB,CAAC;KAC/B;IACD,MAAM,EAAE;QACP,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC;QACtB,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;KAC7B;IACD,KAAK,EAAE;QACN,CAAC,GAAG,EAAE,0BAA0B,CAAC;QACjC,CAAC,GAAG,EAAE,kBAAkB,CAAC;KACzB;IACD,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,6CAA6C,CAAC,CAAC;CAC5D,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 렌더 실행기 — HTML 을 만들고, 자가감사 결과를 읽고, PNG 를 뽑는다.
|
|
3
|
+
*
|
|
4
|
+
* 크롬을 두 번 부른다.
|
|
5
|
+
* ① `--dump-dom` : 스크립트가 계산한 캔버스 크기와 자가감사 결과를 받는다.
|
|
6
|
+
* ② `--screenshot`: ①에서 받은 크기로 정확히 찍는다.
|
|
7
|
+
* 한 번에 하려면 창 크기를 미리 알아야 하는데, 그 크기는 글자 실측 뒤에야 정해진다.
|
|
8
|
+
* 즉 이 왕복이 "글자 폭을 추정하지 않는다"는 규칙의 대가다. 한 번에 1초 남짓이다.
|
|
9
|
+
*
|
|
10
|
+
* ★ 이 모듈은 토큰을 만지지 않는다. 파일을 쓰는 건 렌더 산출물뿐이다.
|
|
11
|
+
* (safety.test.ts 의 A5 가 '파일을 쓰는 모듈은 토큰 경로를 참조하지 않는다'로
|
|
12
|
+
* 이 분리를 강제한다.)
|
|
13
|
+
*/
|
|
14
|
+
import { type AuditReport, type DiagramSpec } from './page.ts';
|
|
15
|
+
import { type CoverAudit, type CoverSpec } from './cover.ts';
|
|
16
|
+
export interface RenderResult<A> {
|
|
17
|
+
/** 뽑아낸 PNG 경로 */
|
|
18
|
+
pngPath: string;
|
|
19
|
+
/** 손으로 고치고 싶을 때 열어보는 HTML */
|
|
20
|
+
htmlPath: string;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
scale: number;
|
|
24
|
+
audit: A;
|
|
25
|
+
bytes: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function renderDiagram(spec: DiagramSpec, scale?: number): Promise<RenderResult<AuditReport>>;
|
|
28
|
+
export declare function renderCover(spec: CoverSpec, scale?: number): Promise<RenderResult<CoverAudit>>;
|
|
29
|
+
export { type DiagramSpec, type AuditReport, formatAudit } from './page.ts';
|
|
30
|
+
export { type CoverSpec, type CoverAudit, formatCoverAudit } from './cover.ts';
|
|
31
|
+
export { ICON_NAMES } from './icons.ts';
|
|
32
|
+
export { TONE_NAMES } from './tones.ts';
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 렌더 실행기 — HTML 을 만들고, 자가감사 결과를 읽고, PNG 를 뽑는다.
|
|
3
|
+
*
|
|
4
|
+
* 크롬을 두 번 부른다.
|
|
5
|
+
* ① `--dump-dom` : 스크립트가 계산한 캔버스 크기와 자가감사 결과를 받는다.
|
|
6
|
+
* ② `--screenshot`: ①에서 받은 크기로 정확히 찍는다.
|
|
7
|
+
* 한 번에 하려면 창 크기를 미리 알아야 하는데, 그 크기는 글자 실측 뒤에야 정해진다.
|
|
8
|
+
* 즉 이 왕복이 "글자 폭을 추정하지 않는다"는 규칙의 대가다. 한 번에 1초 남짓이다.
|
|
9
|
+
*
|
|
10
|
+
* ★ 이 모듈은 토큰을 만지지 않는다. 파일을 쓰는 건 렌더 산출물뿐이다.
|
|
11
|
+
* (safety.test.ts 의 A5 가 '파일을 쓰는 모듈은 토큰 경로를 참조하지 않는다'로
|
|
12
|
+
* 이 분리를 강제한다.)
|
|
13
|
+
*/
|
|
14
|
+
import { mkdtemp, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { pathToFileURL } from 'node:url';
|
|
18
|
+
import { dumpDom, screenshot } from "./chrome.js";
|
|
19
|
+
import { MAX_AREA, MAX_DIM, buildDiagramHtml, parseAudit, } from "./page.js";
|
|
20
|
+
import { buildCoverHtml } from "./cover.js";
|
|
21
|
+
/**
|
|
22
|
+
* 렌더는 한 번에 하나만 돈다.
|
|
23
|
+
*
|
|
24
|
+
* ★ 실측: 렌더 1회가 크롬 프로세스 9개로 최대 약 1GB 를 쓴다. 동시에 2회를 돌리면
|
|
25
|
+
* 17개·약 1.9GB 로 **선형으로 늘어난다.** MCP 클라이언트는 도구를 병렬로 부르므로
|
|
26
|
+
* 모델이 그림 다섯 장을 한 번에 요청하면 크롬 45개·6GB 가 된다.
|
|
27
|
+
* 사용자 기기에서 도는 물건이 그러면 안 된다 — 다른 작업까지 같이 죽는다.
|
|
28
|
+
*
|
|
29
|
+
* 그림 한 장은 4초면 끝나므로 줄 세워도 체감 손해가 거의 없다.
|
|
30
|
+
* 얻는 것(메모리 상한이 1GB 로 고정된다)이 훨씬 크다.
|
|
31
|
+
*/
|
|
32
|
+
let renderQueue = Promise.resolve();
|
|
33
|
+
function serialize(task) {
|
|
34
|
+
// 앞 작업이 실패해도 줄은 계속 이어져야 한다.
|
|
35
|
+
const next = renderQueue.then(task, task);
|
|
36
|
+
renderQueue = next.then(() => undefined, () => undefined);
|
|
37
|
+
return next;
|
|
38
|
+
}
|
|
39
|
+
const RENDER_PREFIX = 'velog-mcp-render-';
|
|
40
|
+
const PROFILE_PREFIX = 'velog-mcp-chrome-';
|
|
41
|
+
const KEEP_MS = 24 * 60 * 60 * 1000;
|
|
42
|
+
/**
|
|
43
|
+
* 오래된 렌더 산출물을 치운다.
|
|
44
|
+
*
|
|
45
|
+
* 방금 만든 건 지우지 않는다 — 사용자가 HTML 을 열어 손보거나 PNG 를 다시 쓸 수 있어야
|
|
46
|
+
* 해서 경로를 돌려주기 때문이다. 대신 하루가 지난 건 아무도 안 본다.
|
|
47
|
+
* 지우는 대상은 **우리가 만든 이름표가 붙은 것**뿐이다. 임시폴더의 남의 파일은 건드리지 않는다.
|
|
48
|
+
*/
|
|
49
|
+
async function sweepOld() {
|
|
50
|
+
const base = tmpdir();
|
|
51
|
+
const cutoff = Date.now() - KEEP_MS;
|
|
52
|
+
const entries = await readdir(base, { withFileTypes: true }).catch(() => []);
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (!entry.isDirectory())
|
|
55
|
+
continue;
|
|
56
|
+
if (!entry.name.startsWith(RENDER_PREFIX) && !entry.name.startsWith(PROFILE_PREFIX))
|
|
57
|
+
continue;
|
|
58
|
+
const full = join(base, entry.name);
|
|
59
|
+
const info = await stat(full).catch(() => null);
|
|
60
|
+
if (info && info.mtimeMs < cutoff) {
|
|
61
|
+
await rm(full, { recursive: true, force: true }).catch(() => { });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function render(args, parse) {
|
|
66
|
+
// 정리 실패가 렌더를 막으면 안 된다 — 청소는 부수적인 일이다.
|
|
67
|
+
await sweepOld().catch(() => { });
|
|
68
|
+
// ★ 폴더 생성도 try 안에서 한다. 밖에 두면 두 번째 생성만 실패했을 때 앞서 만든
|
|
69
|
+
// 것들이 finally 에 도달하지 못하고 남는다.
|
|
70
|
+
// 프로필만 finally 에서 지운다. 렌더 결과(dir)는 남겨야 하므로 밖에 둘 이유가 없다.
|
|
71
|
+
let profileA = '';
|
|
72
|
+
let profileB = '';
|
|
73
|
+
try {
|
|
74
|
+
const dir = await mkdtemp(join(tmpdir(), RENDER_PREFIX));
|
|
75
|
+
// 크롬 두 번에 **서로 다른 프로필**을 준다. 산출물이 나오면 SIGKILL 하고 실제
|
|
76
|
+
// 종료를 기다리지 않으므로, 같은 프로필을 재사용하면 앞선 크롬이 잠금을 놓기
|
|
77
|
+
// 전에 다음 크롬이 같은 폴더를 열게 된다.
|
|
78
|
+
profileA = await mkdtemp(join(tmpdir(), PROFILE_PREFIX));
|
|
79
|
+
profileB = await mkdtemp(join(tmpdir(), PROFILE_PREFIX));
|
|
80
|
+
const htmlPath = join(dir, `${args.basename}.html`);
|
|
81
|
+
const pngPath = join(dir, `${args.basename}.png`);
|
|
82
|
+
await writeFile(htmlPath, args.html, 'utf8');
|
|
83
|
+
const fileUrl = pathToFileURL(htmlPath).href;
|
|
84
|
+
const dom = await dumpDom(fileUrl, { profileDir: profileA });
|
|
85
|
+
const audit = parse(dom);
|
|
86
|
+
// 2차 방어. 1차는 페이지 안에서 크기를 **설정하기 전에** 거른다(page.ts).
|
|
87
|
+
// 여기까지 왔다는 건 1차가 뚫렸다는 뜻이므로 그대로 두면 안 된다.
|
|
88
|
+
if (audit.w > MAX_DIM || audit.h > MAX_DIM || audit.w * audit.h > MAX_AREA) {
|
|
89
|
+
throw new Error(`그림이 너무 큽니다: ${audit.w}×${audit.h}px ` +
|
|
90
|
+
`(상한 ${MAX_DIM}px / 면적 ${(MAX_AREA / 1_000_000).toFixed(0)}백만px).`);
|
|
91
|
+
}
|
|
92
|
+
await screenshot(fileUrl, pngPath, {
|
|
93
|
+
profileDir: profileB,
|
|
94
|
+
width: audit.w,
|
|
95
|
+
height: audit.h,
|
|
96
|
+
scale: args.scale,
|
|
97
|
+
});
|
|
98
|
+
const info = await stat(pngPath).catch(() => null);
|
|
99
|
+
if (!info) {
|
|
100
|
+
throw new Error('크롬이 PNG 를 만들지 못했습니다. 화면 크기가 비정상이거나(0 이하) ' +
|
|
101
|
+
'크롬 실행이 중간에 끊겼을 수 있습니다.');
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
pngPath,
|
|
105
|
+
htmlPath,
|
|
106
|
+
width: audit.w,
|
|
107
|
+
height: audit.h,
|
|
108
|
+
scale: args.scale,
|
|
109
|
+
audit,
|
|
110
|
+
bytes: info.size,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
// 크롬 프로필은 임시 산출물이라 항상 지운다. 렌더 결과(dir)는 남긴다 —
|
|
115
|
+
// 사용자가 HTML 을 열어 손보거나 PNG 를 다시 쓸 수 있어야 한다.
|
|
116
|
+
for (const profile of [profileA, profileB]) {
|
|
117
|
+
if (profile)
|
|
118
|
+
await rm(profile, { recursive: true, force: true }).catch(() => { });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export function renderDiagram(spec, scale = 2) {
|
|
123
|
+
return serialize(() => render({ html: buildDiagramHtml(spec), basename: 'diagram', scale }, parseAudit));
|
|
124
|
+
}
|
|
125
|
+
export function renderCover(spec, scale = 2) {
|
|
126
|
+
return serialize(() => render({ html: buildCoverHtml(spec), basename: 'cover', scale }, (dom) => {
|
|
127
|
+
const match = /<title>AUDIT (\{.*?\})<\/title>/s.exec(dom);
|
|
128
|
+
if (!match?.[1])
|
|
129
|
+
throw new Error('표지 렌더 결과를 읽지 못했습니다.');
|
|
130
|
+
return JSON.parse(match[1]);
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
export { formatAudit } from "./page.js";
|
|
134
|
+
export { formatCoverAudit } from "./cover.js";
|
|
135
|
+
export { ICON_NAMES } from "./icons.js";
|
|
136
|
+
export { TONE_NAMES } from "./tones.js";
|
|
137
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/render/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 다이어그램 HTML 생성.
|
|
3
|
+
*
|
|
4
|
+
* 이 파일의 핵심은 "스타일 규칙을 사람이 기억하는 게 아니라 코드가 강제한다" 는 것이다.
|
|
5
|
+
* 손으로 그릴 때 반복해서 틀렸던 것들을 그대로 옮겨 놨다:
|
|
6
|
+
*
|
|
7
|
+
* - 글자 폭은 **추정하지 않는다.** 전부 브라우저의 getBBox 실측이다.
|
|
8
|
+
* (글자수 × 상수로 잡으면 한글/영문 섞인 라벨에서 칩이 삐져나온다.)
|
|
9
|
+
* - 캔버스 크기를 사람이 정하지 않는다. 그린 뒤 내용 bbox 로 되맞춘다.
|
|
10
|
+
* 잘림 사고가 구조적으로 안 난다.
|
|
11
|
+
* - 선이 노드를 관통하거나 노드 뒤에 숨는 것, 서로 다른 선이 겹치는 것,
|
|
12
|
+
* 노드끼리 겹치는 것을 **자가감사**가 잡아서 보고한다.
|
|
13
|
+
*
|
|
14
|
+
* ★ 페이지 안 스크립트에 대한 보안 원칙
|
|
15
|
+
* 데이터는 `<script type="application/json">` 블록으로만 들어가고, DOM 은 전부
|
|
16
|
+
* createElementNS + textContent 로 만든다. innerHTML 은 한 번도 쓰지 않는다.
|
|
17
|
+
* 따라서 라벨 문자열에 마크업이 들어와도 그냥 글자로 그려진다.
|
|
18
|
+
* 색은 입력 단계(zod)에서 `#rrggbb` 로 좁혀 두었다.
|
|
19
|
+
*/
|
|
20
|
+
import { type Plane } from './tones.ts';
|
|
21
|
+
export interface DiagramNode {
|
|
22
|
+
id?: string | undefined;
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
w?: number | undefined;
|
|
26
|
+
h?: number | undefined;
|
|
27
|
+
title: string;
|
|
28
|
+
sub?: string | undefined;
|
|
29
|
+
icon?: string | undefined;
|
|
30
|
+
icon_tone?: string | undefined;
|
|
31
|
+
tag?: string | undefined;
|
|
32
|
+
tag_tone?: string | undefined;
|
|
33
|
+
}
|
|
34
|
+
export interface DiagramGroup {
|
|
35
|
+
name: string;
|
|
36
|
+
sub?: string | undefined;
|
|
37
|
+
tone?: string | undefined;
|
|
38
|
+
x?: number | undefined;
|
|
39
|
+
y?: number | undefined;
|
|
40
|
+
w?: number | undefined;
|
|
41
|
+
h?: number | undefined;
|
|
42
|
+
members?: string[] | undefined;
|
|
43
|
+
}
|
|
44
|
+
export interface DiagramEdge {
|
|
45
|
+
plane?: string | undefined;
|
|
46
|
+
from?: string | undefined;
|
|
47
|
+
to?: string | undefined;
|
|
48
|
+
points?: Array<[number, number]> | undefined;
|
|
49
|
+
label?: string | undefined;
|
|
50
|
+
label_at?: [number, number] | undefined;
|
|
51
|
+
label_anchor?: 'start' | 'middle' | 'end' | undefined;
|
|
52
|
+
}
|
|
53
|
+
export interface DiagramSpec {
|
|
54
|
+
title: string;
|
|
55
|
+
subtitle?: string | undefined;
|
|
56
|
+
nodes: DiagramNode[];
|
|
57
|
+
groups?: DiagramGroup[] | undefined;
|
|
58
|
+
edges?: DiagramEdge[] | undefined;
|
|
59
|
+
planes?: Plane[] | undefined;
|
|
60
|
+
legend?: boolean | undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 캔버스 상한. **페이지 안에서** 강제된다 (page 스크립트 참고).
|
|
64
|
+
* 바깥에서 검사하면 이미 브라우저가 표면을 잡은 뒤라 늦는다.
|
|
65
|
+
*/
|
|
66
|
+
export declare const MAX_DIM = 6000;
|
|
67
|
+
export declare const MAX_AREA = 9000000;
|
|
68
|
+
export interface AuditReport {
|
|
69
|
+
w: number;
|
|
70
|
+
h: number;
|
|
71
|
+
/** 노드 밖으로 삐져나온 글자 */
|
|
72
|
+
over: string[];
|
|
73
|
+
/** 자간을 눌러 억지로 맞춘 글자 — 0건이어야 한다 */
|
|
74
|
+
compressed: string[];
|
|
75
|
+
/** 노드를 관통하거나 노드 뒤로 숨은 선 */
|
|
76
|
+
cross: string[];
|
|
77
|
+
/** 서로 다른 선이 같은 자리에 겹친 구간 */
|
|
78
|
+
overlap: string[];
|
|
79
|
+
/** 노드끼리 겹침 · 배지가 아이콘 침범 */
|
|
80
|
+
collide: string[];
|
|
81
|
+
/** 라벨이 카드 위나 다른 라벨 위에 얹힘 */
|
|
82
|
+
label: string[];
|
|
83
|
+
}
|
|
84
|
+
export declare function buildDiagramHtml(spec: DiagramSpec): string;
|
|
85
|
+
/** `--dump-dom` 출력에서 자가감사 결과만 뽑는다. */
|
|
86
|
+
export declare function parseAudit(dom: string): AuditReport;
|
|
87
|
+
/** 사람이 읽는 감사 요약. 문제가 없으면 짧게 끝난다. */
|
|
88
|
+
export declare function formatAudit(a: AuditReport): string;
|
|
89
|
+
export declare const TONE_LIST: string;
|