tdsl-parser 1.18.0

PEG parser for the Timeline DSL (.tdsl) format
Documentation
// 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)* ~ "*/" }