Files
rose-ash/blog/bp/blog/ghost/lexical_validator.py
giles f42042ccb7
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m5s
Monorepo: consolidate 7 repos into one
Combines shared, blog, market, cart, events, federation, and account
into a single repository. Eliminates submodule sync, sibling model
copying at build time, and per-app CI orchestration.

Changes:
- Remove per-app .git, .gitmodules, .gitea, submodule shared/ dirs
- Remove stale sibling model copies from each app
- Update all 6 Dockerfiles for monorepo build context (root = .)
- Add build directives to docker-compose.yml
- Add single .gitea/workflows/ci.yml with change detection
- Add .dockerignore for monorepo build context
- Create __init__.py for federation and account (cross-app imports)
2026-02-24 19:44:17 +00:00

87 lines
2.1 KiB
Python

"""
Server-side validation for Lexical editor JSON.
Walk the document tree and reject any node whose ``type`` is not in
ALLOWED_NODE_TYPES. This is a belt-and-braces check: the Lexical
client already restricts which nodes can be created, but we validate
server-side too.
"""
from __future__ import annotations
ALLOWED_NODE_TYPES: frozenset[str] = frozenset(
{
# Standard Lexical nodes
"root",
"paragraph",
"heading",
"quote",
"list",
"listitem",
"link",
"autolink",
"code",
"code-highlight",
"linebreak",
"text",
"horizontalrule",
"image",
"tab",
# Ghost "extended-*" variants
"extended-text",
"extended-heading",
"extended-quote",
# Ghost card types
"html",
"gallery",
"embed",
"bookmark",
"markdown",
"email",
"email-cta",
"button",
"callout",
"toggle",
"video",
"audio",
"file",
"product",
"header",
"signup",
"aside",
"codeblock",
"call-to-action",
"at-link",
"paywall",
}
)
def validate_lexical(doc: dict) -> tuple[bool, str | None]:
"""Recursively validate a Lexical JSON document.
Returns ``(True, None)`` when the document is valid, or
``(False, reason)`` when an unknown node type is found.
"""
if not isinstance(doc, dict):
return False, "Document must be a JSON object"
root = doc.get("root")
if not isinstance(root, dict):
return False, "Document must contain a 'root' object"
return _walk(root)
def _walk(node: dict) -> tuple[bool, str | None]:
node_type = node.get("type")
if node_type is not None and node_type not in ALLOWED_NODE_TYPES:
return False, f"Disallowed node type: {node_type}"
for child in node.get("children", []):
if isinstance(child, dict):
ok, reason = _walk(child)
if not ok:
return False, reason
return True, None