@sayue_ltr/fleq 1.50.1 → 2.0.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 +174 -0
- package/README.md +42 -6
- package/dist/config.js +37 -4
- package/dist/dmdata/rest-client.js +58 -3
- package/dist/dmdata/telegram-parser.js +115 -64
- package/dist/dmdata/ws-client.js +49 -18
- package/dist/engine/cli/cli-run.js +88 -3
- package/dist/engine/cli/cli.js +12 -0
- package/dist/engine/eew/eew-tracker.js +41 -15
- 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 +52 -4
- package/dist/engine/monitor/shutdown.js +1 -0
- package/dist/engine/notification/notifier.js +21 -4
- package/dist/engine/notification/sound-player.js +398 -36
- 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 +105 -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 +105 -0
- package/dist/engine/template/field-accessor.js +31 -0
- package/dist/engine/template/filters.js +100 -0
- package/dist/engine/template/index.js +5 -0
- package/dist/engine/template/parser.js +185 -0
- package/dist/engine/template/tokenizer.js +96 -0
- package/dist/engine/template/types.js +2 -0
- package/dist/types.js +3 -2
- package/dist/ui/display-adapter.js +60 -0
- package/dist/ui/earthquake-formatter.js +22 -5
- package/dist/ui/eew-formatter.js +25 -10
- package/dist/ui/formatter.js +116 -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 +327 -0
- package/dist/ui/repl-handlers/index.js +11 -0
- package/dist/ui/repl-handlers/info-handlers.js +633 -0
- package/dist/ui/repl-handlers/operation-handlers.js +233 -0
- package/dist/ui/repl-handlers/settings-handlers.js +927 -0
- package/dist/ui/repl-handlers/types.js +10 -0
- package/dist/ui/repl.js +81 -1752
- package/dist/ui/statistics-formatter.js +258 -0
- package/dist/ui/status-line.js +80 -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/test-samples.js +6 -0
- package/dist/ui/theme.js +43 -5
- package/dist/ui/tip-shuffler.js +81 -0
- package/dist/ui/volcano-formatter.js +15 -13
- package/dist/ui/waiting-tips-eew.js +63 -0
- package/dist/ui/waiting-tips-info-systems.js +81 -0
- package/dist/ui/waiting-tips-seismology.js +97 -0
- package/dist/ui/waiting-tips-tsunami.js +72 -0
- package/dist/ui/waiting-tips-weather.js +189 -0
- package/dist/ui/waiting-tips.js +420 -249
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getFieldValue = getFieldValue;
|
|
4
|
+
/**
|
|
5
|
+
* PresentationEvent からドットパスで値を取得する。
|
|
6
|
+
*
|
|
7
|
+
* 表示専用ポリシー (dmdata.jp 再配信ポリシー対応) のため、配列インデックス参照
|
|
8
|
+
* (`[N]`) は parser 側で禁止済み。本関数では二重防御として、念のため `raw`
|
|
9
|
+
* フィールドへのアクセスも拒否する。
|
|
10
|
+
*
|
|
11
|
+
* segments 例:
|
|
12
|
+
* - ["title"] → event.title
|
|
13
|
+
* - ["earthquake", "magnitude"] → event.earthquake.magnitude
|
|
14
|
+
*/
|
|
15
|
+
function getFieldValue(event, segments) {
|
|
16
|
+
// 二重防御: parser を経由せず直接呼ばれた場合にも raw への参照を拒否
|
|
17
|
+
if (segments[0] === "raw")
|
|
18
|
+
return undefined;
|
|
19
|
+
let current = event;
|
|
20
|
+
for (const seg of segments) {
|
|
21
|
+
if (current == null)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (typeof current === "object") {
|
|
24
|
+
current = current[seg];
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return current;
|
|
31
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ── Template filter functions ──
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.applyFilter = applyFilter;
|
|
5
|
+
/**
|
|
6
|
+
* フィルタ内部で値を文字列化する共通関数。
|
|
7
|
+
*
|
|
8
|
+
* 表示専用ポリシー対応: 配列は改行区切りで文字列化する。
|
|
9
|
+
* `String([...])` が "," 連結するため、`|upper` や `|replace` といった
|
|
10
|
+
* 文字列系フィルタ経由で配列を 1 行機械可読出力に整形できてしまうのを防ぐ。
|
|
11
|
+
*/
|
|
12
|
+
function toString(value) {
|
|
13
|
+
if (value == null)
|
|
14
|
+
return "";
|
|
15
|
+
if (Array.isArray(value))
|
|
16
|
+
return value.join("\n");
|
|
17
|
+
return String(value);
|
|
18
|
+
}
|
|
19
|
+
function filterDefault(value, args) {
|
|
20
|
+
if (value == null || value === "") {
|
|
21
|
+
return args[0] ?? "";
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function filterTruncate(value, args) {
|
|
26
|
+
const str = toString(value);
|
|
27
|
+
const limit = typeof args[0] === "number" ? args[0] : Number(args[0]);
|
|
28
|
+
if (!Number.isFinite(limit) || str.length <= limit)
|
|
29
|
+
return str;
|
|
30
|
+
return str.slice(0, limit);
|
|
31
|
+
}
|
|
32
|
+
function filterPad(value, args) {
|
|
33
|
+
const str = toString(value);
|
|
34
|
+
const width = typeof args[0] === "number" ? args[0] : Number(args[0]);
|
|
35
|
+
if (!Number.isFinite(width))
|
|
36
|
+
return str;
|
|
37
|
+
return str.padEnd(width);
|
|
38
|
+
}
|
|
39
|
+
function filterDate(value, args) {
|
|
40
|
+
const format = args[0] != null ? String(args[0]) : "HH:mm";
|
|
41
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
42
|
+
if (isNaN(date.getTime()))
|
|
43
|
+
return toString(value);
|
|
44
|
+
const HH = String(date.getHours()).padStart(2, "0");
|
|
45
|
+
const mm = String(date.getMinutes()).padStart(2, "0");
|
|
46
|
+
const ss = String(date.getSeconds()).padStart(2, "0");
|
|
47
|
+
const MM = String(date.getMonth() + 1).padStart(2, "0");
|
|
48
|
+
const DD = String(date.getDate()).padStart(2, "0");
|
|
49
|
+
switch (format) {
|
|
50
|
+
case "HH:mm":
|
|
51
|
+
return `${HH}:${mm}`;
|
|
52
|
+
case "HH:mm:ss":
|
|
53
|
+
return `${HH}:${mm}:${ss}`;
|
|
54
|
+
case "MM/DD HH:mm":
|
|
55
|
+
return `${MM}/${DD} ${HH}:${mm}`;
|
|
56
|
+
default:
|
|
57
|
+
return `${HH}:${mm}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function filterReplace(value, args) {
|
|
61
|
+
const str = toString(value);
|
|
62
|
+
const search = args[0] != null ? String(args[0]) : "";
|
|
63
|
+
const replacement = args[1] != null ? String(args[1]) : "";
|
|
64
|
+
// 表示専用ポリシー対応: search/replacement に改行文字を含めると、
|
|
65
|
+
// 配列の改行 join を 1 行化する経路(\n → "," 等の置換)を作れてしまうため禁止。
|
|
66
|
+
if (/[\r\n]/.test(search) || /[\r\n]/.test(replacement)) {
|
|
67
|
+
throw new Error("テンプレート実行エラー: replace フィルタの引数に改行文字 (\\n / \\r) を含めることはできません。表示専用制限です。");
|
|
68
|
+
}
|
|
69
|
+
return str.split(search).join(replacement);
|
|
70
|
+
}
|
|
71
|
+
function filterUpper(value) {
|
|
72
|
+
return toString(value).toUpperCase();
|
|
73
|
+
}
|
|
74
|
+
function filterLower(value) {
|
|
75
|
+
return toString(value).toLowerCase();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* テンプレートフィルタを適用する。
|
|
79
|
+
* 未知のフィルタ名の場合は値をそのまま返す。
|
|
80
|
+
*/
|
|
81
|
+
function applyFilter(name, value, args) {
|
|
82
|
+
switch (name) {
|
|
83
|
+
case "default":
|
|
84
|
+
return filterDefault(value, args);
|
|
85
|
+
case "truncate":
|
|
86
|
+
return filterTruncate(value, args);
|
|
87
|
+
case "pad":
|
|
88
|
+
return filterPad(value, args);
|
|
89
|
+
case "date":
|
|
90
|
+
return filterDate(value, args);
|
|
91
|
+
case "replace":
|
|
92
|
+
return filterReplace(value, args);
|
|
93
|
+
case "upper":
|
|
94
|
+
return filterUpper(value);
|
|
95
|
+
case "lower":
|
|
96
|
+
return filterLower(value);
|
|
97
|
+
default:
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.compileTemplate = void 0;
|
|
4
|
+
var compile_template_1 = require("./compile-template");
|
|
5
|
+
Object.defineProperty(exports, "compileTemplate", { enumerable: true, get: function () { return compile_template_1.compileTemplate; } });
|
|
@@ -0,0 +1,185 @@
|
|
|
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
|
+
* 例: "foo.bar.baz" → ["foo", "bar", "baz"]
|
|
149
|
+
*
|
|
150
|
+
* 表示専用ポリシー (dmdata.jp 再配信ポリシー対応) のため、以下を禁止する:
|
|
151
|
+
* - ブラケット記法 `[N]` (配列インデックス参照)
|
|
152
|
+
* - 先頭セグメントが `raw` のパス (生 XML データへの直接参照)
|
|
153
|
+
*/
|
|
154
|
+
function parsePathSegments(path) {
|
|
155
|
+
const segments = [];
|
|
156
|
+
let i = 0;
|
|
157
|
+
while (i < path.length) {
|
|
158
|
+
if (path[i] === ".") {
|
|
159
|
+
i++; // ドット区切りをスキップ
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (path[i] === "[") {
|
|
163
|
+
throw new Error(`テンプレート構文エラー: 配列インデックス参照 [N] は無効です (path: "${path}")。表示専用制限により、要素を1行に並べる機械可読出力を防いでいます。`);
|
|
164
|
+
}
|
|
165
|
+
// 識別子: 次の . または [ まで
|
|
166
|
+
const start = i;
|
|
167
|
+
while (i < path.length && path[i] !== "." && path[i] !== "[")
|
|
168
|
+
i++;
|
|
169
|
+
segments.push(path.slice(start, i));
|
|
170
|
+
}
|
|
171
|
+
if (segments[0] === "raw") {
|
|
172
|
+
throw new Error(`テンプレート構文エラー: raw フィールド参照は無効です (path: "${path}")。表示専用制限により、生 XML データへの直接アクセスを禁止しています。`);
|
|
173
|
+
}
|
|
174
|
+
return segments;
|
|
175
|
+
}
|
|
176
|
+
/** 文字列リテラル内のエスケープシーケンスを復元する */
|
|
177
|
+
function unescapeString(s) {
|
|
178
|
+
return s.replace(/\\(["'\\nt])/g, (_, ch) => {
|
|
179
|
+
switch (ch) {
|
|
180
|
+
case "n": return "\n";
|
|
181
|
+
case "t": return "\t";
|
|
182
|
+
default: return ch; // \\, \", \'
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
@@ -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
|
@@ -23,7 +23,7 @@ exports.DEFAULT_CONFIG = {
|
|
|
23
23
|
volcano: true,
|
|
24
24
|
},
|
|
25
25
|
sound: true,
|
|
26
|
-
eewLog:
|
|
26
|
+
eewLog: false,
|
|
27
27
|
eewLogFields: {
|
|
28
28
|
hypocenter: true,
|
|
29
29
|
originTime: true,
|
|
@@ -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
|
+
}
|
|
@@ -381,6 +381,11 @@ function displayEarthquakeInfo(info) {
|
|
|
381
381
|
buf.push(wl);
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
|
+
// EventID (同一地震の紐付け用、通常モードのみ表示)
|
|
385
|
+
if (info.eventId) {
|
|
386
|
+
buf.push((0, formatter_1.frameDivider)(level, width));
|
|
387
|
+
buf.push((0, formatter_1.frameLine)(level, chalk_1.default.gray(`EventID: ${info.eventId}`), width));
|
|
388
|
+
}
|
|
384
389
|
// フッター
|
|
385
390
|
(0, formatter_1.renderFooter)(level, info.type, info.reportDateTime, info.publishingOffice, width, buf);
|
|
386
391
|
buf.push((0, formatter_1.frameBottom)(level, width));
|
|
@@ -825,7 +830,7 @@ function displayLgObservationInfo(info) {
|
|
|
825
830
|
buf.push((0, formatter_1.frameLine)(level, chalk_1.default.white("位置: ") + chalk_1.default.white(`${eq.latitude} ${eq.longitude}`), width));
|
|
826
831
|
}
|
|
827
832
|
}
|
|
828
|
-
// 地域リスト (LgInt 降順)
|
|
833
|
+
// 地域リスト (LgInt 降順 → 階級別グループ化)
|
|
829
834
|
if (info.areas.length > 0) {
|
|
830
835
|
buf.push((0, formatter_1.frameDivider)(level, width));
|
|
831
836
|
const sorted = [...info.areas].sort((a, b) => (0, formatter_1.lgIntToNumeric)(b.maxLgInt) - (0, formatter_1.lgIntToNumeric)(a.maxLgInt));
|
|
@@ -833,13 +838,25 @@ function displayLgObservationInfo(info) {
|
|
|
833
838
|
const maxObs = (0, formatter_1.getMaxObservations)();
|
|
834
839
|
const displayAreas = maxObs != null ? sorted.slice(0, maxObs) : sorted;
|
|
835
840
|
const hiddenCount = sorted.length - displayAreas.length;
|
|
841
|
+
// 長周期階級別にグループ化
|
|
842
|
+
const byLgInt = new Map();
|
|
836
843
|
for (const area of displayAreas) {
|
|
837
|
-
const
|
|
844
|
+
const key = area.maxLgInt;
|
|
845
|
+
if (!byLgInt.has(key))
|
|
846
|
+
byLgInt.set(key, []);
|
|
838
847
|
const ic = (0, formatter_1.intensityColor)(area.maxInt);
|
|
839
|
-
|
|
840
|
-
chalk_1.default.white(area.name)
|
|
841
|
-
ic(` (震度${area.maxInt})`),
|
|
848
|
+
byLgInt.get(key).push({
|
|
849
|
+
primary: chalk_1.default.white(area.name),
|
|
850
|
+
badges: [ic(` (震度${area.maxInt})`)],
|
|
851
|
+
});
|
|
842
852
|
}
|
|
853
|
+
// LgInt 降順でソート
|
|
854
|
+
const sortedEntries = [...byLgInt.entries()].sort((a, b) => (0, formatter_1.lgIntToNumeric)(b[0]) - (0, formatter_1.lgIntToNumeric)(a[0]));
|
|
855
|
+
const groups = sortedEntries.map(([lgInt, items]) => ({
|
|
856
|
+
prefix: (0, formatter_1.lgIntensityColor)(lgInt)(`長周期${lgInt}: `),
|
|
857
|
+
items,
|
|
858
|
+
}));
|
|
859
|
+
(0, formatter_1.renderGroupedItemList)({ level, width, groups, buf });
|
|
843
860
|
if (hiddenCount > 0) {
|
|
844
861
|
buf.push((0, formatter_1.frameLine)(level, chalk_1.default.gray(`... 他 ${hiddenCount} 地点`), width));
|
|
845
862
|
}
|
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
|
}
|