All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m5s
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)
87 lines
2.1 KiB
Python
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
|