// Timeline DSL Grammar (PEG)
file = { SOI ~ statement* ~ EOI }
statement = _{
timeline_block
| lane_decl
| group_decl
| span_decl
| event_decl
| event_range_decl
| import_block
| map_block
| template_block
| apply_block
}
// ─── Timeline Block ─────────────────────────────────────────
timeline_block = {
"timeline" ~ string_literal ~ "{" ~ timeline_prop* ~ "}"
}
timeline_prop = _{
title_prop | unit_prop | range_prop | calendar_prop | color_map_block
}
title_prop = { "title" ~ string_literal ~ ";" }
unit_prop = { "unit" ~ ident ~ ";" }
range_prop = { "range" ~ time_value ~ ".." ~ time_value ~ ";" }
calendar_prop = { "calendar" ~ ident ~ ";" }
color_map_block = { "color_map" ~ "{" ~ color_map_entry* ~ "}" }
color_map_entry = { ident ~ ":" ~ string_literal ~ ";" }
// ─── Lane Declaration ───────────────────────────────────────
lane_decl = {
"lane" ~ string_literal ~ ("as" ~ ident)? ~ "{" ~ lane_prop* ~ "}"
}
lane_prop = _{
kind_prop | order_prop
}
kind_prop = { "kind" ~ ident ~ ";" }
order_prop = { "order" ~ integer ~ ";" }
// ─── Group Declaration ──────────────────────────────────────
group_decl = {
"group" ~ string_literal ~ "{" ~ lane_decl+ ~ "}"
}
// ─── Span Declaration ───────────────────────────────────────
span_decl = {
"span" ~ ident ~ time_value ~ ".." ~ time_value ~ string_literal ~ block_options ~ ";"
}
// ─── Event Declaration ──────────────────────────────────────
event_decl = {
"event" ~ ident ~ time_value ~ string_literal ~ block_options ~ ";"
}
// ─── Event Range Declaration ────────────────────────────────
event_range_decl = {
"event_range" ~ ident ~ time_value ~ ".." ~ time_value ~ string_literal ~ block_options ~ ";"
}
// ─── Block Options (shared by span/event/event_range) ───────
block_options = { "{" ~ item_option* ~ "}" }
item_option = _{
tags_option | source_option | id_option | origin_option
}
tags_option = { "tags" ~ "[" ~ string_list ~ "]" ~ ";" }
source_option = { "source" ~ source_ref ~ ";" }
id_option = { "id" ~ string_literal ~ ";" }
origin_option = { "origin" ~ ident ~ ";" }
// ─── Import Block ───────────────────────────────────────────
import_block = {
"import" ~ source_name ~ ("as" ~ ident)? ~ "{" ~ import_item* ~ "}"
}
source_name = { ident }
import_item = _{
entity_import | query_import | field_priority_policy | policy_import
}
entity_import = { "entity" ~ qid ~ ("as" ~ ident)? ~ ";" }
query_import = { "query" ~ string_literal ~ ("as" ~ ident)? ~ ";" }
policy_import = { "policy" ~ ident ~ ";" }
field_priority_policy = { "policy" ~ "field_priority" ~ "{" ~ field_strategy_decl* ~ "}" }
field_strategy_decl = _{
label_strategy | time_strategy | tags_strategy
}
label_strategy = { "label" ~ ":" ~ field_strategy_value ~ ";" }
time_strategy = { "time" ~ ":" ~ field_strategy_value ~ ";" }
tags_strategy = { "tags" ~ ":" ~ field_strategy_value ~ ";" }
field_strategy_value = { "manual" | "wikidata" | "merge" }
// ─── Map Block ──────────────────────────────────────────────
map_block = {
"map" ~ dotted_ident ~ "to" ~ target_type ~ "{" ~ map_prop* ~ "}"
}
target_type = { ident }
map_prop = _{
map_lane | map_start | map_end | map_time | map_label | map_tags | map_filter | map_expand
}
map_lane = { "lane" ~ ident ~ ";" }
map_start = { "start" ~ map_expr ~ ";" }
map_end = { "end" ~ map_expr ~ ";" }
map_time = { "time" ~ map_expr ~ ";" }
map_label = { "label" ~ label_expr ~ ";" }
map_tags = { "tags" ~ "[" ~ string_list ~ "]" ~ ";" }
map_filter = { "filter" ~ filter_expr ~ ";" }
map_expand = { "expand" ~ claim_call ~ ";" }
// ─── Filter Expressions (for `filter` clause in map blocks) ─
// Precedence (low → high): || , && , ! , compare / string_op
filter_expr = { filter_or }
filter_or = { filter_and ~ ("||" ~ filter_and)* }
filter_and = { filter_not ~ ("&&" ~ filter_not)* }
filter_not = { ("!" ~ filter_atom) | filter_atom }
filter_atom = _{ filter_paren | filter_string_op | filter_compare }
filter_paren = { "(" ~ filter_expr ~ ")" }
filter_compare = { filter_operand ~ compare_op ~ filter_operand }
filter_string_op = { label_ref ~ string_match_op ~ string_literal }
// Longer operators must come first for PEG longest-match.
compare_op = { ">=" | "<=" | "==" | "!=" | ">" | "<" }
string_match_op = { "startswith" | "contains" }
filter_operand = _{ null_literal | claim_expr | integer }
null_literal = @{ "null" ~ !(ASCII_ALPHANUMERIC | "_") }
// ─── Template Block ─────────────────────────────────────────
template_block = {
"template" ~ string_literal ~ ("as" ~ ident)?
~ "to" ~ target_type ~ "{" ~ map_prop* ~ "}"
}
// ─── Apply Block ────────────────────────────────────────────
apply_block = {
"apply" ~ ident ~ "to" ~ ident ~ "{" ~ apply_override* ~ "}"
}
apply_override = _{
map_lane
}
// ─── Expressions ────────────────────────────────────────────
map_expr = { map_operand ~ ("??" ~ map_operand)* }
map_operand = _{ claim_expr | integer }
claim_offset = @{ ("+" | "-") ~ ASCII_DIGIT+ }
claim_expr = { claim_call ~ claim_qualifier? ~ claim_accessor? ~ claim_offset? }
claim_qualifier = { "." ~ "qualifier" ~ "(" ~ property_id ~ ")" }
claim_accessor = { "." ~ ident }
claim_call = { "claim" ~ "(" ~ property_id ~ ")" }
label_expr = { label_ref ~ ("??" ~ label_ref)* }
label_ref = ${ "label" ~ "@" ~ ident }
// ─── Primitives ─────────────────────────────────────────────
integer = @{ "-"? ~ ASCII_DIGIT+ }
qid = @{ "Q" ~ ASCII_DIGIT+ }
property_id = @{ "P" ~ ASCII_DIGIT+ }
// time literal: YYYY-MM-DD / YYYY-MM / YYYY[Y...] (negative years allowed only at year precision)
// year_pos は YYYY-MM 形式専用なので 4 桁まで(仕様 §1.2)。
// year_lit は桁数制限なし(後方互換: 既存の `range 0..10000;` 等を許容するため)。
year_pos = @{ ASCII_DIGIT{1,4} }
year_lit = @{ "-"? ~ ASCII_DIGIT+ }
year_month_lit = ${ year_pos ~ "-" ~ ASCII_DIGIT{2} ~ !("-" ~ ASCII_DIGIT) }
date_lit = ${ year_pos ~ "-" ~ ASCII_DIGIT{2} ~ "-" ~ ASCII_DIGIT{2} }
time_value = { date_lit | year_month_lit | year_lit }
// 単独の時刻リテラルを行全体に対してパースするためのエントリポイント。
// `tdsl-parser::parse_time_literal` から CSV 経路 (#260) などで再利用される。
time_literal_only = { SOI ~ time_value ~ EOI }
ident = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | "-")* }
dotted_ident = @{ ident ~ ("." ~ ident)+ }
source_ref = ${ ident ~ ":" ~ qid }
string_literal = ${ "\"" ~ string_inner ~ "\"" }
string_inner = @{ (!"\"" ~ !"\\" ~ ANY | "\\" ~ ANY)* }
string_list = { string_literal ~ ("," ~ string_literal)* }
// ─── Whitespace & Comments ──────────────────────────────────
WHITESPACE = _{ " " | "\t" | "\r" | "\n" }
COMMENT = _{ "//" ~ (!"\n" ~ ANY)* | "/*" ~ (!"*/" ~ ANY)* ~ "*/" }