|
|
|
|
|
from dataclasses import dataclass, field |
|
|
from typing import Literal, List, Dict, Any, Callable, Optional |
|
|
|
|
|
BlockType = Literal["plain","sequence","grouped","scheduled","alternate","alternate_distinct","numbered","and_rule","top_level"] |
|
|
|
|
|
@dataclass |
|
|
class FieldSpec: |
|
|
name: str |
|
|
kind: Literal["text","number","choice","list","boolean"] = "text" |
|
|
required: bool = True |
|
|
choices: Optional[List[str]] = None |
|
|
default: Any = None |
|
|
help: str = "" |
|
|
|
|
|
@dataclass |
|
|
class Constraint: |
|
|
check: Callable[[Dict[str, Any]], bool] |
|
|
message: str |
|
|
|
|
|
@dataclass |
|
|
class FormatBlueprint: |
|
|
id: str |
|
|
title: str |
|
|
block_type: BlockType |
|
|
fields: List[FieldSpec] = field(default_factory=list) |
|
|
assemble: Callable[[Dict[str, Any]], str] = lambda v: v.get("text","") |
|
|
constraints: List[Constraint] = field(default_factory=list) |
|
|
|
|
|
def _pairs_from_list(v) -> List[Dict[str, str]]: |
|
|
out = [] |
|
|
for x in v or []: |
|
|
if isinstance(x, dict) and "key" in x and "val" in x: |
|
|
out.append({"key": str(x["key"]), "val": str(x["val"])}) |
|
|
return out |
|
|
|
|
|
def bp_top_level(): |
|
|
def assemble(v): |
|
|
owner = (v.get("owner") or "").strip() |
|
|
pairs = _pairs_from_list(v.get("pairs")) |
|
|
body = " ".join([f":: {p['key'].strip()} :: {p['val'].strip()} ;" for p in pairs]) |
|
|
return f"{owner} {body} !!".strip() |
|
|
return FormatBlueprint( |
|
|
id="top_level_simple", |
|
|
title="Top-level (owner :: key :: val ; ... !!)", |
|
|
block_type="top_level", |
|
|
fields=[ |
|
|
FieldSpec("owner", help="Например: portrait"), |
|
|
FieldSpec("pairs", kind="list", help="Список пар key/val"), |
|
|
], |
|
|
assemble=assemble, |
|
|
constraints=[ |
|
|
Constraint(lambda v: bool((v.get("owner") or "").strip()), "owner пуст"), |
|
|
], |
|
|
) |
|
|
|
|
|
def bp_sequence(): |
|
|
def assemble(v): |
|
|
items = [str(x).strip() for x in (v.get("items") or []) if str(x).strip()] |
|
|
if not items: |
|
|
return "" |
|
|
inner = " , ".join(items) |
|
|
return f"{{ {inner} }} ;" |
|
|
return FormatBlueprint( |
|
|
id="sequence_simple", |
|
|
title="Sequence { a , b , c } ;", |
|
|
block_type="sequence", |
|
|
fields=[FieldSpec("items", kind="list", help="Список элементов")], |
|
|
assemble=assemble, |
|
|
constraints=[Constraint(lambda v: len(v.get("items") or [])>=1, "Нужно >=1 элемента")], |
|
|
) |
|
|
|
|
|
def bp_numbered(): |
|
|
def assemble(v): |
|
|
n = int(v.get("count") or 1) |
|
|
marker = str(v.get("marker") or "").strip() |
|
|
options = [str(x).strip() for x in (v.get("options") or []) if str(x).strip()] |
|
|
inside = " | ".join(options) |
|
|
return f"{n}{marker} {{ {inside} }}" |
|
|
return FormatBlueprint( |
|
|
id="numbered_simple", |
|
|
title="Numbered (N[!|_] { a | b | c })", |
|
|
block_type="numbered", |
|
|
fields=[ |
|
|
FieldSpec("count", kind="number", default=3, help="Сколько выбрать"), |
|
|
FieldSpec("marker", kind="choice", choices=["", "!", "_"], default="!"), |
|
|
FieldSpec("options", kind="list", help="Опции в { }"), |
|
|
], |
|
|
assemble=assemble, |
|
|
constraints=[ |
|
|
Constraint(lambda v: int(v.get("count") or 0)>=1, "count >= 1"), |
|
|
Constraint(lambda v: len(v.get("options") or [])>=1, "Требуется >=1 опции"), |
|
|
], |
|
|
) |
|
|
|
|
|
def bp_alternate(): |
|
|
def assemble(v): |
|
|
opts = [str(x).strip() for x in (v.get("options") or []) if str(x).strip()] |
|
|
body = " | ".join(opts) |
|
|
distinct = bool(v.get("distinct") or False) |
|
|
return f"[ {body} ]!" if distinct else f"[ {body} ]" |
|
|
return FormatBlueprint( |
|
|
id="alternate_simple", |
|
|
title="Alternate [ a | b | c ] (! для distinct)", |
|
|
block_type="alternate", |
|
|
fields=[ |
|
|
FieldSpec("options", kind="list", help="Опции в []"), |
|
|
FieldSpec("distinct", kind="boolean", default=False, help="Добавить !"), |
|
|
], |
|
|
assemble=assemble, |
|
|
constraints=[Constraint(lambda v: len(v.get("options") or [])>=1, "Требуется >=1 опции")], |
|
|
) |
|
|
|
|
|
def bp_scheduled(): |
|
|
|
|
|
def assemble(v): |
|
|
parts = [str(x).strip() for x in (v.get("parts") or []) if str(x).strip()] |
|
|
tail = (v.get("tail") or "").strip() |
|
|
return f"[ {' : '.join(parts)} ] : {tail}" |
|
|
return FormatBlueprint( |
|
|
id="scheduled_simple", |
|
|
title="Scheduled [ a : b : ... ] : 10-50%", |
|
|
block_type="scheduled", |
|
|
fields=[ |
|
|
FieldSpec("parts", kind="list", help="Секции внутри [] через ':'"), |
|
|
FieldSpec("tail", kind="text", default="50", help="Число/диапазон/проценты"), |
|
|
], |
|
|
assemble=assemble, |
|
|
constraints=[Constraint(lambda v: len(v.get('parts') or [])>=1, "Нужна >=1 секция")], |
|
|
) |
|
|
|
|
|
def get_blueprints(): |
|
|
return [ |
|
|
bp_top_level(), |
|
|
bp_sequence(), |
|
|
bp_numbered(), |
|
|
bp_alternate(), |
|
|
bp_scheduled(), |
|
|
] |
|
|
|