@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,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parse = parse;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const MAX_DEPTH = 32;
|
|
6
|
+
class Parser {
|
|
7
|
+
tokens;
|
|
8
|
+
source;
|
|
9
|
+
pos = 0;
|
|
10
|
+
depth = 0;
|
|
11
|
+
constructor(tokens, source) {
|
|
12
|
+
this.tokens = tokens;
|
|
13
|
+
this.source = source;
|
|
14
|
+
}
|
|
15
|
+
parse() {
|
|
16
|
+
const ast = this.parseOr();
|
|
17
|
+
if (this.current().kind !== "eof") {
|
|
18
|
+
throw new errors_1.FilterSyntaxError(this.source, this.current().pos, "で予期しないトークン");
|
|
19
|
+
}
|
|
20
|
+
return ast;
|
|
21
|
+
}
|
|
22
|
+
parseOr() {
|
|
23
|
+
const children = [this.parseAnd()];
|
|
24
|
+
while (this.current().kind === "or") {
|
|
25
|
+
this.advance();
|
|
26
|
+
children.push(this.parseAnd());
|
|
27
|
+
}
|
|
28
|
+
return children.length === 1 ? children[0] : { kind: "or", children };
|
|
29
|
+
}
|
|
30
|
+
parseAnd() {
|
|
31
|
+
const children = [this.parseUnary()];
|
|
32
|
+
while (this.current().kind === "and") {
|
|
33
|
+
this.advance();
|
|
34
|
+
children.push(this.parseUnary());
|
|
35
|
+
}
|
|
36
|
+
return children.length === 1 ? children[0] : { kind: "and", children };
|
|
37
|
+
}
|
|
38
|
+
parseUnary() {
|
|
39
|
+
if (this.current().kind === "not") {
|
|
40
|
+
this.advance();
|
|
41
|
+
this.enterDepth();
|
|
42
|
+
const operand = this.parseUnary();
|
|
43
|
+
this.leaveDepth();
|
|
44
|
+
return { kind: "not", operand };
|
|
45
|
+
}
|
|
46
|
+
return this.parsePrimary();
|
|
47
|
+
}
|
|
48
|
+
parsePrimary() {
|
|
49
|
+
if (this.current().kind === "lparen") {
|
|
50
|
+
this.advance();
|
|
51
|
+
this.enterDepth();
|
|
52
|
+
const expr = this.parseOr();
|
|
53
|
+
this.leaveDepth();
|
|
54
|
+
this.expect("rparen", "で閉じ括弧 ')' が必要");
|
|
55
|
+
return expr;
|
|
56
|
+
}
|
|
57
|
+
const left = this.parseValue();
|
|
58
|
+
const op = this.tryCompOp();
|
|
59
|
+
if (op == null) {
|
|
60
|
+
return { kind: "truthy", value: left };
|
|
61
|
+
}
|
|
62
|
+
const right = this.parseValue();
|
|
63
|
+
return { kind: "comparison", left, op, right };
|
|
64
|
+
}
|
|
65
|
+
parseValue() {
|
|
66
|
+
const token = this.current();
|
|
67
|
+
switch (token.kind) {
|
|
68
|
+
case "ident":
|
|
69
|
+
this.advance();
|
|
70
|
+
return { kind: "path", segments: token.value.split("."), pos: token.pos };
|
|
71
|
+
case "string":
|
|
72
|
+
this.advance();
|
|
73
|
+
return { kind: "string", value: token.value, pos: token.pos };
|
|
74
|
+
case "number":
|
|
75
|
+
this.advance();
|
|
76
|
+
return { kind: "number", value: Number(token.value), pos: token.pos };
|
|
77
|
+
case "boolean":
|
|
78
|
+
this.advance();
|
|
79
|
+
return { kind: "boolean", value: token.value === "true", pos: token.pos };
|
|
80
|
+
case "null":
|
|
81
|
+
this.advance();
|
|
82
|
+
return { kind: "null", pos: token.pos };
|
|
83
|
+
case "lbracket":
|
|
84
|
+
return this.parseList();
|
|
85
|
+
default:
|
|
86
|
+
throw new errors_1.FilterSyntaxError(this.source, token.pos, "で値が必要");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
parseList() {
|
|
90
|
+
const start = this.current().pos;
|
|
91
|
+
this.expect("lbracket", "で '[' が必要");
|
|
92
|
+
const items = [];
|
|
93
|
+
while (this.current().kind !== "rbracket") {
|
|
94
|
+
if (items.length > 0) {
|
|
95
|
+
this.expect("comma", "で ',' が必要");
|
|
96
|
+
}
|
|
97
|
+
items.push(this.parseValue());
|
|
98
|
+
}
|
|
99
|
+
this.expect("rbracket", "で ']' が必要");
|
|
100
|
+
return { kind: "list", items, pos: start };
|
|
101
|
+
}
|
|
102
|
+
tryCompOp() {
|
|
103
|
+
const token = this.current();
|
|
104
|
+
if (token.kind === "op") {
|
|
105
|
+
this.advance();
|
|
106
|
+
return token.value;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
current() {
|
|
111
|
+
return this.tokens[this.pos];
|
|
112
|
+
}
|
|
113
|
+
advance() {
|
|
114
|
+
const token = this.tokens[this.pos];
|
|
115
|
+
if (this.pos < this.tokens.length - 1)
|
|
116
|
+
this.pos++;
|
|
117
|
+
return token;
|
|
118
|
+
}
|
|
119
|
+
expect(kind, errorMsg) {
|
|
120
|
+
if (this.current().kind !== kind) {
|
|
121
|
+
throw new errors_1.FilterSyntaxError(this.source, this.current().pos, errorMsg);
|
|
122
|
+
}
|
|
123
|
+
this.advance();
|
|
124
|
+
}
|
|
125
|
+
enterDepth() {
|
|
126
|
+
this.depth++;
|
|
127
|
+
if (this.depth > MAX_DEPTH) {
|
|
128
|
+
throw new errors_1.FilterSyntaxError(this.source, this.current().pos, `でネストが深すぎる (最大 ${MAX_DEPTH} 段)`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
leaveDepth() {
|
|
132
|
+
this.depth--;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function parse(tokens, source) {
|
|
136
|
+
return new Parser(tokens, source).parse();
|
|
137
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LG_INT_RANK = exports.INTENSITY_RANK = exports.FRAME_LEVEL_RANK = void 0;
|
|
4
|
+
exports.toFrameLevelRank = toFrameLevelRank;
|
|
5
|
+
exports.toIntensityRank = toIntensityRank;
|
|
6
|
+
exports.toLgIntRank = toLgIntRank;
|
|
7
|
+
/** frameLevel → 数値ランク (順序比較用) */
|
|
8
|
+
exports.FRAME_LEVEL_RANK = {
|
|
9
|
+
cancel: 0,
|
|
10
|
+
info: 1,
|
|
11
|
+
normal: 2,
|
|
12
|
+
warning: 3,
|
|
13
|
+
critical: 4,
|
|
14
|
+
};
|
|
15
|
+
/** 震度文字列 → 数値ランク (順序比較用) */
|
|
16
|
+
exports.INTENSITY_RANK = {
|
|
17
|
+
"1": 1, "2": 2, "3": 3, "4": 4,
|
|
18
|
+
"5-": 5, "5弱": 5, "5+": 6, "5強": 6,
|
|
19
|
+
"6-": 7, "6弱": 7, "6+": 8, "6強": 8, "7": 9,
|
|
20
|
+
};
|
|
21
|
+
/** 長周期地震動階級 → 数値ランク */
|
|
22
|
+
exports.LG_INT_RANK = {
|
|
23
|
+
"0": 0, "1": 1, "2": 2, "3": 3, "4": 4,
|
|
24
|
+
};
|
|
25
|
+
/** 文字列からランク値を返す。未知の値は null */
|
|
26
|
+
function toFrameLevelRank(value) {
|
|
27
|
+
return exports.FRAME_LEVEL_RANK[value] ?? null;
|
|
28
|
+
}
|
|
29
|
+
function toIntensityRank(value) {
|
|
30
|
+
return exports.INTENSITY_RANK[value.replace(/\s+/g, "")] ?? null;
|
|
31
|
+
}
|
|
32
|
+
function toLgIntRank(value) {
|
|
33
|
+
return exports.LG_INT_RANK[value] ?? null;
|
|
34
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tokenize = tokenize;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const KEYWORDS = {
|
|
6
|
+
and: "and",
|
|
7
|
+
or: "or",
|
|
8
|
+
not: "not",
|
|
9
|
+
true: "boolean",
|
|
10
|
+
false: "boolean",
|
|
11
|
+
null: "null",
|
|
12
|
+
in: "op",
|
|
13
|
+
contains: "op",
|
|
14
|
+
};
|
|
15
|
+
const OPERATORS = ["!=", "<=", ">=", "!~", "=", "<", ">", "~"];
|
|
16
|
+
function tokenize(source) {
|
|
17
|
+
const tokens = [];
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < source.length) {
|
|
20
|
+
// 空白スキップ
|
|
21
|
+
if (/\s/.test(source[i])) {
|
|
22
|
+
i++;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
// 文字列リテラル
|
|
26
|
+
if (source[i] === '"' || source[i] === "'") {
|
|
27
|
+
const quote = source[i];
|
|
28
|
+
const start = i;
|
|
29
|
+
i++; // 開き引用符
|
|
30
|
+
let value = "";
|
|
31
|
+
while (i < source.length && source[i] !== quote) {
|
|
32
|
+
if (source[i] === "\\" && i + 1 < source.length) {
|
|
33
|
+
i++;
|
|
34
|
+
value += source[i];
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
value += source[i];
|
|
38
|
+
}
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
if (i >= source.length) {
|
|
42
|
+
throw new errors_1.FilterSyntaxError(source, start, "で文字列が閉じられていない");
|
|
43
|
+
}
|
|
44
|
+
i++; // 閉じ引用符
|
|
45
|
+
tokens.push({ kind: "string", value, pos: start });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// 数値リテラル
|
|
49
|
+
if (/[0-9]/.test(source[i]) || (source[i] === "-" && i + 1 < source.length && /[0-9]/.test(source[i + 1]))) {
|
|
50
|
+
const start = i;
|
|
51
|
+
if (source[i] === "-")
|
|
52
|
+
i++;
|
|
53
|
+
while (i < source.length && /[0-9]/.test(source[i]))
|
|
54
|
+
i++;
|
|
55
|
+
if (i < source.length && source[i] === ".") {
|
|
56
|
+
i++;
|
|
57
|
+
while (i < source.length && /[0-9]/.test(source[i]))
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
tokens.push({ kind: "number", value: source.slice(start, i), pos: start });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// 括弧・ブラケット・カンマ
|
|
64
|
+
if (source[i] === "(") {
|
|
65
|
+
tokens.push({ kind: "lparen", value: "(", pos: i });
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (source[i] === ")") {
|
|
70
|
+
tokens.push({ kind: "rparen", value: ")", pos: i });
|
|
71
|
+
i++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (source[i] === "[") {
|
|
75
|
+
tokens.push({ kind: "lbracket", value: "[", pos: i });
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (source[i] === "]") {
|
|
80
|
+
tokens.push({ kind: "rbracket", value: "]", pos: i });
|
|
81
|
+
i++;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (source[i] === ",") {
|
|
85
|
+
tokens.push({ kind: "comma", value: ",", pos: i });
|
|
86
|
+
i++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// 演算子
|
|
90
|
+
let matchedOp = null;
|
|
91
|
+
for (const op of OPERATORS) {
|
|
92
|
+
if (source.startsWith(op, i)) {
|
|
93
|
+
matchedOp = op;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (matchedOp != null) {
|
|
98
|
+
tokens.push({ kind: "op", value: matchedOp, pos: i });
|
|
99
|
+
i += matchedOp.length;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// 識別子 / キーワード (dot パス含む)
|
|
103
|
+
if (/[a-zA-Z_]/.test(source[i])) {
|
|
104
|
+
const start = i;
|
|
105
|
+
while (i < source.length && /[a-zA-Z0-9_.]/.test(source[i]))
|
|
106
|
+
i++;
|
|
107
|
+
const word = source.slice(start, i);
|
|
108
|
+
const keyword = KEYWORDS[word];
|
|
109
|
+
if (keyword != null) {
|
|
110
|
+
tokens.push({ kind: keyword, value: word, pos: start });
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
tokens.push({ kind: "ident", value: word, pos: start });
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
throw new errors_1.FilterSyntaxError(source, i, `で予期しない文字 '${source[i]}'`);
|
|
118
|
+
}
|
|
119
|
+
tokens.push({ kind: "eof", value: "", pos: source.length });
|
|
120
|
+
return tokens;
|
|
121
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.typeCheck = typeCheck;
|
|
4
|
+
const field_registry_1 = require("./field-registry");
|
|
5
|
+
const errors_1 = require("./errors");
|
|
6
|
+
/** AST を走査し、フィールド参照と演算子の型整合を検証する */
|
|
7
|
+
function typeCheck(ast, source) {
|
|
8
|
+
switch (ast.kind) {
|
|
9
|
+
case "or":
|
|
10
|
+
case "and":
|
|
11
|
+
for (const child of ast.children)
|
|
12
|
+
typeCheck(child, source);
|
|
13
|
+
break;
|
|
14
|
+
case "not":
|
|
15
|
+
typeCheck(ast.operand, source);
|
|
16
|
+
break;
|
|
17
|
+
case "truthy":
|
|
18
|
+
validateFieldExists(ast.value);
|
|
19
|
+
break;
|
|
20
|
+
case "comparison":
|
|
21
|
+
validateComparison(ast.left, ast.op, ast.right, source);
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function validateFieldExists(node) {
|
|
26
|
+
if (node.kind === "path") {
|
|
27
|
+
const name = node.segments.join(".");
|
|
28
|
+
const field = (0, field_registry_1.resolveField)(name);
|
|
29
|
+
if (field == null) {
|
|
30
|
+
throw new errors_1.FilterFieldError(name, (0, field_registry_1.fieldNames)());
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function validateComparison(left, op, right, source) {
|
|
35
|
+
// パスが左辺にある場合のフィールド検証
|
|
36
|
+
if (left.kind === "path") {
|
|
37
|
+
const name = left.segments.join(".");
|
|
38
|
+
const field = (0, field_registry_1.resolveField)(name);
|
|
39
|
+
if (field == null) {
|
|
40
|
+
throw new errors_1.FilterFieldError(name, (0, field_registry_1.fieldNames)());
|
|
41
|
+
}
|
|
42
|
+
// enum 型に対する数値リテラルチェック
|
|
43
|
+
if (field.kind === "enum:intensity" && right.kind === "number") {
|
|
44
|
+
throw new errors_1.FilterTypeError(`型が不一致: ${name} ${op} ${right.value}\n` +
|
|
45
|
+
`\`${name}\` は震度文字列("1", "5-", "6+"等)で比較する。数値リテラルは使えない`);
|
|
46
|
+
}
|
|
47
|
+
if (field.kind === "enum:lgInt" && right.kind === "number") {
|
|
48
|
+
throw new errors_1.FilterTypeError(`型が不一致: ${name} ${op} ${right.value}\n` +
|
|
49
|
+
`\`${name}\` は長周期階級文字列("0"〜"4")で比較する。数値リテラルは使えない`);
|
|
50
|
+
}
|
|
51
|
+
// 順序比較の型検証
|
|
52
|
+
if (op === "<" || op === "<=" || op === ">" || op === ">=") {
|
|
53
|
+
if (!field.supportsOrder) {
|
|
54
|
+
throw new errors_1.FilterTypeError(`\`${name}\` (${field.kind}) は順序比較に対応していない`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// regex 演算子の検証
|
|
58
|
+
if (op === "~" || op === "!~") {
|
|
59
|
+
if (field.kind !== "string" && field.kind !== "enum:frameLevel" && field.kind !== "enum:intensity" && field.kind !== "enum:lgInt") {
|
|
60
|
+
throw new errors_1.FilterTypeError(`\`${name}\` (${field.kind}) は正規表現マッチに対応していない`);
|
|
61
|
+
}
|
|
62
|
+
if (right.kind === "string") {
|
|
63
|
+
try {
|
|
64
|
+
new RegExp(right.value);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new errors_1.FilterTypeError(`正規表現が不正だ: "~" の右辺 "${right.value}" を解釈できない`);
|
|
68
|
+
}
|
|
69
|
+
if (isRedosRisk(right.value)) {
|
|
70
|
+
throw new errors_1.FilterTypeError(`正規表現が危険だ: "${right.value}" は入れ子の量指定子を含んでおり、ReDoS の恐れがある`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// in の検証: 右辺はリストでなければならない
|
|
75
|
+
if (op === "in") {
|
|
76
|
+
if (right.kind !== "list") {
|
|
77
|
+
throw new errors_1.FilterTypeError(`\`in\` の右辺にはリスト [...] が必要`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// contains の検証
|
|
81
|
+
if (op === "contains") {
|
|
82
|
+
if (field.kind !== "string[]" && field.kind !== "number[]" && field.kind !== "string") {
|
|
83
|
+
throw new errors_1.FilterTypeError(`\`${name}\` (${field.kind}) は contains に対応していない`);
|
|
84
|
+
}
|
|
85
|
+
// 右辺はリテラル (string/number) でなければならない
|
|
86
|
+
if (right.kind !== "string" && right.kind !== "number") {
|
|
87
|
+
throw new errors_1.FilterTypeError(`\`contains\` の右辺にはリテラル (文字列または数値) が必要`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// 右辺のパスも検証
|
|
92
|
+
if (right.kind === "path") {
|
|
93
|
+
validateFieldExists(right);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 入れ子の量指定子パターンを簡易検出して ReDoS リスクを判定する。
|
|
98
|
+
* `(a+)+`, `(a*)*`, `(a+)*` のように、量指定子を含むグループに
|
|
99
|
+
* さらに量指定子が付くケースを検出する。
|
|
100
|
+
*/
|
|
101
|
+
function isRedosRisk(pattern) {
|
|
102
|
+
// 量指定子(+, *, ?, {n,m})で終わるグループの直後に量指定子が来るパターン
|
|
103
|
+
return /(\+|\*|\?|\})\)(\+|\*|\?|\{)/.test(pattern);
|
|
104
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PipelineController = void 0;
|
|
4
|
+
const compile_filter_1 = require("../filter/compile-filter");
|
|
5
|
+
const compile_template_1 = require("../template/compile-template");
|
|
6
|
+
/**
|
|
7
|
+
* FilterTemplatePipeline の状態を管理する controller。
|
|
8
|
+
* REPL はこの API 経由でのみ pipeline を変更する。
|
|
9
|
+
* getPipeline() は同一オブジェクト参照を返すため、
|
|
10
|
+
* message-router 側に渡した pipeline と常に同期する。
|
|
11
|
+
*/
|
|
12
|
+
class PipelineController {
|
|
13
|
+
_pipeline;
|
|
14
|
+
_filterExpr = null;
|
|
15
|
+
_templateExpr = null;
|
|
16
|
+
_focusExpr = null;
|
|
17
|
+
constructor() {
|
|
18
|
+
this._pipeline = { filter: null, template: null, focus: null };
|
|
19
|
+
}
|
|
20
|
+
/** 同一オブジェクト参照を返す。router 側と共有される。 */
|
|
21
|
+
getPipeline() {
|
|
22
|
+
return this._pipeline;
|
|
23
|
+
}
|
|
24
|
+
// --- Filter ---
|
|
25
|
+
getFilterExpr() { return this._filterExpr; }
|
|
26
|
+
/** フィルタ式をコンパイルして設定する。無効な式の場合は例外を投げる。 */
|
|
27
|
+
setFilter(expr) {
|
|
28
|
+
const predicate = (0, compile_filter_1.compileFilter)(expr);
|
|
29
|
+
this._pipeline.filter = predicate;
|
|
30
|
+
this._filterExpr = expr;
|
|
31
|
+
}
|
|
32
|
+
clearFilter() {
|
|
33
|
+
this._pipeline.filter = null;
|
|
34
|
+
this._filterExpr = null;
|
|
35
|
+
}
|
|
36
|
+
// --- Template ---
|
|
37
|
+
getTemplateExpr() { return this._templateExpr; }
|
|
38
|
+
/** テンプレート式をコンパイルして設定する。 */
|
|
39
|
+
setTemplate(expr) {
|
|
40
|
+
const renderer = (0, compile_template_1.compileTemplate)(expr);
|
|
41
|
+
this._pipeline.template = renderer;
|
|
42
|
+
this._templateExpr = expr;
|
|
43
|
+
}
|
|
44
|
+
clearTemplate() {
|
|
45
|
+
this._pipeline.template = null;
|
|
46
|
+
this._templateExpr = null;
|
|
47
|
+
}
|
|
48
|
+
// --- Focus ---
|
|
49
|
+
getFocusExpr() { return this._focusExpr; }
|
|
50
|
+
/** フォーカス式をコンパイルして設定する。無効な式の場合は例外を投げる。 */
|
|
51
|
+
setFocus(expr) {
|
|
52
|
+
const predicate = (0, compile_filter_1.compileFilter)(expr);
|
|
53
|
+
this._pipeline.focus = predicate;
|
|
54
|
+
this._focusExpr = expr;
|
|
55
|
+
}
|
|
56
|
+
clearFocus() {
|
|
57
|
+
this._pipeline.focus = null;
|
|
58
|
+
this._focusExpr = null;
|
|
59
|
+
}
|
|
60
|
+
// --- Factory ---
|
|
61
|
+
/** 式文字列から PipelineController を構築する。null/undefined はスキップ。 */
|
|
62
|
+
static fromExpressions(opts) {
|
|
63
|
+
const ctrl = new PipelineController();
|
|
64
|
+
if (opts.filter != null)
|
|
65
|
+
ctrl.setFilter(opts.filter);
|
|
66
|
+
if (opts.template != null)
|
|
67
|
+
ctrl.setTemplate(opts.template);
|
|
68
|
+
if (opts.focus != null)
|
|
69
|
+
ctrl.setFocus(opts.focus);
|
|
70
|
+
return ctrl;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.PipelineController = PipelineController;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shouldDisplay = shouldDisplay;
|
|
4
|
+
exports.renderTemplate = renderTemplate;
|
|
5
|
+
/** PresentationEvent にフィルタを適用する。true = 表示、false = 非表示 */
|
|
6
|
+
function shouldDisplay(event, pipeline) {
|
|
7
|
+
if (pipeline.filter == null)
|
|
8
|
+
return true;
|
|
9
|
+
return pipeline.filter(event);
|
|
10
|
+
}
|
|
11
|
+
/** テンプレートが設定されていれば1行に変換する。null = テンプレートなし */
|
|
12
|
+
function renderTemplate(event, pipeline) {
|
|
13
|
+
if (pipeline.template == null)
|
|
14
|
+
return null;
|
|
15
|
+
return pipeline.template(event);
|
|
16
|
+
}
|