@sayue_ltr/fleq 1.49.2
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/CHANGELOG.md +898 -0
- package/LICENSE +21 -0
- package/README.md +535 -0
- package/assets/icons/.gitkeep +0 -0
- package/assets/sounds/.gitkeep +0 -0
- package/assets/sounds/cancel.mp3 +0 -0
- package/assets/sounds/critical.mp3 +0 -0
- package/assets/sounds/info.mp3 +0 -0
- package/assets/sounds/normal.mp3 +0 -0
- package/assets/sounds/warning.mp3 +0 -0
- package/dist/config.js +638 -0
- package/dist/dmdata/connection-manager.js +2 -0
- package/dist/dmdata/endpoint-selector.js +185 -0
- package/dist/dmdata/multi-connection-manager.js +158 -0
- package/dist/dmdata/rest-client.js +281 -0
- package/dist/dmdata/telegram-parser.js +704 -0
- package/dist/dmdata/volcano-parser.js +647 -0
- package/dist/dmdata/ws-client.js +336 -0
- package/dist/engine/cli/cli-init.js +266 -0
- package/dist/engine/cli/cli-run.js +121 -0
- package/dist/engine/cli/cli.js +121 -0
- package/dist/engine/eew/eew-logger.js +355 -0
- package/dist/engine/eew/eew-tracker.js +229 -0
- package/dist/engine/messages/message-router.js +261 -0
- package/dist/engine/messages/tsunami-state.js +96 -0
- package/dist/engine/messages/volcano-state.js +131 -0
- package/dist/engine/messages/volcano-vfvo53-aggregator.js +173 -0
- package/dist/engine/monitor/monitor.js +118 -0
- package/dist/engine/monitor/repl-coordinator.js +63 -0
- package/dist/engine/monitor/shutdown.js +114 -0
- package/dist/engine/notification/node-notifier-loader.js +19 -0
- package/dist/engine/notification/notifier.js +338 -0
- package/dist/engine/notification/sound-player.js +230 -0
- package/dist/engine/notification/volcano-presentation.js +166 -0
- package/dist/engine/startup/config-resolver.js +139 -0
- package/dist/engine/startup/tsunami-initializer.js +91 -0
- package/dist/engine/startup/update-checker.js +229 -0
- package/dist/engine/startup/volcano-initializer.js +89 -0
- package/dist/index.js +24 -0
- package/dist/logger.js +95 -0
- package/dist/types.js +61 -0
- package/dist/ui/earthquake-formatter.js +871 -0
- package/dist/ui/eew-formatter.js +335 -0
- package/dist/ui/formatter.js +689 -0
- package/dist/ui/repl.js +2059 -0
- package/dist/ui/test-samples.js +880 -0
- package/dist/ui/theme.js +516 -0
- package/dist/ui/volcano-formatter.js +667 -0
- package/dist/ui/waiting-tips.js +227 -0
- package/dist/utils/intensity.js +13 -0
- package/dist/utils/secrets.js +14 -0
- package/package.json +69 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.SEVERITY_LABELS = exports.FRAME_CHARS = void 0;
|
|
40
|
+
exports.setFrameWidth = setFrameWidth;
|
|
41
|
+
exports.clearFrameWidth = clearFrameWidth;
|
|
42
|
+
exports.setInfoFullText = setInfoFullText;
|
|
43
|
+
exports.getInfoFullText = getInfoFullText;
|
|
44
|
+
exports.setDisplayMode = setDisplayMode;
|
|
45
|
+
exports.getDisplayMode = getDisplayMode;
|
|
46
|
+
exports.setMaxObservations = setMaxObservations;
|
|
47
|
+
exports.getMaxObservations = getMaxObservations;
|
|
48
|
+
exports.setTruncation = setTruncation;
|
|
49
|
+
exports.getTruncation = getTruncation;
|
|
50
|
+
exports.createRenderBuffer = createRenderBuffer;
|
|
51
|
+
exports.flushWithRecap = flushWithRecap;
|
|
52
|
+
exports.frameColor = frameColor;
|
|
53
|
+
exports.getFrameWidth = getFrameWidth;
|
|
54
|
+
exports.frameTop = frameTop;
|
|
55
|
+
exports.frameLine = frameLine;
|
|
56
|
+
exports.frameDivider = frameDivider;
|
|
57
|
+
exports.frameBottom = frameBottom;
|
|
58
|
+
exports.stripAnsi = stripAnsi;
|
|
59
|
+
exports.visualWidth = visualWidth;
|
|
60
|
+
exports.visualPadEnd = visualPadEnd;
|
|
61
|
+
exports.renderFrameTable = renderFrameTable;
|
|
62
|
+
exports.wrapFrameLines = wrapFrameLines;
|
|
63
|
+
exports.wrapTextLines = wrapTextLines;
|
|
64
|
+
exports.collectHighlightSpans = collectHighlightSpans;
|
|
65
|
+
exports.highlightAndWrap = highlightAndWrap;
|
|
66
|
+
exports.formatTimestamp = formatTimestamp;
|
|
67
|
+
exports.formatElapsedTime = formatElapsedTime;
|
|
68
|
+
exports.intensityColor = intensityColor;
|
|
69
|
+
exports.lgIntensityColor = lgIntensityColor;
|
|
70
|
+
exports.lgIntToNumeric = lgIntToNumeric;
|
|
71
|
+
exports.intensityToNumeric = intensityToNumeric;
|
|
72
|
+
exports.colorMagnitude = colorMagnitude;
|
|
73
|
+
exports.renderFooter = renderFooter;
|
|
74
|
+
exports.displayRawHeader = displayRawHeader;
|
|
75
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
76
|
+
const types_1 = require("../types");
|
|
77
|
+
const theme = __importStar(require("./theme"));
|
|
78
|
+
// ── フレーム幅キャッシュ ──
|
|
79
|
+
/** 現在のフレーム幅 (setFrameWidth で更新、getFrameWidth で参照) */
|
|
80
|
+
let cachedFrameWidth = null;
|
|
81
|
+
/** フレーム幅を外部から設定する (config 変更時に呼ぶ) */
|
|
82
|
+
function setFrameWidth(width) {
|
|
83
|
+
cachedFrameWidth = width;
|
|
84
|
+
}
|
|
85
|
+
/** フレーム幅を自動モード (ターミナル幅追従) に戻す */
|
|
86
|
+
function clearFrameWidth() {
|
|
87
|
+
cachedFrameWidth = null;
|
|
88
|
+
}
|
|
89
|
+
/** infoFullText キャッシュ */
|
|
90
|
+
let cachedInfoFullText = false;
|
|
91
|
+
/** infoFullText を外部から設定する */
|
|
92
|
+
function setInfoFullText(value) {
|
|
93
|
+
cachedInfoFullText = value;
|
|
94
|
+
}
|
|
95
|
+
/** infoFullText の現在値を返す */
|
|
96
|
+
function getInfoFullText() {
|
|
97
|
+
return cachedInfoFullText;
|
|
98
|
+
}
|
|
99
|
+
// ── 表示モードキャッシュ ──
|
|
100
|
+
/** 現在の表示モード */
|
|
101
|
+
let cachedDisplayMode = "normal";
|
|
102
|
+
/** 表示モードを外部から設定する */
|
|
103
|
+
function setDisplayMode(mode) {
|
|
104
|
+
cachedDisplayMode = mode;
|
|
105
|
+
}
|
|
106
|
+
/** 表示モードの現在値を返す */
|
|
107
|
+
function getDisplayMode() {
|
|
108
|
+
return cachedDisplayMode;
|
|
109
|
+
}
|
|
110
|
+
// ── 観測点折りたたみキャッシュ ──
|
|
111
|
+
/** 現在の観測点最大表示件数 */
|
|
112
|
+
let cachedMaxObservations = null;
|
|
113
|
+
/** 観測点最大表示件数を外部から設定する */
|
|
114
|
+
function setMaxObservations(value) {
|
|
115
|
+
cachedMaxObservations = value;
|
|
116
|
+
}
|
|
117
|
+
/** 観測点最大表示件数の現在値を返す */
|
|
118
|
+
function getMaxObservations() {
|
|
119
|
+
return cachedMaxObservations;
|
|
120
|
+
}
|
|
121
|
+
// ── 省略上限キャッシュ ──
|
|
122
|
+
/** 現在の省略上限設定 */
|
|
123
|
+
let cachedTruncation = { ...types_1.DEFAULT_CONFIG.truncation };
|
|
124
|
+
/** 省略上限設定を外部から設定する */
|
|
125
|
+
function setTruncation(value) {
|
|
126
|
+
cachedTruncation = value;
|
|
127
|
+
}
|
|
128
|
+
/** 省略上限設定の現在値を返す */
|
|
129
|
+
function getTruncation() {
|
|
130
|
+
return cachedTruncation;
|
|
131
|
+
}
|
|
132
|
+
function createRenderBuffer() {
|
|
133
|
+
const _lines = [];
|
|
134
|
+
let _titleLine = null;
|
|
135
|
+
let _cardLine = null;
|
|
136
|
+
const _headlineLines = [];
|
|
137
|
+
return {
|
|
138
|
+
push(line) {
|
|
139
|
+
_lines.push({ text: line, kind: "normal" });
|
|
140
|
+
},
|
|
141
|
+
pushEmpty() {
|
|
142
|
+
_lines.push({ text: "", kind: "normal" });
|
|
143
|
+
},
|
|
144
|
+
pushTitle(line) {
|
|
145
|
+
_lines.push({ text: line, kind: "title" });
|
|
146
|
+
if (_titleLine == null)
|
|
147
|
+
_titleLine = line;
|
|
148
|
+
},
|
|
149
|
+
pushCard(line) {
|
|
150
|
+
_lines.push({ text: line, kind: "card" });
|
|
151
|
+
if (_cardLine == null)
|
|
152
|
+
_cardLine = line;
|
|
153
|
+
},
|
|
154
|
+
pushHeadline(line) {
|
|
155
|
+
_lines.push({ text: line, kind: "headline" });
|
|
156
|
+
_headlineLines.push(line);
|
|
157
|
+
},
|
|
158
|
+
get lineCount() { return _lines.length; },
|
|
159
|
+
get lines() { return _lines; },
|
|
160
|
+
get titleLine() { return _titleLine; },
|
|
161
|
+
get cardLine() { return _cardLine; },
|
|
162
|
+
get headlineLines() { return _headlineLines; },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** recap 予約行数 (フレーム下部 + 空行 + プロンプト行) */
|
|
166
|
+
const RECAP_RESERVE_ROWS = 3;
|
|
167
|
+
/**
|
|
168
|
+
* バッファの内容を出力し、ターミナル高さを超える場合はフレーム下部直前に
|
|
169
|
+
* サマリー (recap) を再掲する。
|
|
170
|
+
*/
|
|
171
|
+
function flushWithRecap(buf, level, width) {
|
|
172
|
+
const isTTY = process.stdout.isTTY;
|
|
173
|
+
const rows = process.stdout.rows;
|
|
174
|
+
// recap 判定: TTY かつ行数がターミナル高さを超える場合
|
|
175
|
+
const needRecap = isTTY === true && typeof rows === "number" && rows > 0
|
|
176
|
+
&& buf.lineCount > rows - RECAP_RESERVE_ROWS;
|
|
177
|
+
if (!needRecap) {
|
|
178
|
+
// そのまま出力
|
|
179
|
+
for (const line of buf.lines) {
|
|
180
|
+
if (line.text === "") {
|
|
181
|
+
console.log();
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.log(line.text);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// recap あり: 末尾2行 (frameBottom + 空行) の直前に挿入
|
|
190
|
+
const allLines = [...buf.lines];
|
|
191
|
+
// 末尾の空行と frameBottom を分離
|
|
192
|
+
const tail = [];
|
|
193
|
+
while (allLines.length > 0) {
|
|
194
|
+
const last = allLines[allLines.length - 1];
|
|
195
|
+
if (last.text === "" || last.kind === "normal") {
|
|
196
|
+
tail.unshift(allLines.pop());
|
|
197
|
+
if (tail.length >= 2)
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// 本体出力
|
|
205
|
+
for (const line of allLines) {
|
|
206
|
+
if (line.text === "") {
|
|
207
|
+
console.log();
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
console.log(line.text);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// recap セクション — 要約データがある場合のみ表示
|
|
214
|
+
const hasRecapData = buf.titleLine != null || buf.cardLine != null || buf.headlineLines.length > 0;
|
|
215
|
+
if (hasRecapData) {
|
|
216
|
+
console.log(frameDivider(level, width));
|
|
217
|
+
console.log(frameLine(level, chalk_1.default.gray("▼ サマリー"), width));
|
|
218
|
+
if (buf.titleLine != null) {
|
|
219
|
+
console.log(buf.titleLine);
|
|
220
|
+
}
|
|
221
|
+
if (buf.cardLine != null) {
|
|
222
|
+
console.log(buf.cardLine);
|
|
223
|
+
}
|
|
224
|
+
// headline は1行目のみ
|
|
225
|
+
if (buf.headlineLines.length > 0) {
|
|
226
|
+
console.log(buf.headlineLines[0]);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// tail 出力 (frameBottom + 空行)
|
|
230
|
+
for (const line of tail) {
|
|
231
|
+
if (line.text === "") {
|
|
232
|
+
console.log();
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
console.log(line.text);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
exports.FRAME_CHARS = {
|
|
240
|
+
critical: { tl: "╔", tr: "╗", bl: "╚", br: "╝", h: "═", v: "║", divL: "╠", divR: "╣" },
|
|
241
|
+
warning: { tl: "╔", tr: "╗", bl: "╚", br: "╝", h: "═", v: "║", divL: "╠", divR: "╣" },
|
|
242
|
+
normal: { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", divL: "├", divR: "┤" },
|
|
243
|
+
info: { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", divL: "├", divR: "┤" },
|
|
244
|
+
cancel: { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", divL: "├", divR: "┤" },
|
|
245
|
+
};
|
|
246
|
+
/** フレームレベル → ロール名マッピング */
|
|
247
|
+
const FRAME_ROLE_MAP = {
|
|
248
|
+
critical: "frameCritical",
|
|
249
|
+
warning: "frameWarning",
|
|
250
|
+
normal: "frameNormal",
|
|
251
|
+
info: "frameInfo",
|
|
252
|
+
cancel: "frameCancel",
|
|
253
|
+
};
|
|
254
|
+
/** フレームレベルに対応する色を返す (呼び出し時点の chalk.level を反映) */
|
|
255
|
+
function frameColor(level) {
|
|
256
|
+
return theme.getRoleChalk(FRAME_ROLE_MAP[level]);
|
|
257
|
+
}
|
|
258
|
+
/** FRAMES 互換のアクセサ — 罫線文字 + 色 */
|
|
259
|
+
function getFrame(level) {
|
|
260
|
+
return { ...exports.FRAME_CHARS[level], color: frameColor(level) };
|
|
261
|
+
}
|
|
262
|
+
/** フレームレベルのテキストラベル (アクセシビリティ: 色が見えない環境向け) */
|
|
263
|
+
exports.SEVERITY_LABELS = {
|
|
264
|
+
critical: "[緊急]",
|
|
265
|
+
warning: "[警告]",
|
|
266
|
+
normal: "[情報]",
|
|
267
|
+
info: "[通知]",
|
|
268
|
+
cancel: "[取消]",
|
|
269
|
+
};
|
|
270
|
+
const FRAME_WIDTH = 60;
|
|
271
|
+
/** キャッシュ済みの tableWidth を返す。未設定ならターミナル幅に追従 (fallback: 60) */
|
|
272
|
+
function getFrameWidth() {
|
|
273
|
+
if (cachedFrameWidth != null)
|
|
274
|
+
return cachedFrameWidth;
|
|
275
|
+
const cols = process.stdout.columns;
|
|
276
|
+
if (cols == null || cols < 40)
|
|
277
|
+
return FRAME_WIDTH;
|
|
278
|
+
return Math.min(cols, 200);
|
|
279
|
+
}
|
|
280
|
+
function frameTop(level, width = FRAME_WIDTH) {
|
|
281
|
+
const f = getFrame(level);
|
|
282
|
+
return f.color(f.tl + f.h.repeat(width - 2) + f.tr);
|
|
283
|
+
}
|
|
284
|
+
function frameLine(level, content, width = FRAME_WIDTH) {
|
|
285
|
+
const f = getFrame(level);
|
|
286
|
+
// 生改行が混入すると罫線が崩れるため、空白に置換して防御
|
|
287
|
+
const safeContent = (content.includes("\n") || content.includes("\r"))
|
|
288
|
+
? content.replace(/\r?\n/g, " ")
|
|
289
|
+
: content;
|
|
290
|
+
// ANSI エスケープを除去して可視幅を計算
|
|
291
|
+
const visibleLen = visualWidth(safeContent);
|
|
292
|
+
const pad = Math.max(0, width - 4 - visibleLen);
|
|
293
|
+
return f.color(f.v) + " " + safeContent + " ".repeat(pad) + " " + f.color(f.v);
|
|
294
|
+
}
|
|
295
|
+
function frameDivider(level, width = FRAME_WIDTH) {
|
|
296
|
+
const f = getFrame(level);
|
|
297
|
+
return f.color(f.divL + f.h.repeat(width - 2) + f.divR);
|
|
298
|
+
}
|
|
299
|
+
function frameBottom(level, width = FRAME_WIDTH) {
|
|
300
|
+
const f = getFrame(level);
|
|
301
|
+
return f.color(f.bl + f.h.repeat(width - 2) + f.br);
|
|
302
|
+
}
|
|
303
|
+
/** ANSI / VT エスケープシーケンスを除去 (表示幅計算用 & インジェクション防止) */
|
|
304
|
+
function stripAnsi(str) {
|
|
305
|
+
// eslint-disable-next-line no-control-regex
|
|
306
|
+
return str.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "");
|
|
307
|
+
}
|
|
308
|
+
/** 外部由来の文字列から制御文字・ANSIエスケープを除去して安全にする */
|
|
309
|
+
function sanitizeForTerminal(str) {
|
|
310
|
+
// ANSIエスケープ除去後、残った制御文字(改行・タブ以外)を除去
|
|
311
|
+
// eslint-disable-next-line no-control-regex
|
|
312
|
+
return stripAnsi(str).replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
313
|
+
}
|
|
314
|
+
/** 文字列の視覚的な幅を計算(全角文字を2として数える) */
|
|
315
|
+
function visualWidth(str) {
|
|
316
|
+
const plain = stripAnsi(str);
|
|
317
|
+
let width = 0;
|
|
318
|
+
for (const ch of plain) {
|
|
319
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
320
|
+
// CJK統合漢字、ひらがな、カタカナ、全角記号、全角括弧等
|
|
321
|
+
if ((cp >= 0x1100 && cp <= 0x115F) || // Hangul Jamo
|
|
322
|
+
(cp >= 0x2E80 && cp <= 0x303E) || // CJK部首・記号
|
|
323
|
+
(cp >= 0x3041 && cp <= 0x33BF) || // ひらがな・カタカナ・CJK互換
|
|
324
|
+
(cp >= 0x3400 && cp <= 0x4DBF) || // CJK統合漢字拡張A
|
|
325
|
+
(cp >= 0x4E00 && cp <= 0xA4CF) || // CJK統合漢字 + Yi
|
|
326
|
+
(cp >= 0xAC00 && cp <= 0xD7AF) || // Hangul Syllables
|
|
327
|
+
(cp >= 0xF900 && cp <= 0xFAFF) || // CJK互換漢字
|
|
328
|
+
(cp >= 0xFE30 && cp <= 0xFE4F) || // CJK互換形
|
|
329
|
+
(cp >= 0xFF01 && cp <= 0xFF60) || // 全角ASCII・半角カタカナ
|
|
330
|
+
(cp >= 0xFFE0 && cp <= 0xFFE6) || // 全角記号
|
|
331
|
+
(cp >= 0x20000 && cp <= 0x2FA1F) // CJK統合漢字拡張B-F
|
|
332
|
+
) {
|
|
333
|
+
width += 2;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
width += 1;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return width;
|
|
340
|
+
}
|
|
341
|
+
/** 視覚幅を考慮してスペースでパディング(padEnd の全角対応版) */
|
|
342
|
+
function visualPadEnd(str, targetWidth) {
|
|
343
|
+
const currentWidth = visualWidth(str);
|
|
344
|
+
const padSize = Math.max(0, targetWidth - currentWidth);
|
|
345
|
+
return str + " ".repeat(padSize);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* フレーム内にカラム区切りテーブルを描画する。
|
|
349
|
+
* headers: ヘッダー文字列の配列 (スタイル適用前)
|
|
350
|
+
* rows: 各行のセル配列 (chalk でスタイル適用済み)
|
|
351
|
+
* 最後のカラムは残り幅を使い切る可変幅。
|
|
352
|
+
*/
|
|
353
|
+
function renderFrameTable(level, headers, rows, width, buf) {
|
|
354
|
+
const out = buf ? (line) => buf.push(line) : (line) => console.log(line);
|
|
355
|
+
const innerWidth = width - 4;
|
|
356
|
+
const numCols = headers.length;
|
|
357
|
+
// 各カラムの最大視覚幅を計算
|
|
358
|
+
const colWidths = headers.map((h, i) => {
|
|
359
|
+
let maxW = visualWidth(h);
|
|
360
|
+
for (const row of rows) {
|
|
361
|
+
if (row[i] != null) {
|
|
362
|
+
maxW = Math.max(maxW, visualWidth(row[i]));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return maxW;
|
|
366
|
+
});
|
|
367
|
+
// セパレータ幅 " │ " = 3 文字 × (カラム数 - 1)
|
|
368
|
+
const separatorWidth = (numCols - 1) * 3;
|
|
369
|
+
const totalContent = colWidths.reduce((a, b) => a + b, 0) + separatorWidth;
|
|
370
|
+
// 合計がフレーム内幅を超える場合、最後のカラムを縮小
|
|
371
|
+
if (totalContent > innerWidth) {
|
|
372
|
+
const shrink = totalContent - innerWidth;
|
|
373
|
+
colWidths[numCols - 1] = Math.max(4, colWidths[numCols - 1] - shrink);
|
|
374
|
+
}
|
|
375
|
+
const colSep = chalk_1.default.gray(" │ ");
|
|
376
|
+
const headerLine = headers
|
|
377
|
+
.map((h, i) => visualPadEnd(chalk_1.default.bold(h), colWidths[i]))
|
|
378
|
+
.join(colSep);
|
|
379
|
+
out(frameLine(level, headerLine, width));
|
|
380
|
+
// セパレータ行
|
|
381
|
+
const sepParts = colWidths.map((w) => "─".repeat(w));
|
|
382
|
+
out(frameLine(level, chalk_1.default.gray(sepParts.join("─┼─")), width));
|
|
383
|
+
// データ行
|
|
384
|
+
for (const row of rows) {
|
|
385
|
+
const cells = row.map((cell, i) => visualPadEnd(cell ?? "", colWidths[i]));
|
|
386
|
+
out(frameLine(level, cells.join(colSep), width));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* コンテンツがフレーム幅を超える場合に折り返して複数の frameLine を生成する。
|
|
391
|
+
* 改行文字で段落分割した上で、各段落を区切り文字ベースで折り返す。
|
|
392
|
+
* カンマ+スペース / 日本語句読点(、。) / パイプ区切りを基準に折り返す。
|
|
393
|
+
* 分割できない場合は文字単位でハード折り返しする。
|
|
394
|
+
* 2行目以降は indent 分のスペースでインデントする。
|
|
395
|
+
*/
|
|
396
|
+
function wrapFrameLines(level, content, width, indent = 0) {
|
|
397
|
+
// 改行で段落分割し、各段落を個別に折り返す
|
|
398
|
+
const paragraphs = content.replace(/\r\n?/g, "\n").split("\n");
|
|
399
|
+
if (paragraphs.length > 1) {
|
|
400
|
+
const out = [];
|
|
401
|
+
for (const p of paragraphs) {
|
|
402
|
+
if (p === "" || p.trim() === "") {
|
|
403
|
+
out.push(frameLine(level, "", width));
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
out.push(...wrapSingleLine(level, p, width, indent));
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
return wrapSingleLine(level, content, width, indent);
|
|
411
|
+
}
|
|
412
|
+
/** 単一行(改行なし)の折り返し処理 */
|
|
413
|
+
function wrapSingleLine(level, content, width, indent = 0) {
|
|
414
|
+
const innerWidth = width - 4; // フレーム内の有効幅 (左右の罫線+スペース)
|
|
415
|
+
if (visualWidth(content) <= innerWidth) {
|
|
416
|
+
return [frameLine(level, content, width)];
|
|
417
|
+
}
|
|
418
|
+
// 複数の区切りパターンで分割を試行
|
|
419
|
+
const delimiters = [", ", "、", " │ "];
|
|
420
|
+
let parts = null;
|
|
421
|
+
let joinStr = "";
|
|
422
|
+
for (const delim of delimiters) {
|
|
423
|
+
const split = content.split(delim);
|
|
424
|
+
if (split.length > 1) {
|
|
425
|
+
parts = split;
|
|
426
|
+
joinStr = delim;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (parts != null && parts.length > 1) {
|
|
431
|
+
const lines = [];
|
|
432
|
+
const indentStr = " ".repeat(indent);
|
|
433
|
+
let currentLine = parts[0];
|
|
434
|
+
for (let i = 1; i < parts.length; i++) {
|
|
435
|
+
const candidate = currentLine + joinStr + parts[i];
|
|
436
|
+
if (visualWidth(candidate) <= innerWidth) {
|
|
437
|
+
currentLine = candidate;
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
// 末尾にカンマ区切りの場合はカンマを付与
|
|
441
|
+
const suffix = joinStr === ", " ? "," : "";
|
|
442
|
+
lines.push(frameLine(level, currentLine + suffix, width));
|
|
443
|
+
currentLine = indentStr + parts[i];
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
lines.push(frameLine(level, currentLine, width));
|
|
447
|
+
return lines;
|
|
448
|
+
}
|
|
449
|
+
// 分割できない場合は文字単位でハード折り返し
|
|
450
|
+
const wrapped = wrapTextLines(stripAnsi(content), innerWidth);
|
|
451
|
+
if (wrapped.length <= 1) {
|
|
452
|
+
return [frameLine(level, content, width)];
|
|
453
|
+
}
|
|
454
|
+
return wrapped.map((line) => frameLine(level, line, width));
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* テキストを文字単位で折り返す。CJK文字は幅2として計算。
|
|
458
|
+
* フレーム装飾は含まず、折り返し後の各行を文字列配列で返す。
|
|
459
|
+
*/
|
|
460
|
+
function wrapTextLines(text, maxWidth) {
|
|
461
|
+
if (maxWidth <= 0)
|
|
462
|
+
return [text];
|
|
463
|
+
if (visualWidth(text) <= maxWidth)
|
|
464
|
+
return [text];
|
|
465
|
+
const lines = [];
|
|
466
|
+
let currentLine = "";
|
|
467
|
+
let currentWidth = 0;
|
|
468
|
+
for (const ch of text) {
|
|
469
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
470
|
+
let charWidth = 1;
|
|
471
|
+
if ((cp >= 0x1100 && cp <= 0x115F) ||
|
|
472
|
+
(cp >= 0x2E80 && cp <= 0x303E) ||
|
|
473
|
+
(cp >= 0x3041 && cp <= 0x33BF) ||
|
|
474
|
+
(cp >= 0x3400 && cp <= 0x4DBF) ||
|
|
475
|
+
(cp >= 0x4E00 && cp <= 0xA4CF) ||
|
|
476
|
+
(cp >= 0xAC00 && cp <= 0xD7AF) ||
|
|
477
|
+
(cp >= 0xF900 && cp <= 0xFAFF) ||
|
|
478
|
+
(cp >= 0xFE30 && cp <= 0xFE4F) ||
|
|
479
|
+
(cp >= 0xFF01 && cp <= 0xFF60) ||
|
|
480
|
+
(cp >= 0xFFE0 && cp <= 0xFFE6) ||
|
|
481
|
+
(cp >= 0x20000 && cp <= 0x2FA1F)) {
|
|
482
|
+
charWidth = 2;
|
|
483
|
+
}
|
|
484
|
+
if (currentWidth + charWidth > maxWidth) {
|
|
485
|
+
lines.push(currentLine);
|
|
486
|
+
currentLine = ch;
|
|
487
|
+
currentWidth = charWidth;
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
currentLine += ch;
|
|
491
|
+
currentWidth += charWidth;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (currentLine.length > 0) {
|
|
495
|
+
lines.push(currentLine);
|
|
496
|
+
}
|
|
497
|
+
return lines;
|
|
498
|
+
}
|
|
499
|
+
/** 元の行からマッチspanを収集する */
|
|
500
|
+
function collectHighlightSpans(line, rules) {
|
|
501
|
+
const spans = [];
|
|
502
|
+
for (const rule of rules) {
|
|
503
|
+
const flags = rule.flags.includes("g") ? rule.flags : rule.flags + "g";
|
|
504
|
+
const regex = new RegExp(rule.source, flags);
|
|
505
|
+
let match;
|
|
506
|
+
while ((match = regex.exec(line)) !== null) {
|
|
507
|
+
// 文字列インデックスをchar配列インデックスに変換
|
|
508
|
+
const startCharIdx = Array.from(line.slice(0, match.index)).length;
|
|
509
|
+
const matchChars = Array.from(match[0]).length;
|
|
510
|
+
const endCharIdx = startCharIdx + matchChars;
|
|
511
|
+
// 既存spanと重複しない場合のみ追加(同一開始位置では長いマッチ優先)
|
|
512
|
+
const overlapping = spans.find(s => startCharIdx < s.end && endCharIdx > s.start);
|
|
513
|
+
if (!overlapping) {
|
|
514
|
+
spans.push({ start: startCharIdx, end: endCharIdx, style: rule.style() });
|
|
515
|
+
}
|
|
516
|
+
else if (overlapping.start === startCharIdx && matchChars > (overlapping.end - overlapping.start)) {
|
|
517
|
+
// 同一開始位置で長い方が勝つ
|
|
518
|
+
overlapping.end = endCharIdx;
|
|
519
|
+
overlapping.style = rule.style();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return spans.sort((a, b) => a.start - b.start);
|
|
524
|
+
}
|
|
525
|
+
/** span付きの行を折り返し、各折り返し行にANSIを適用する */
|
|
526
|
+
function highlightAndWrap(line, rules, maxWidth) {
|
|
527
|
+
const spans = collectHighlightSpans(line, rules);
|
|
528
|
+
// spanがなければ従来通り(素通し)
|
|
529
|
+
if (spans.length === 0) {
|
|
530
|
+
return wrapTextLines(line, maxWidth);
|
|
531
|
+
}
|
|
532
|
+
// 平文で折り返し
|
|
533
|
+
const wrappedLines = wrapTextLines(line, maxWidth);
|
|
534
|
+
// 各折り返し行に対して、charオフセットを追跡しながらspanを適用
|
|
535
|
+
let charOffset = 0;
|
|
536
|
+
return wrappedLines.map((wrapped) => {
|
|
537
|
+
const chars = Array.from(wrapped);
|
|
538
|
+
const lineEnd = charOffset + chars.length;
|
|
539
|
+
// この行にかかるspanを取得
|
|
540
|
+
const relevantSpans = spans.filter(s => s.start < lineEnd && s.end > charOffset);
|
|
541
|
+
if (relevantSpans.length === 0) {
|
|
542
|
+
charOffset = lineEnd;
|
|
543
|
+
return wrapped;
|
|
544
|
+
}
|
|
545
|
+
// spanに応じて部分ごとに色付け
|
|
546
|
+
let result = "";
|
|
547
|
+
let pos = charOffset;
|
|
548
|
+
for (const span of relevantSpans) {
|
|
549
|
+
const spanStart = Math.max(span.start, charOffset);
|
|
550
|
+
const spanEnd = Math.min(span.end, lineEnd);
|
|
551
|
+
if (pos < spanStart) {
|
|
552
|
+
result += chars.slice(pos - charOffset, spanStart - charOffset).join("");
|
|
553
|
+
}
|
|
554
|
+
result += span.style(chars.slice(spanStart - charOffset, spanEnd - charOffset).join(""));
|
|
555
|
+
pos = spanEnd;
|
|
556
|
+
}
|
|
557
|
+
if (pos < lineEnd) {
|
|
558
|
+
result += chars.slice(pos - charOffset).join("");
|
|
559
|
+
}
|
|
560
|
+
charOffset = lineEnd;
|
|
561
|
+
return result;
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
// ── 時刻フォーマット ──
|
|
565
|
+
/** 絶対時刻を整形 ("YYYY-MM-DD HH:MM:SS") */
|
|
566
|
+
function formatTimestamp(isoStr) {
|
|
567
|
+
const d = new Date(isoStr);
|
|
568
|
+
if (isNaN(d.getTime()))
|
|
569
|
+
return isoStr;
|
|
570
|
+
const yyyy = d.getFullYear();
|
|
571
|
+
const MM = String(d.getMonth() + 1).padStart(2, "0");
|
|
572
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
573
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
574
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
575
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
576
|
+
return `${yyyy}-${MM}-${dd} ${hh}:${mm}:${ss}`;
|
|
577
|
+
}
|
|
578
|
+
/** 経過時間を "HH:MM:SS" 形式に整形 */
|
|
579
|
+
function formatElapsedTime(ms) {
|
|
580
|
+
const safeMs = Math.max(0, ms);
|
|
581
|
+
const totalSec = Math.floor(safeMs / 1000);
|
|
582
|
+
const hh = Math.floor(totalSec / 3600);
|
|
583
|
+
const mm = Math.floor((totalSec % 3600) / 60);
|
|
584
|
+
const ss = totalSec % 60;
|
|
585
|
+
return `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}`;
|
|
586
|
+
}
|
|
587
|
+
/** 区切り線 (後方互換用、新コードではフレーム関数を使用) */
|
|
588
|
+
function separator(char = "─", len = 60) {
|
|
589
|
+
return chalk_1.default.gray(char.repeat(len));
|
|
590
|
+
}
|
|
591
|
+
/** 震度に応じた色を返す (CUD対応) */
|
|
592
|
+
function intensityColor(intensity) {
|
|
593
|
+
const norm = intensity.replace(/\s+/g, "");
|
|
594
|
+
switch (norm) {
|
|
595
|
+
case "1":
|
|
596
|
+
return theme.getRoleChalk("intensity1");
|
|
597
|
+
case "2":
|
|
598
|
+
return theme.getRoleChalk("intensity2");
|
|
599
|
+
case "3":
|
|
600
|
+
return theme.getRoleChalk("intensity3");
|
|
601
|
+
case "4":
|
|
602
|
+
return theme.getRoleChalk("intensity4");
|
|
603
|
+
case "5-":
|
|
604
|
+
case "5弱":
|
|
605
|
+
return theme.getRoleChalk("intensity5Lower");
|
|
606
|
+
case "5+":
|
|
607
|
+
case "5強":
|
|
608
|
+
return theme.getRoleChalk("intensity5Upper");
|
|
609
|
+
case "6-":
|
|
610
|
+
case "6弱":
|
|
611
|
+
return theme.getRoleChalk("intensity6Lower");
|
|
612
|
+
case "6+":
|
|
613
|
+
case "6強":
|
|
614
|
+
return theme.getRoleChalk("intensity6Upper");
|
|
615
|
+
case "7":
|
|
616
|
+
return theme.getRoleChalk("intensity7");
|
|
617
|
+
default:
|
|
618
|
+
return chalk_1.default.white;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/** 長周期地震動階級に応じた色を返す (CUD対応) */
|
|
622
|
+
function lgIntensityColor(lgInt) {
|
|
623
|
+
switch (lgInt) {
|
|
624
|
+
case "0":
|
|
625
|
+
return theme.getRoleChalk("lgInt0");
|
|
626
|
+
case "1":
|
|
627
|
+
return theme.getRoleChalk("lgInt1");
|
|
628
|
+
case "2":
|
|
629
|
+
return theme.getRoleChalk("lgInt2");
|
|
630
|
+
case "3":
|
|
631
|
+
return theme.getRoleChalk("lgInt3");
|
|
632
|
+
case "4":
|
|
633
|
+
return theme.getRoleChalk("lgInt4");
|
|
634
|
+
default:
|
|
635
|
+
return chalk_1.default.white;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// ── 共通ヘルパー (配信区分別フォーマッタから使用) ──
|
|
639
|
+
/** 長周期地震動階級の数値変換 (フレームレベル判定用) */
|
|
640
|
+
function lgIntToNumeric(lgInt) {
|
|
641
|
+
const map = { "0": 0, "1": 1, "2": 2, "3": 3, "4": 4 };
|
|
642
|
+
return map[lgInt] ?? -1;
|
|
643
|
+
}
|
|
644
|
+
/** 震度文字列から数値優先度を返す (フレームレベル判定用) */
|
|
645
|
+
function intensityToNumeric(maxInt) {
|
|
646
|
+
const norm = maxInt.replace(/\s+/g, "");
|
|
647
|
+
const map = {
|
|
648
|
+
"1": 1, "2": 2, "3": 3, "4": 4,
|
|
649
|
+
"5-": 5, "5弱": 5, "5+": 6, "5強": 6,
|
|
650
|
+
"6-": 7, "6弱": 7, "6+": 8, "6強": 8, "7": 9,
|
|
651
|
+
};
|
|
652
|
+
return map[norm] ?? 0;
|
|
653
|
+
}
|
|
654
|
+
/** マグニチュードに色を付ける (CUD対応) */
|
|
655
|
+
function colorMagnitude(magStr) {
|
|
656
|
+
const mag = parseFloat(magStr);
|
|
657
|
+
const magColor = mag >= 7.0
|
|
658
|
+
? theme.getRoleChalk("magnitudeMax")
|
|
659
|
+
: mag >= 5.0
|
|
660
|
+
? theme.getRoleChalk("magnitudeHigh")
|
|
661
|
+
: mag >= 3.0
|
|
662
|
+
? theme.getRoleChalk("magnitudeLow")
|
|
663
|
+
: chalk_1.default.white;
|
|
664
|
+
return magColor(`M${magStr}`);
|
|
665
|
+
}
|
|
666
|
+
/** 共通フッター: type / reportDateTime / publishingOffice をテーブル最下段に表示 */
|
|
667
|
+
function renderFooter(level, type, reportDateTime, publishingOffice, width, buf) {
|
|
668
|
+
const out = buf ? (line) => buf.push(line) : (line) => console.log(line);
|
|
669
|
+
out(frameDivider(level, width));
|
|
670
|
+
out(frameLine(level, chalk_1.default.gray(`${type} ${formatTimestamp(reportDateTime)} ${publishingOffice}`), width));
|
|
671
|
+
}
|
|
672
|
+
// ── フォールバック表示 ──
|
|
673
|
+
/** xmlReport の情報だけで簡易表示(パース失敗時のフォールバック) */
|
|
674
|
+
function displayRawHeader(msg) {
|
|
675
|
+
console.log();
|
|
676
|
+
console.log(separator());
|
|
677
|
+
console.log(theme.getRoleChalk("rawHeaderLabel")(`電文受信: `) +
|
|
678
|
+
chalk_1.default.white(sanitizeForTerminal(msg.xmlReport?.control?.title || msg.head.type)) +
|
|
679
|
+
chalk_1.default.gray(` [${sanitizeForTerminal(msg.head.type)}]`));
|
|
680
|
+
if (msg.xmlReport) {
|
|
681
|
+
const r = msg.xmlReport;
|
|
682
|
+
console.log(chalk_1.default.gray(` ${sanitizeForTerminal(r.head.title)}`));
|
|
683
|
+
console.log(chalk_1.default.gray(` ${sanitizeForTerminal(r.head.reportDateTime)} ${sanitizeForTerminal(r.control.publishingOffice)}`));
|
|
684
|
+
if (r.head.headline) {
|
|
685
|
+
console.log(chalk_1.default.white(` ${sanitizeForTerminal(r.head.headline)}`));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
console.log(separator());
|
|
689
|
+
}
|