@sayue_ltr/fleq 1.50.0 → 1.51.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/CHANGELOG.md +103 -0
- package/README.md +47 -5
- package/dist/config.js +35 -2
- package/dist/dmdata/rest-client.js +58 -3
- package/dist/dmdata/telegram-parser.js +37 -59
- package/dist/dmdata/ws-client.js +49 -18
- package/dist/engine/cli/cli-run.js +71 -1
- package/dist/engine/cli/cli.js +12 -0
- package/dist/engine/filter/compile-filter.js +21 -0
- package/dist/engine/filter/compiler.js +188 -0
- package/dist/engine/filter/errors.js +41 -0
- package/dist/engine/filter/field-registry.js +78 -0
- package/dist/engine/filter/index.js +15 -0
- package/dist/engine/filter/parser.js +137 -0
- package/dist/engine/filter/rank-maps.js +34 -0
- package/dist/engine/filter/tokenizer.js +121 -0
- package/dist/engine/filter/type-checker.js +104 -0
- package/dist/engine/filter/types.js +2 -0
- package/dist/engine/filter-template/pipeline-controller.js +73 -0
- package/dist/engine/filter-template/pipeline.js +16 -0
- package/dist/engine/messages/display-callbacks.js +7 -0
- package/dist/engine/messages/message-router.js +114 -182
- package/dist/engine/messages/summary-tracker.js +106 -0
- package/dist/engine/messages/telegram-stats.js +103 -0
- package/dist/engine/messages/volcano-route-handler.js +122 -0
- package/dist/engine/monitor/monitor.js +51 -3
- package/dist/engine/monitor/shutdown.js +1 -0
- package/dist/engine/notification/notifier.js +16 -1
- package/dist/engine/notification/sound-player.js +193 -28
- package/dist/engine/presentation/diff-store.js +158 -0
- package/dist/engine/presentation/diff-types.js +2 -0
- package/dist/engine/presentation/events/from-earthquake.js +53 -0
- package/dist/engine/presentation/events/from-eew.js +72 -0
- package/dist/engine/presentation/events/from-lg-observation.js +58 -0
- package/dist/engine/presentation/events/from-nankai-trough.js +39 -0
- package/dist/engine/presentation/events/from-raw.js +35 -0
- package/dist/engine/presentation/events/from-seismic-text.js +37 -0
- package/dist/engine/presentation/events/from-tsunami.js +51 -0
- package/dist/engine/presentation/events/from-volcano.js +88 -0
- package/dist/engine/presentation/events/to-presentation-event.js +32 -0
- package/dist/engine/presentation/level-helpers.js +118 -0
- package/dist/engine/presentation/processors/process-earthquake.js +36 -0
- package/dist/engine/presentation/processors/process-eew.js +90 -0
- package/dist/engine/presentation/processors/process-lg-observation.js +30 -0
- package/dist/engine/presentation/processors/process-message.js +53 -0
- package/dist/engine/presentation/processors/process-nankai-trough.js +30 -0
- package/dist/engine/presentation/processors/process-raw.js +22 -0
- package/dist/engine/presentation/processors/process-seismic-text.js +30 -0
- package/dist/engine/presentation/processors/process-tsunami.js +42 -0
- package/dist/engine/presentation/processors/process-volcano.js +41 -0
- package/dist/engine/presentation/types.js +2 -0
- package/dist/engine/startup/config-resolver.js +2 -0
- package/dist/engine/template/compile-template.js +18 -0
- package/dist/engine/template/compiler.js +102 -0
- package/dist/engine/template/field-accessor.js +25 -0
- package/dist/engine/template/filters.js +94 -0
- package/dist/engine/template/index.js +5 -0
- package/dist/engine/template/parser.js +190 -0
- package/dist/engine/template/tokenizer.js +96 -0
- package/dist/engine/template/types.js +2 -0
- package/dist/types.js +2 -1
- package/dist/ui/display-adapter.js +60 -0
- package/dist/ui/earthquake-formatter.js +17 -5
- package/dist/ui/eew-formatter.js +25 -10
- package/dist/ui/formatter.js +67 -32
- package/dist/ui/minimap/grid-layout.js +91 -0
- package/dist/ui/minimap/index.js +16 -0
- package/dist/ui/minimap/minimap-renderer.js +277 -0
- package/dist/ui/minimap/pref-mapping.js +82 -0
- package/dist/ui/minimap/types.js +2 -0
- package/dist/ui/night-overlay.js +56 -0
- package/dist/ui/repl-handlers/command-definitions.js +320 -0
- package/dist/ui/repl-handlers/index.js +11 -0
- package/dist/ui/repl-handlers/info-handlers.js +577 -0
- package/dist/ui/repl-handlers/operation-handlers.js +233 -0
- package/dist/ui/repl-handlers/settings-handlers.js +923 -0
- package/dist/ui/repl-handlers/types.js +10 -0
- package/dist/ui/repl.js +81 -1752
- package/dist/ui/statistics-formatter.js +208 -0
- package/dist/ui/status-line.js +69 -0
- package/dist/ui/summary/index.js +5 -0
- package/dist/ui/summary/summary-line.js +18 -0
- package/dist/ui/summary/summary-model.js +31 -0
- package/dist/ui/summary/token-builders.js +317 -0
- package/dist/ui/summary/types.js +2 -0
- package/dist/ui/summary/width-fit.js +41 -0
- package/dist/ui/summary-interval-formatter.js +72 -0
- package/dist/ui/theme.js +34 -5
- package/dist/ui/tip-shuffler.js +81 -0
- package/dist/ui/volcano-formatter.js +15 -13
- package/dist/ui/waiting-tips.js +289 -249
- package/package.json +1 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseTemplate = parseTemplate;
|
|
4
|
+
const tokenizer_1 = require("./tokenizer");
|
|
5
|
+
const MAX_DEPTH = 32;
|
|
6
|
+
function parseTemplate(source) {
|
|
7
|
+
const tokens = (0, tokenizer_1.tokenizeTemplate)(source);
|
|
8
|
+
return new TemplateParser(tokens).parse();
|
|
9
|
+
}
|
|
10
|
+
class TemplateParser {
|
|
11
|
+
tokens;
|
|
12
|
+
pos = 0;
|
|
13
|
+
depth = 0;
|
|
14
|
+
constructor(tokens) {
|
|
15
|
+
this.tokens = tokens;
|
|
16
|
+
}
|
|
17
|
+
parse() {
|
|
18
|
+
return this.parseNodes();
|
|
19
|
+
}
|
|
20
|
+
parseNodes() {
|
|
21
|
+
const nodes = [];
|
|
22
|
+
while (this.pos < this.tokens.length) {
|
|
23
|
+
const token = this.tokens[this.pos];
|
|
24
|
+
if (token.kind === "eof" || token.kind === "else" || token.kind === "endif")
|
|
25
|
+
break;
|
|
26
|
+
if (token.kind === "text" && this.peek(-1)?.kind !== "open" && this.peek(-1)?.kind !== "if_open") {
|
|
27
|
+
// 外側テキスト
|
|
28
|
+
nodes.push({ kind: "text", value: token.value });
|
|
29
|
+
this.pos++;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (token.kind === "if_open") {
|
|
33
|
+
nodes.push(this.parseIfBlock());
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (token.kind === "open") {
|
|
37
|
+
nodes.push(this.parseInterpolation());
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
// 未消費トークンはテキストとして扱う
|
|
41
|
+
nodes.push({ kind: "text", value: token.value });
|
|
42
|
+
this.pos++;
|
|
43
|
+
}
|
|
44
|
+
return nodes;
|
|
45
|
+
}
|
|
46
|
+
parseInterpolation() {
|
|
47
|
+
this.expect("open"); // {{
|
|
48
|
+
const exprToken = this.tokens[this.pos];
|
|
49
|
+
this.pos++;
|
|
50
|
+
const expr = this.parseExpr(exprToken.value);
|
|
51
|
+
const filters = [];
|
|
52
|
+
while (this.pos < this.tokens.length && this.tokens[this.pos].kind === "pipe") {
|
|
53
|
+
this.pos++; // |
|
|
54
|
+
const nameToken = this.tokens[this.pos];
|
|
55
|
+
this.pos++;
|
|
56
|
+
const args = [];
|
|
57
|
+
// 複数引数対応: colon が続く限り引数を読む
|
|
58
|
+
while (this.pos < this.tokens.length && this.tokens[this.pos].kind === "colon") {
|
|
59
|
+
this.pos++; // :
|
|
60
|
+
const argToken = this.tokens[this.pos];
|
|
61
|
+
this.pos++;
|
|
62
|
+
args.push(this.parseExpr(argToken.value));
|
|
63
|
+
}
|
|
64
|
+
filters.push({ name: nameToken.value, args });
|
|
65
|
+
}
|
|
66
|
+
this.expect("close"); // }}
|
|
67
|
+
return { kind: "interpolation", expr, filters };
|
|
68
|
+
}
|
|
69
|
+
parseIfBlock() {
|
|
70
|
+
this.expect("if_open"); // {{#if
|
|
71
|
+
const condToken = this.tokens[this.pos];
|
|
72
|
+
this.pos++;
|
|
73
|
+
const test = this.parsePredicate(condToken.value);
|
|
74
|
+
this.expect("close"); // }}
|
|
75
|
+
this.depth++;
|
|
76
|
+
if (this.depth > MAX_DEPTH) {
|
|
77
|
+
throw new Error(`テンプレート構文エラー: #if のネストが深すぎる (最大 ${MAX_DEPTH} 段)`);
|
|
78
|
+
}
|
|
79
|
+
const body = this.parseNodes();
|
|
80
|
+
let elseBody;
|
|
81
|
+
if (this.pos < this.tokens.length && this.tokens[this.pos].kind === "else") {
|
|
82
|
+
this.pos++; // {{else}}
|
|
83
|
+
elseBody = this.parseNodes();
|
|
84
|
+
}
|
|
85
|
+
if (this.pos < this.tokens.length && this.tokens[this.pos].kind === "endif") {
|
|
86
|
+
this.pos++; // {{/if}}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
this.depth--;
|
|
90
|
+
throw new Error('テンプレート構文エラー: "endif" が必要ですが見つかりません。{{/if}} で閉じてください');
|
|
91
|
+
}
|
|
92
|
+
this.depth--;
|
|
93
|
+
return { kind: "if", test, body, elseBody };
|
|
94
|
+
}
|
|
95
|
+
parseExpr(raw) {
|
|
96
|
+
const trimmed = raw.trim();
|
|
97
|
+
// 文字列リテラル
|
|
98
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
99
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
100
|
+
return { kind: "literal", value: unescapeString(trimmed.slice(1, -1)) };
|
|
101
|
+
}
|
|
102
|
+
// 数値
|
|
103
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
104
|
+
return { kind: "literal", value: Number(trimmed) };
|
|
105
|
+
}
|
|
106
|
+
// boolean / null
|
|
107
|
+
if (trimmed === "true")
|
|
108
|
+
return { kind: "literal", value: true };
|
|
109
|
+
if (trimmed === "false")
|
|
110
|
+
return { kind: "literal", value: false };
|
|
111
|
+
if (trimmed === "null")
|
|
112
|
+
return { kind: "literal", value: null };
|
|
113
|
+
// パス: dot + bracket 記法を (string | number)[] に分割
|
|
114
|
+
return { kind: "path", segments: parsePathSegments(trimmed) };
|
|
115
|
+
}
|
|
116
|
+
parsePredicate(raw) {
|
|
117
|
+
const trimmed = raw.trim();
|
|
118
|
+
// 簡易比較: "field op value" 形式
|
|
119
|
+
const cmpMatch = trimmed.match(/^(\S+)\s+(=|!=|>|>=|<|<=)\s+(.+)$/);
|
|
120
|
+
if (cmpMatch) {
|
|
121
|
+
const opMap = {
|
|
122
|
+
"=": "eq", "!=": "ne", ">": "gt", ">=": "ge", "<": "lt", "<=": "le",
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
kind: "compare",
|
|
126
|
+
op: opMap[cmpMatch[2]],
|
|
127
|
+
left: this.parseExpr(cmpMatch[1]),
|
|
128
|
+
right: this.parseExpr(cmpMatch[3]),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// truthy
|
|
132
|
+
return { kind: "truthy", expr: this.parseExpr(trimmed) };
|
|
133
|
+
}
|
|
134
|
+
expect(kind) {
|
|
135
|
+
if (this.pos < this.tokens.length && this.tokens[this.pos].kind === kind) {
|
|
136
|
+
this.pos++;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const actual = this.pos < this.tokens.length ? this.tokens[this.pos].kind : "eof";
|
|
140
|
+
throw new Error(`テンプレート構文エラー: "${kind}" が必要ですが "${actual}" が見つかりました`);
|
|
141
|
+
}
|
|
142
|
+
peek(offset) {
|
|
143
|
+
return this.tokens[this.pos + offset];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* ドットパスおよびブラケット記法を segments 配列に分割する。
|
|
148
|
+
* 例: "areaItems[0].name" → ["areaItems", 0, "name"]
|
|
149
|
+
* "foo.bar.baz" → ["foo", "bar", "baz"]
|
|
150
|
+
*/
|
|
151
|
+
function parsePathSegments(path) {
|
|
152
|
+
const segments = [];
|
|
153
|
+
let i = 0;
|
|
154
|
+
while (i < path.length) {
|
|
155
|
+
if (path[i] === ".") {
|
|
156
|
+
i++; // ドット区切りをスキップ
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (path[i] === "[") {
|
|
160
|
+
// ブラケットアクセス: [N]
|
|
161
|
+
i++; // [
|
|
162
|
+
const start = i;
|
|
163
|
+
while (i < path.length && path[i] !== "]")
|
|
164
|
+
i++;
|
|
165
|
+
const inner = path.slice(start, i);
|
|
166
|
+
if (i < path.length)
|
|
167
|
+
i++; // ]
|
|
168
|
+
// 数値ならnumber、そうでなければstring
|
|
169
|
+
const num = Number(inner);
|
|
170
|
+
segments.push(Number.isNaN(num) ? inner : num);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
// 識別子: 次の . または [ まで
|
|
174
|
+
const start = i;
|
|
175
|
+
while (i < path.length && path[i] !== "." && path[i] !== "[")
|
|
176
|
+
i++;
|
|
177
|
+
segments.push(path.slice(start, i));
|
|
178
|
+
}
|
|
179
|
+
return segments;
|
|
180
|
+
}
|
|
181
|
+
/** 文字列リテラル内のエスケープシーケンスを復元する */
|
|
182
|
+
function unescapeString(s) {
|
|
183
|
+
return s.replace(/\\(["'\\nt])/g, (_, ch) => {
|
|
184
|
+
switch (ch) {
|
|
185
|
+
case "n": return "\n";
|
|
186
|
+
case "t": return "\t";
|
|
187
|
+
default: return ch; // \\, \", \'
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tokenizeTemplate = tokenizeTemplate;
|
|
4
|
+
function tokenizeTemplate(source) {
|
|
5
|
+
const tokens = [];
|
|
6
|
+
let i = 0;
|
|
7
|
+
while (i < source.length) {
|
|
8
|
+
// {{#if ...}}
|
|
9
|
+
if (source.startsWith("{{#if", i)) {
|
|
10
|
+
tokens.push({ kind: "if_open", value: "{{#if", pos: i });
|
|
11
|
+
i += 5;
|
|
12
|
+
// 空白スキップ
|
|
13
|
+
while (i < source.length && source[i] === " ")
|
|
14
|
+
i++;
|
|
15
|
+
// 条件式を "}}" まで読む
|
|
16
|
+
const start = i;
|
|
17
|
+
while (i < source.length && !source.startsWith("}}", i))
|
|
18
|
+
i++;
|
|
19
|
+
tokens.push({ kind: "text", value: source.slice(start, i), pos: start });
|
|
20
|
+
if (source.startsWith("}}", i)) {
|
|
21
|
+
tokens.push({ kind: "close", value: "}}", pos: i });
|
|
22
|
+
i += 2;
|
|
23
|
+
}
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
// {{else}}
|
|
27
|
+
if (source.startsWith("{{else}}", i)) {
|
|
28
|
+
tokens.push({ kind: "else", value: "{{else}}", pos: i });
|
|
29
|
+
i += 8;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
// {{/if}}
|
|
33
|
+
if (source.startsWith("{{/if}}", i)) {
|
|
34
|
+
tokens.push({ kind: "endif", value: "{{/if}}", pos: i });
|
|
35
|
+
i += 7;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
// {{ interpolation }}
|
|
39
|
+
if (source.startsWith("{{", i)) {
|
|
40
|
+
tokens.push({ kind: "open", value: "{{", pos: i });
|
|
41
|
+
i += 2;
|
|
42
|
+
// interpolation 内容を解析
|
|
43
|
+
while (i < source.length && !source.startsWith("}}", i)) {
|
|
44
|
+
if (source[i] === "|") {
|
|
45
|
+
tokens.push({ kind: "pipe", value: "|", pos: i });
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
else if (source[i] === ":") {
|
|
49
|
+
tokens.push({ kind: "colon", value: ":", pos: i });
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
else if (source[i] === " ") {
|
|
53
|
+
i++;
|
|
54
|
+
}
|
|
55
|
+
else if (source[i] === '"' || source[i] === "'") {
|
|
56
|
+
// 文字列リテラル
|
|
57
|
+
const quote = source[i];
|
|
58
|
+
const start = i;
|
|
59
|
+
i++;
|
|
60
|
+
while (i < source.length && source[i] !== quote) {
|
|
61
|
+
if (source[i] === "\\" && i + 1 < source.length)
|
|
62
|
+
i++;
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
if (i < source.length)
|
|
66
|
+
i++; // 閉じ引用符
|
|
67
|
+
tokens.push({ kind: "text", value: source.slice(start, i), pos: start });
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// 識別子/数値
|
|
71
|
+
const start = i;
|
|
72
|
+
while (i < source.length &&
|
|
73
|
+
!source.startsWith("}}", i) &&
|
|
74
|
+
source[i] !== "|" &&
|
|
75
|
+
source[i] !== ":" &&
|
|
76
|
+
source[i] !== " ") {
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
tokens.push({ kind: "text", value: source.slice(start, i), pos: start });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (source.startsWith("}}", i)) {
|
|
83
|
+
tokens.push({ kind: "close", value: "}}", pos: i });
|
|
84
|
+
i += 2;
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// テキスト
|
|
89
|
+
const start = i;
|
|
90
|
+
while (i < source.length && !source.startsWith("{{", i))
|
|
91
|
+
i++;
|
|
92
|
+
tokens.push({ kind: "text", value: source.slice(start, i), pos: start });
|
|
93
|
+
}
|
|
94
|
+
tokens.push({ kind: "eof", value: "", pos: source.length });
|
|
95
|
+
return tokens;
|
|
96
|
+
}
|
package/dist/types.js
CHANGED
|
@@ -39,6 +39,8 @@ exports.DEFAULT_CONFIG = {
|
|
|
39
39
|
maxIntChangeReason: true,
|
|
40
40
|
},
|
|
41
41
|
maxObservations: null,
|
|
42
|
+
nightMode: false,
|
|
43
|
+
summaryInterval: null,
|
|
42
44
|
backup: false,
|
|
43
45
|
truncation: {
|
|
44
46
|
seismicTextLines: 15,
|
|
@@ -50,7 +52,6 @@ exports.DEFAULT_CONFIG = {
|
|
|
50
52
|
volcanoAshfallDetailLines: 16,
|
|
51
53
|
volcanoAshfallRegularLines: 10,
|
|
52
54
|
volcanoPreventionLines: 8,
|
|
53
|
-
volcanoMunicipalities: 6,
|
|
54
55
|
ashfallAreasQuick: 5,
|
|
55
56
|
ashfallAreasOther: 3,
|
|
56
57
|
ashfallPeriodsQuick: 1,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* engine 層の DisplayCallbacks を実装する UI アダプター。
|
|
4
|
+
* 全ての display 関数をここに集約し、engine → ui の逆方向依存を断つ。
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.createDisplayAdapter = createDisplayAdapter;
|
|
8
|
+
const formatter_1 = require("./formatter");
|
|
9
|
+
const summary_1 = require("./summary");
|
|
10
|
+
const eew_formatter_1 = require("./eew-formatter");
|
|
11
|
+
const earthquake_formatter_1 = require("./earthquake-formatter");
|
|
12
|
+
const volcano_formatter_1 = require("./volcano-formatter");
|
|
13
|
+
/** DisplayCallbacks の実装を生成する */
|
|
14
|
+
function createDisplayAdapter() {
|
|
15
|
+
return {
|
|
16
|
+
displayOutcome(outcome) {
|
|
17
|
+
switch (outcome.domain) {
|
|
18
|
+
case "eew":
|
|
19
|
+
(0, eew_formatter_1.displayEewInfo)(outcome.parsed, {
|
|
20
|
+
activeCount: outcome.eewResult.activeCount,
|
|
21
|
+
diff: outcome.eewResult.diff,
|
|
22
|
+
colorIndex: outcome.eewResult.colorIndex,
|
|
23
|
+
});
|
|
24
|
+
break;
|
|
25
|
+
case "earthquake":
|
|
26
|
+
(0, earthquake_formatter_1.displayEarthquakeInfo)(outcome.parsed);
|
|
27
|
+
break;
|
|
28
|
+
case "seismicText":
|
|
29
|
+
(0, earthquake_formatter_1.displaySeismicTextInfo)(outcome.parsed);
|
|
30
|
+
break;
|
|
31
|
+
case "lgObservation":
|
|
32
|
+
(0, earthquake_formatter_1.displayLgObservationInfo)(outcome.parsed);
|
|
33
|
+
break;
|
|
34
|
+
case "tsunami":
|
|
35
|
+
(0, earthquake_formatter_1.displayTsunamiInfo)(outcome.parsed);
|
|
36
|
+
break;
|
|
37
|
+
case "nankaiTrough":
|
|
38
|
+
(0, earthquake_formatter_1.displayNankaiTroughInfo)(outcome.parsed);
|
|
39
|
+
break;
|
|
40
|
+
case "raw":
|
|
41
|
+
(0, formatter_1.displayRawHeader)(outcome.msg);
|
|
42
|
+
break;
|
|
43
|
+
// volcano: VolcanoRouteHandler 経由で displayVolcano/displayVolcanoBatch を直接呼ぶ
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
displayRawHeader(msg) {
|
|
47
|
+
(0, formatter_1.displayRawHeader)(msg);
|
|
48
|
+
},
|
|
49
|
+
displayVolcano(info, presentation) {
|
|
50
|
+
(0, volcano_formatter_1.displayVolcanoInfo)(info, presentation);
|
|
51
|
+
},
|
|
52
|
+
displayVolcanoBatch(batch, presentation) {
|
|
53
|
+
(0, volcano_formatter_1.displayVolcanoAshfallBatch)(batch, presentation);
|
|
54
|
+
},
|
|
55
|
+
getDisplayMode() {
|
|
56
|
+
return (0, formatter_1.getDisplayMode)();
|
|
57
|
+
},
|
|
58
|
+
renderSummaryLine: summary_1.renderSummaryLine,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -825,7 +825,7 @@ function displayLgObservationInfo(info) {
|
|
|
825
825
|
buf.push((0, formatter_1.frameLine)(level, chalk_1.default.white("位置: ") + chalk_1.default.white(`${eq.latitude} ${eq.longitude}`), width));
|
|
826
826
|
}
|
|
827
827
|
}
|
|
828
|
-
// 地域リスト (LgInt 降順)
|
|
828
|
+
// 地域リスト (LgInt 降順 → 階級別グループ化)
|
|
829
829
|
if (info.areas.length > 0) {
|
|
830
830
|
buf.push((0, formatter_1.frameDivider)(level, width));
|
|
831
831
|
const sorted = [...info.areas].sort((a, b) => (0, formatter_1.lgIntToNumeric)(b.maxLgInt) - (0, formatter_1.lgIntToNumeric)(a.maxLgInt));
|
|
@@ -833,13 +833,25 @@ function displayLgObservationInfo(info) {
|
|
|
833
833
|
const maxObs = (0, formatter_1.getMaxObservations)();
|
|
834
834
|
const displayAreas = maxObs != null ? sorted.slice(0, maxObs) : sorted;
|
|
835
835
|
const hiddenCount = sorted.length - displayAreas.length;
|
|
836
|
+
// 長周期階級別にグループ化
|
|
837
|
+
const byLgInt = new Map();
|
|
836
838
|
for (const area of displayAreas) {
|
|
837
|
-
const
|
|
839
|
+
const key = area.maxLgInt;
|
|
840
|
+
if (!byLgInt.has(key))
|
|
841
|
+
byLgInt.set(key, []);
|
|
838
842
|
const ic = (0, formatter_1.intensityColor)(area.maxInt);
|
|
839
|
-
|
|
840
|
-
chalk_1.default.white(area.name)
|
|
841
|
-
ic(` (震度${area.maxInt})`),
|
|
843
|
+
byLgInt.get(key).push({
|
|
844
|
+
primary: chalk_1.default.white(area.name),
|
|
845
|
+
badges: [ic(` (震度${area.maxInt})`)],
|
|
846
|
+
});
|
|
842
847
|
}
|
|
848
|
+
// LgInt 降順でソート
|
|
849
|
+
const sortedEntries = [...byLgInt.entries()].sort((a, b) => (0, formatter_1.lgIntToNumeric)(b[0]) - (0, formatter_1.lgIntToNumeric)(a[0]));
|
|
850
|
+
const groups = sortedEntries.map(([lgInt, items]) => ({
|
|
851
|
+
prefix: (0, formatter_1.lgIntensityColor)(lgInt)(`長周期${lgInt}: `),
|
|
852
|
+
items,
|
|
853
|
+
}));
|
|
854
|
+
(0, formatter_1.renderGroupedItemList)({ level, width, groups, buf });
|
|
843
855
|
if (hiddenCount > 0) {
|
|
844
856
|
buf.push((0, formatter_1.frameLine)(level, chalk_1.default.gray(`... 他 ${hiddenCount} 地点`), width));
|
|
845
857
|
}
|
package/dist/ui/eew-formatter.js
CHANGED
|
@@ -282,23 +282,38 @@ function displayEewInfo(info, context) {
|
|
|
282
282
|
const allAreas = info.forecastIntensity.areas;
|
|
283
283
|
const displayAreas = maxObs != null ? allAreas.slice(0, maxObs) : allAreas;
|
|
284
284
|
const hiddenCount = allAreas.length - displayAreas.length;
|
|
285
|
+
// 震度別にグループ化
|
|
286
|
+
const byIntensity = new Map();
|
|
285
287
|
for (const area of displayAreas) {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
+
const key = area.intensity;
|
|
289
|
+
if (!byIntensity.has(key))
|
|
290
|
+
byIntensity.set(key, []);
|
|
291
|
+
const badges = [];
|
|
288
292
|
if (area.isPlum) {
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
if (area.hasArrived) {
|
|
292
|
-
areaText += theme.getRoleChalk("arrivedLabel")(" [到達]");
|
|
293
|
+
badges.push(theme.getRoleChalk("plumLabel")(" [PLUM]"));
|
|
293
294
|
}
|
|
295
|
+
// [到達] は下部の専用セクションに集約するため、ここでは付与しない
|
|
294
296
|
if (area.lgIntensity && (0, formatter_1.lgIntToNumeric)(area.lgIntensity) >= 1) {
|
|
295
297
|
const lc = (0, formatter_1.lgIntensityColor)(area.lgIntensity);
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
for (const wl of (0, formatter_1.wrapFrameLines)(level, color(`震度${area.intensity}: `) + areaText, width)) {
|
|
299
|
-
buf.push(wl);
|
|
298
|
+
badges.push(lc(` [長周期${area.lgIntensity}]`));
|
|
300
299
|
}
|
|
300
|
+
byIntensity.get(key).push({
|
|
301
|
+
primary: chalk_1.default.white(area.name),
|
|
302
|
+
badges: badges.length > 0 ? badges : undefined,
|
|
303
|
+
});
|
|
301
304
|
}
|
|
305
|
+
// 震度降順でソート
|
|
306
|
+
const order = ["7", "6+", "6強", "6-", "6弱", "5+", "5強", "5-", "5弱", "4", "3", "2", "1"];
|
|
307
|
+
const sortedEntries = [...byIntensity.entries()].sort((a, b) => {
|
|
308
|
+
const ai = order.indexOf(a[0]);
|
|
309
|
+
const bi = order.indexOf(b[0]);
|
|
310
|
+
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
|
311
|
+
});
|
|
312
|
+
const groups = sortedEntries.map(([int, items]) => ({
|
|
313
|
+
prefix: (0, formatter_1.intensityColor)(int)(`震度${int}: `),
|
|
314
|
+
items,
|
|
315
|
+
}));
|
|
316
|
+
(0, formatter_1.renderGroupedItemList)({ level, width, groups, buf });
|
|
302
317
|
if (hiddenCount > 0) {
|
|
303
318
|
buf.push((0, formatter_1.frameLine)(level, chalk_1.default.gray(`... 他 ${hiddenCount} 地点`), width));
|
|
304
319
|
}
|
package/dist/ui/formatter.js
CHANGED
|
@@ -61,6 +61,8 @@ exports.visualPadEnd = visualPadEnd;
|
|
|
61
61
|
exports.renderFrameTable = renderFrameTable;
|
|
62
62
|
exports.wrapFrameLines = wrapFrameLines;
|
|
63
63
|
exports.wrapTextLines = wrapTextLines;
|
|
64
|
+
exports.renderGroupedItemList = renderGroupedItemList;
|
|
65
|
+
exports.renderSimpleNameList = renderSimpleNameList;
|
|
64
66
|
exports.collectHighlightSpans = collectHighlightSpans;
|
|
65
67
|
exports.highlightAndWrap = highlightAndWrap;
|
|
66
68
|
exports.formatTimestamp = formatTimestamp;
|
|
@@ -311,30 +313,28 @@ function sanitizeForTerminal(str) {
|
|
|
311
313
|
// eslint-disable-next-line no-control-regex
|
|
312
314
|
return stripAnsi(str).replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
313
315
|
}
|
|
316
|
+
/** コードポイントが CJK 等の全角文字かどうかを判定する */
|
|
317
|
+
function isWideChar(cp) {
|
|
318
|
+
return ((cp >= 0x1100 && cp <= 0x115F) || // Hangul Jamo
|
|
319
|
+
(cp >= 0x2E80 && cp <= 0x303E) || // CJK部首・記号
|
|
320
|
+
(cp >= 0x3041 && cp <= 0x33BF) || // ひらがな・カタカナ・CJK互換
|
|
321
|
+
(cp >= 0x3400 && cp <= 0x4DBF) || // CJK統合漢字拡張A
|
|
322
|
+
(cp >= 0x4E00 && cp <= 0xA4CF) || // CJK統合漢字 + Yi
|
|
323
|
+
(cp >= 0xAC00 && cp <= 0xD7AF) || // Hangul Syllables
|
|
324
|
+
(cp >= 0xF900 && cp <= 0xFAFF) || // CJK互換漢字
|
|
325
|
+
(cp >= 0xFE30 && cp <= 0xFE4F) || // CJK互換形
|
|
326
|
+
(cp >= 0xFF01 && cp <= 0xFF60) || // 全角ASCII・半角カタカナ
|
|
327
|
+
(cp >= 0xFFE0 && cp <= 0xFFE6) || // 全角記号
|
|
328
|
+
(cp >= 0x20000 && cp <= 0x2FA1F) // CJK統合漢字拡張B-F
|
|
329
|
+
);
|
|
330
|
+
}
|
|
314
331
|
/** 文字列の視覚的な幅を計算(全角文字を2として数える) */
|
|
315
332
|
function visualWidth(str) {
|
|
316
333
|
const plain = stripAnsi(str);
|
|
317
334
|
let width = 0;
|
|
318
335
|
for (const ch of plain) {
|
|
319
336
|
const cp = ch.codePointAt(0) ?? 0;
|
|
320
|
-
|
|
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
|
-
}
|
|
337
|
+
width += isWideChar(cp) ? 2 : 1;
|
|
338
338
|
}
|
|
339
339
|
return width;
|
|
340
340
|
}
|
|
@@ -467,20 +467,7 @@ function wrapTextLines(text, maxWidth) {
|
|
|
467
467
|
let currentWidth = 0;
|
|
468
468
|
for (const ch of text) {
|
|
469
469
|
const cp = ch.codePointAt(0) ?? 0;
|
|
470
|
-
|
|
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
|
-
}
|
|
470
|
+
const charWidth = isWideChar(cp) ? 2 : 1;
|
|
484
471
|
if (currentWidth + charWidth > maxWidth) {
|
|
485
472
|
lines.push(currentLine);
|
|
486
473
|
currentLine = ch;
|
|
@@ -496,6 +483,54 @@ function wrapTextLines(text, maxWidth) {
|
|
|
496
483
|
}
|
|
497
484
|
return lines;
|
|
498
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* 震度別・階級別にグループ化された項目リストを描画する。
|
|
488
|
+
* 各グループは `prefix + item, item, ...` 形式で、2行目以降は prefix 幅分のハンギングインデント。
|
|
489
|
+
* badges は primary の直後に連結される(先頭スペースは呼び出し側で付与済み想定)。
|
|
490
|
+
*/
|
|
491
|
+
function renderGroupedItemList(options) {
|
|
492
|
+
const { level, width, groups, itemSeparator = ", " } = options;
|
|
493
|
+
const out = options.buf
|
|
494
|
+
? (line) => options.buf.push(line)
|
|
495
|
+
: (line) => console.log(line);
|
|
496
|
+
for (const group of groups) {
|
|
497
|
+
if (group.items.length === 0)
|
|
498
|
+
continue;
|
|
499
|
+
const prefix = group.prefix;
|
|
500
|
+
const indentWidth = visualWidth(stripAnsi(prefix));
|
|
501
|
+
// 各項目を "primary + badges" 形式の文字列に組み立て
|
|
502
|
+
const itemTexts = group.items.map((item) => {
|
|
503
|
+
let text = item.primary;
|
|
504
|
+
if (item.badges != null && item.badges.length > 0) {
|
|
505
|
+
text += item.badges.join("");
|
|
506
|
+
}
|
|
507
|
+
return text;
|
|
508
|
+
});
|
|
509
|
+
const content = prefix + itemTexts.join(itemSeparator);
|
|
510
|
+
for (const line of wrapFrameLines(level, content, width, indentWidth)) {
|
|
511
|
+
out(line);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* 単純な名前リストを描画する。
|
|
517
|
+
* `label + name, name, ...` 形式。2行目以降は label 幅分のハンギングインデント。
|
|
518
|
+
* label 指定時は ` label ` の形式(先頭・末尾にスペース)で gray 表示される。
|
|
519
|
+
*/
|
|
520
|
+
function renderSimpleNameList(options) {
|
|
521
|
+
const { level, width, items, label, separator = ", " } = options;
|
|
522
|
+
if (items.length === 0)
|
|
523
|
+
return;
|
|
524
|
+
const out = options.buf
|
|
525
|
+
? (line) => options.buf.push(line)
|
|
526
|
+
: (line) => console.log(line);
|
|
527
|
+
const prefix = label ? ` ${chalk_1.default.gray(label)} ` : "";
|
|
528
|
+
const indentWidth = label ? visualWidth(stripAnsi(prefix)) : 0;
|
|
529
|
+
const content = prefix + items.join(separator);
|
|
530
|
+
for (const line of wrapFrameLines(level, content, width, indentWidth)) {
|
|
531
|
+
out(line);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
499
534
|
/** 元の行からマッチspanを収集する */
|
|
500
535
|
function collectHighlightSpans(line, rules) {
|
|
501
536
|
const spans = [];
|