Spaces:
Running
Running
Download editor/builder.py from acecalisto3/SignalMesh: direct link, hf CLI and curl.
- Browser
- Download file 8.87 kB
-
https://huggingface.co/spaces/acecalisto3/SignalMesh/resolve/main/editor/builder.py
- Command line
-
hf download hf://spaces/acecalisto3/SignalMesh/editor/builder.py
-
curl -L -o builder.py https://huggingface.co/spaces/acecalisto3/SignalMesh/resolve/main/editor/builder.py
8.87 kB
| """Building new YOOtheme layout, rather than only editing what is already there. | |
| The editor could read a layout, rewrite any prop and move whole sections, but it | |
| could not *make* anything: a new section was a clone of an existing one, and | |
| there was no way to add an element at all. This is the missing half. | |
| Two sources of truth, neither of them invented here: | |
| * the element shapes are YOOtheme's own — `type` plus a `props` dict, sections | |
| holding rows holding columns holding elements. Get the nesting wrong and the | |
| builder renders an empty band, so `place` refuses it rather than writing it. | |
| * the presets are real sections lifted whole from sites that shipped. A preset | |
| beats a constructor for anything with more than a couple of elements in it, | |
| which is why the catalogue leads with them. | |
| Everything returns a plain node. Nothing here writes; the caller splices and | |
| saves through the one write path, same as every other edit. | |
| """ | |
| import json | |
| from pathlib import Path | |
| import vocabulary | |
| PRESETS = Path(__file__).parent / "presets" | |
| # What may contain what. The builder's own contract, and the reason a section | |
| # full of elements with no column renders as nothing at all. | |
| CONTAINS = { | |
| "layout": {"section"}, | |
| "section": {"row"}, | |
| "row": {"column"}, | |
| # A column takes any element, and also a row — nested grids are ordinary | |
| # YOOtheme, which the shipped hero presets use for their button rows. What it | |
| # cannot hold is a section or another column. | |
| "column": None, | |
| } | |
| NOT_IN_COLUMN = {"section", "column", "layout"} | |
| WIDTHS = {1: ["1-1"], 2: ["1-2", "1-2"], 3: ["1-3", "1-3", "1-3"], | |
| 4: ["1-4", "1-4", "1-4", "1-4"]} | |
| def section(props=None, children=None): | |
| return {"type": "section", "props": props or {}, "children": children or []} | |
| def row(props=None, children=None): | |
| return {"type": "row", "props": props or {"gutter": "medium"}, "children": children or []} | |
| def column(width="1-1", props=None, children=None): | |
| props = dict(props or {}) | |
| props.setdefault("width_default", width) | |
| return {"type": "column", "props": props, "children": children or []} | |
| def headline(content, element="h2", style="heading-large"): | |
| return {"type": "headline", "props": {"content": content, | |
| "title_element": element, | |
| "title_style": style}} | |
| def text(content, cls=""): | |
| props = {"content": content} | |
| if cls: | |
| props["class"] = cls | |
| return {"type": "text", "props": props} | |
| def button(label, link="#", style="primary"): | |
| return {"type": "button", "props": {"content": label, "link": link, | |
| "button_style": style}} | |
| def image(src, alt=""): | |
| return {"type": "image", "props": {"image": src, "image_alt": alt}} | |
| def panel(title, content, style="card-default"): | |
| return {"type": "panel", "props": {"title": title, "content": content, | |
| "panel_style": style, "hover": True}} | |
| def divider(): | |
| return {"type": "divider", "props": {}} | |
| ELEMENTS = {"headline": headline, "text": text, "button": button, | |
| "image": image, "panel": panel, "divider": divider} | |
| def grid(columns, gutter="medium", widths=None): | |
| """A row of columns from a list of children-lists. Widths default to even.""" | |
| widths = widths or WIDTHS.get(len(columns), ["1-1"] * len(columns)) | |
| return row({"gutter": gutter}, | |
| [column(widths[i] if i < len(widths) else "1-1", children=kids) | |
| for i, kids in enumerate(columns)]) | |
| def element(kind, **props): | |
| """One element by name, with the constructor's own defaults.""" | |
| if kind not in ELEMENTS: | |
| raise ValueError(f"unknown element {kind!r} — have: {', '.join(sorted(ELEMENTS))}") | |
| make = ELEMENTS[kind] | |
| try: | |
| return make(**props) | |
| except TypeError as e: | |
| raise ValueError(f"{kind}: {e}") from None | |
| # ── presets ─────────────────────────────────────────────────────────────────── | |
| def _manifest(): | |
| path = PRESETS / "presets.manifest.json" | |
| if not path.exists(): | |
| return {} | |
| try: | |
| return json.loads(path.read_text()) | |
| except (ValueError, OSError): | |
| return {} | |
| def catalogue(): | |
| """Everything that can be built, described. This is what a builder needs to | |
| know before it starts, and it is generated from what is actually on disk — | |
| a preset listed but missing is worse than one that was never offered. | |
| `constructors` are what this module can build directly. `known` is every | |
| element proven by a real layout, with its real prop keys, which `edit_page` | |
| can write even where there is no constructor. `documented` is named by | |
| YOOtheme but never yet seen, so its slug is unverified — look for it, do not | |
| assume it.""" | |
| man = _manifest() | |
| out = {"elements": sorted(ELEMENTS), | |
| "constructors": sorted(ELEMENTS), | |
| "known": {k: vocabulary.props(k) for k in vocabulary.confirmed()}, | |
| "documented": sorted(vocabulary.documented()), | |
| "sections": [], "templates": []} | |
| for group, key in (("sections", "sections"), ("templates", "full_templates")): | |
| for entry in man.get(key) or []: | |
| if (PRESETS / entry.get("file", "")).exists(): | |
| out[group].append({"name": entry.get("name"), | |
| "category": entry.get("category", ""), | |
| "description": entry.get("description", "")}) | |
| return out | |
| def preset(name): | |
| """A preset section, fresh each time — callers mutate what they get back.""" | |
| man = _manifest() | |
| for key in ("sections", "full_templates"): | |
| for entry in man.get(key) or []: | |
| if entry.get("name") == name: | |
| path = PRESETS / entry["file"] | |
| if not path.exists(): | |
| raise KeyError(f"preset {name!r} is listed but its file is missing") | |
| return json.loads(path.read_text()) | |
| raise KeyError(f"no preset {name!r}") | |
| # ── placement ───────────────────────────────────────────────────────────────── | |
| def check(parent, node): | |
| """Whether `node` may live inside `parent`. Refusing here is the point. | |
| A headline dropped straight into a section is not an error YOOtheme reports | |
| — it renders nothing, and the operator is left looking at a blank band | |
| wondering which of their changes broke the page. | |
| """ | |
| if parent.get("type") not in CONTAINS: | |
| # A composite element — a button holding button_items, an accordion | |
| # holding panels, a gallery holding images. Its children are its own | |
| # contract, not this one's, and inventing a rule for an element we do not | |
| # model would reject layouts YOOtheme renders perfectly well. | |
| return True | |
| allowed = CONTAINS[parent["type"]] | |
| if allowed is None: # a column | |
| if node.get("type") in NOT_IN_COLUMN: | |
| raise ValueError(f"a {node.get('type')} cannot go inside a column") | |
| return True | |
| kind = node.get("type") | |
| if kind not in allowed: | |
| raise ValueError( | |
| f"a {kind} cannot go directly inside a {parent.get('type')} — " | |
| f"that holds {', '.join(sorted(allowed))}") | |
| return True | |
| def place(parent, node, index=None): | |
| """Insert a node into a parent, at the end unless told otherwise.""" | |
| check(parent, node) | |
| kids = parent.setdefault("children", []) | |
| kids.insert(len(kids) if index is None else max(0, min(index, len(kids))), node) | |
| return node | |
| def move(parent, from_index, to_index): | |
| """Reorder within one parent — the sibling move the section planner never | |
| covered, because it only ever rebuilt the top level.""" | |
| kids = parent.get("children") or [] | |
| if not 0 <= from_index < len(kids): | |
| raise ValueError(f"no child at index {from_index}") | |
| node = kids.pop(from_index) | |
| kids.insert(max(0, min(to_index, len(kids))), node) | |
| return node | |
| def wrap(node): | |
| """Give a bare element the row and column it needs to render. | |
| Convenience with a purpose: the common request is "add a button to this | |
| section", and the honest answer is that a button needs a column, so build | |
| the column rather than refusing the request. | |
| """ | |
| if node.get("type") == "section": | |
| return node | |
| if node.get("type") == "row": | |
| return section(children=[node]) | |
| if node.get("type") == "column": | |
| return section(children=[row(children=[node])]) | |
| return section(children=[row(children=[column(children=[node])])]) | |