@kimdayoun/hwpx-mcp 0.3.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.
@@ -0,0 +1,348 @@
1
+ "use strict";
2
+ /**
3
+ * Hanging Indent Calculator (내어쓰기 자동 계산기)
4
+ *
5
+ * 마커 기반 룩업 테이블 + 폰트 크기 스케일링으로
6
+ * 80% 이상의 정확성을 목표로 함
7
+ *
8
+ * 개선 v2:
9
+ * - 앞 공백 포함 계산
10
+ * - 한글 폰트 보정 계수 적용
11
+ * - 공백 너비 조정
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.HangingIndentCalculator = void 0;
15
+ /**
16
+ * 문자 너비 테이블 (em 단위, 기준 폰트 기준)
17
+ * 실제 한글 문서에서 자주 사용되는 마커 문자들의 상대적 너비
18
+ *
19
+ * 한글 폰트(함초롬바탕, 맑은 고딕 등)에서 측정된 값 기준
20
+ */
21
+ const CHAR_WIDTH_TABLE = {
22
+ // 불릿 문자 (전각 = 1em)
23
+ '○': 1.0,
24
+ '●': 1.0,
25
+ '•': 0.6, // 중간 불릿
26
+ '▪': 0.6,
27
+ '◆': 1.0,
28
+ '◇': 1.0,
29
+ '■': 1.0,
30
+ '□': 1.0,
31
+ '※': 1.0,
32
+ '★': 1.0,
33
+ '☆': 1.0,
34
+ '◎': 1.0, // 이중 원
35
+ '◉': 1.0,
36
+ '▶': 1.0, // 화살표
37
+ '▷': 1.0,
38
+ '►': 1.0,
39
+ '▻': 0.6,
40
+ '▸': 0.6, // 작은 화살표
41
+ '▹': 0.6,
42
+ '➢': 1.0,
43
+ '➣': 1.0,
44
+ '➤': 1.0,
45
+ '✓': 0.7, // 체크마크
46
+ '✔': 0.7,
47
+ '✗': 0.7,
48
+ '✘': 0.7,
49
+ '✦': 0.7, // 별
50
+ '✧': 0.7,
51
+ '→': 1.0, // 화살표
52
+ '⇒': 1.0,
53
+ '▣': 1.0, // 박스
54
+ '▤': 1.0,
55
+ '▥': 1.0,
56
+ // 대시/하이픈
57
+ '-': 0.5,
58
+ '–': 0.7, // en-dash
59
+ '—': 1.0, // em-dash
60
+ // 숫자 (반각이지만 한글 폰트에서는 조금 넓음)
61
+ '0': 0.6,
62
+ '1': 0.6,
63
+ '2': 0.6,
64
+ '3': 0.6,
65
+ '4': 0.6,
66
+ '5': 0.6,
67
+ '6': 0.6,
68
+ '7': 0.6,
69
+ '8': 0.6,
70
+ '9': 0.6,
71
+ // 구두점 (한글 폰트에서 조금 넓음)
72
+ '.': 0.35,
73
+ ')': 0.4,
74
+ '(': 0.4,
75
+ // 공백 (한글 폰트에서 더 넓음)
76
+ ' ': 0.5,
77
+ // 한글 자모/글자 (전각)
78
+ '가': 1.0,
79
+ '나': 1.0,
80
+ '다': 1.0,
81
+ '라': 1.0,
82
+ '마': 1.0,
83
+ '바': 1.0,
84
+ '사': 1.0,
85
+ '아': 1.0,
86
+ '자': 1.0,
87
+ '차': 1.0,
88
+ '카': 1.0,
89
+ '타': 1.0,
90
+ '파': 1.0,
91
+ '하': 1.0,
92
+ // 원문자 (전각)
93
+ '①': 1.0,
94
+ '②': 1.0,
95
+ '③': 1.0,
96
+ '④': 1.0,
97
+ '⑤': 1.0,
98
+ '⑥': 1.0,
99
+ '⑦': 1.0,
100
+ '⑧': 1.0,
101
+ '⑨': 1.0,
102
+ '⑩': 1.0,
103
+ '⑪': 1.0,
104
+ '⑫': 1.0,
105
+ '⑬': 1.0,
106
+ '⑭': 1.0,
107
+ '⑮': 1.0,
108
+ '⑯': 1.0,
109
+ '⑰': 1.0,
110
+ '⑱': 1.0,
111
+ '⑲': 1.0,
112
+ '⑳': 1.0,
113
+ // 로마 숫자/알파벳 대문자 (반각이지만 한글 폰트에서 조금 넓음)
114
+ 'I': 0.4,
115
+ 'V': 0.7,
116
+ 'X': 0.7,
117
+ 'L': 0.6,
118
+ 'C': 0.7,
119
+ 'D': 0.7,
120
+ 'M': 0.9,
121
+ 'A': 0.7,
122
+ 'B': 0.7,
123
+ 'E': 0.6,
124
+ 'F': 0.6,
125
+ 'G': 0.7,
126
+ 'H': 0.7,
127
+ // 알파벳 소문자 (반각)
128
+ 'a': 0.55,
129
+ 'b': 0.55,
130
+ 'c': 0.55,
131
+ 'd': 0.55,
132
+ 'e': 0.55,
133
+ 'f': 0.35,
134
+ 'g': 0.55,
135
+ 'h': 0.55,
136
+ // 콜론 (마커 뒤에 올 수 있음)
137
+ ':': 0.35,
138
+ // 법률/공문서 한글 문자
139
+ '제': 1.0,
140
+ '조': 1.0,
141
+ '항': 1.0,
142
+ '호': 1.0,
143
+ '목': 1.0,
144
+ '의': 1.0,
145
+ };
146
+ /**
147
+ * @deprecated Use getFontFactor(fontName) instead
148
+ * 기본 한글 폰트 보정 계수 (하위 호환성 유지)
149
+ */
150
+ const HANGUL_FONT_FACTOR = 1.3;
151
+ /**
152
+ * 폰트별 보정 계수 테이블
153
+ * 실측 기반 값 (한글에서 실제 렌더링 너비 / em 값)
154
+ */
155
+ const FONT_FACTOR_TABLE = {
156
+ // 기본값
157
+ 'default': 1.3,
158
+ // 한컴 폰트
159
+ '함초롬바탕': 1.25,
160
+ '함초롬돋움': 1.25,
161
+ '한컴바탕': 1.3,
162
+ '한컴돋움': 1.3,
163
+ // 마이크로소프트 폰트
164
+ '맑은 고딕': 1.35,
165
+ '맑은고딕': 1.35,
166
+ 'Malgun Gothic': 1.35,
167
+ '바탕': 1.3,
168
+ '돋움': 1.3,
169
+ '굴림': 1.3,
170
+ '궁서': 1.35,
171
+ // 나눔 폰트
172
+ '나눔고딕': 1.3,
173
+ '나눔명조': 1.3,
174
+ 'NanumGothic': 1.3,
175
+ 'NanumMyeongjo': 1.3,
176
+ '나눔바른고딕': 1.28,
177
+ // Adobe 폰트
178
+ '본고딕': 1.25,
179
+ '본명조': 1.25,
180
+ 'Noto Sans KR': 1.25,
181
+ 'Noto Serif KR': 1.25,
182
+ // 영문 폰트 (한글이 없는 경우)
183
+ 'Arial': 1.0,
184
+ 'Times New Roman': 1.0,
185
+ };
186
+ /**
187
+ * 폰트 보정 계수 가져오기
188
+ */
189
+ function getFontFactor(fontName) {
190
+ if (!fontName)
191
+ return FONT_FACTOR_TABLE['default'];
192
+ // 정확한 매칭 시도
193
+ if (FONT_FACTOR_TABLE[fontName]) {
194
+ return FONT_FACTOR_TABLE[fontName];
195
+ }
196
+ // 부분 매칭 시도 (공백/대소문자 무시)
197
+ const normalizedName = fontName.toLowerCase().replace(/\s+/g, '');
198
+ for (const [key, value] of Object.entries(FONT_FACTOR_TABLE)) {
199
+ if (key.toLowerCase().replace(/\s+/g, '') === normalizedName) {
200
+ return value;
201
+ }
202
+ }
203
+ return FONT_FACTOR_TABLE['default'];
204
+ }
205
+ /**
206
+ * 마커 패턴 정의 (순서 중요 - 더 구체적인 패턴이 먼저)
207
+ * 앞 공백도 허용하도록 수정
208
+ */
209
+ const MARKER_PATTERNS = [
210
+ // 법률/공문서 마커 (더 구체적인 것이 먼저)
211
+ // 제1조의2, 제1항의3 등
212
+ { regex: /^(\s*)(제\d+[조항호목]의\d+)\s/, type: 'article' },
213
+ // 제1조, 제2항, 제3호, 제4목 등
214
+ { regex: /^(\s*)(제\d+[조항호목])\s/, type: 'article' },
215
+ // 1호, 2목 등 (숫자 + 호/목)
216
+ { regex: /^(\s*)(\d+[호목])\s/, type: 'article' },
217
+ // 괄호 한글: (가), (나), ... (앞 공백 허용)
218
+ { regex: /^(\s*)\(([가-힣])\)\s/, type: 'parenthesized_korean' },
219
+ // 괄호 숫자: (1), (2), ... (앞 공백 허용)
220
+ { regex: /^(\s*)\((\d+)\)\s/, type: 'parenthesized' },
221
+ // 원문자: ①, ②, ... (앞 공백 허용)
222
+ { regex: /^(\s*)([①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳])\s/, type: 'circled' },
223
+ // 로마 숫자: I., II., III., IV., ... (앞 공백 허용)
224
+ { regex: /^(\s*)([IVXLCDM]+)\.\s/, type: 'roman' },
225
+ // 알파벳 대문자 + 점: A., B., ... (앞 공백 허용)
226
+ { regex: /^(\s*)([A-Z])\.\s/, type: 'alpha' },
227
+ // 알파벳 소문자 + 괄호: a), b), ... (앞 공백 허용)
228
+ { regex: /^(\s*)([a-z])\)\s/, type: 'alpha' },
229
+ // 한글 + 점: 가., 나., ... (앞 공백 허용)
230
+ { regex: /^(\s*)([가나다라마바사아자차카타파하])\.\s/, type: 'korean' },
231
+ // 숫자 + 점: 1., 2., 10., 99., ... (앞 공백 허용)
232
+ { regex: /^(\s*)(\d+)\.\s/, type: 'number' },
233
+ // 불릿 문자들 (앞 공백 허용)
234
+ // 기본: ○◦●•▪◆◇■□※★☆
235
+ // 화살표: ▶▷►▻▸▹➢➣➤→⇒
236
+ // 체크/별: ✓✔✗✘✦✧
237
+ // 박스: ▣▤▥
238
+ // 이중원: ◎◉
239
+ // 대시: -–—
240
+ { regex: /^(\s*)([○◦●•▪◆◇■□※★☆◎◉▶▷►▻▸▹➢➣➤✓✔✗✘✦✧→⇒▣▤▥\-–—])\s/, type: 'bullet' },
241
+ ];
242
+ class HangingIndentCalculator {
243
+ /**
244
+ * 문자의 너비를 em 단위로 반환
245
+ */
246
+ getCharWidth(char) {
247
+ if (CHAR_WIDTH_TABLE[char] !== undefined) {
248
+ return CHAR_WIDTH_TABLE[char];
249
+ }
250
+ // 한글 범위 (가-힣) 체크 - 전각으로 처리
251
+ if (/[가-힣]/.test(char)) {
252
+ return 1.0;
253
+ }
254
+ // 전각 문자 범위 체크
255
+ const code = char.charCodeAt(0);
256
+ if (code >= 0xFF00 && code <= 0xFFEF) {
257
+ return 1.0; // 전각 문자
258
+ }
259
+ // 기본값: 반각 문자로 가정
260
+ return 0.55;
261
+ }
262
+ /**
263
+ * 마커 문자열의 너비를 em 단위로 계산
264
+ */
265
+ calculateMarkerWidthInEm(marker) {
266
+ let totalWidth = 0;
267
+ for (const char of marker) {
268
+ totalWidth += this.getCharWidth(char);
269
+ }
270
+ return totalWidth;
271
+ }
272
+ /**
273
+ * 마커 너비 계산 (points 단위)
274
+ *
275
+ * @param marker 마커 문자열 (예: "○ ", "1. ")
276
+ * @param fontSize 폰트 크기 (pt)
277
+ * @param fontName 폰트 이름 (선택적, 기본값 사용시 생략)
278
+ * @returns 마커의 너비 (pt)
279
+ */
280
+ calculateMarkerWidth(marker, fontSize, fontName) {
281
+ const widthInEm = this.calculateMarkerWidthInEm(marker);
282
+ const fontFactor = getFontFactor(fontName);
283
+ return widthInEm * fontSize * fontFactor;
284
+ }
285
+ /**
286
+ * 텍스트에서 마커 감지 (앞 공백 포함)
287
+ *
288
+ * @param text 텍스트
289
+ * @returns 마커 정보 또는 null
290
+ */
291
+ detectMarker(text) {
292
+ if (!text || text.length === 0) {
293
+ return null;
294
+ }
295
+ for (const pattern of MARKER_PATTERNS) {
296
+ const match = text.match(pattern.regex);
297
+ if (match) {
298
+ const leadingSpaces = match[1]?.length || 0;
299
+ return {
300
+ marker: match[0], // 전체 매치 (앞 공백 + 마커 + 뒤 공백)
301
+ type: pattern.type,
302
+ leadingSpaces,
303
+ };
304
+ }
305
+ }
306
+ return null;
307
+ }
308
+ /**
309
+ * 텍스트에서 내어쓰기 값 자동 계산 (points 단위)
310
+ *
311
+ * @param text 텍스트
312
+ * @param fontSize 폰트 크기 (pt, 기본값 12pt)
313
+ * @param fontName 폰트 이름 (선택적, 기본값 사용시 생략)
314
+ * @returns 내어쓰기 값 (pt)
315
+ */
316
+ calculateHangingIndent(text, fontSize, fontName) {
317
+ const size = fontSize ?? HangingIndentCalculator.DEFAULT_FONT_SIZE;
318
+ const markerInfo = this.detectMarker(text);
319
+ if (!markerInfo) {
320
+ return 0;
321
+ }
322
+ return this.calculateMarkerWidth(markerInfo.marker, size, fontName);
323
+ }
324
+ /**
325
+ * points를 HWPUNIT으로 변환
326
+ *
327
+ * @param points 포인트 값
328
+ * @returns HWPUNIT 값 (points × 100)
329
+ */
330
+ toHwpUnit(points) {
331
+ return Math.round(points * 100);
332
+ }
333
+ /**
334
+ * 텍스트에서 내어쓰기 값 자동 계산 (HWPUNIT 단위)
335
+ *
336
+ * @param text 텍스트
337
+ * @param fontSize 폰트 크기 (pt, 기본값 12pt)
338
+ * @param fontName 폰트 이름 (선택적, 기본값 사용시 생략)
339
+ * @returns 내어쓰기 값 (HWPUNIT)
340
+ */
341
+ calculateHangingIndentInHwpUnit(text, fontSize, fontName) {
342
+ const points = this.calculateHangingIndent(text, fontSize, fontName);
343
+ return this.toHwpUnit(points);
344
+ }
345
+ }
346
+ exports.HangingIndentCalculator = HangingIndentCalculator;
347
+ // 기본 폰트 크기 (pt) - 한글 문서 기본값은 보통 10pt 또는 12pt
348
+ HangingIndentCalculator.DEFAULT_FONT_SIZE = 12;