|
|
| 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(), |
| ] |
|
|