diff --git a/doc/book/meson.build b/doc/book/meson.build index fa8f1dbb..69fc13fa 100644 --- a/doc/book/meson.build +++ b/doc/book/meson.build @@ -8,6 +8,7 @@ srcs = files( 'src/Message_XML.md', 'src/Architecture.md', 'src/Introduction.md', + 'src/Specification.md', 'src/images/content-updates/sync-to-desync-subsurf-3.png', 'src/images/content-updates/sync-subsurf-case2-2.png', 'src/images/content-updates/simple-synchronized-state-4.png', diff --git a/doc/book/protocol-to-markdown.py b/doc/book/protocol-to-markdown.py new file mode 100755 index 00000000..34f73eb5 --- /dev/null +++ b/doc/book/protocol-to-markdown.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 + +import sys +from xml.dom import Node +from xml.dom.minidom import parse + +if len(sys.argv) != 2: + print("usage: protocol-to-markdown.py ") + sys.exit(1) +filename = sys.argv[1] + +doc = parse(filename) +assert doc.documentElement.tagName == "protocol" + +def parse_protocol(node): + protocol = { + "copyright": None, + "summary": None, + "description": None, + "interfaces": [], + } + for child in filter(is_element, node.childNodes): + match child.tagName: + case "copyright": + protocol["copyright"] = parse_text(child) + case "description": + protocol |= parse_description(child) + case "interface": + protocol["interfaces"].append(parse_interface(child)) + return protocol + +def parse_interface(node): + interface = { + "name": node.getAttribute("name"), + "version": node.getAttribute("version"), + "frozen": node.getAttribute("frozen") == "true", + "summary": None, + "description": None, + "requests": [], + "events": [], + "enums": [], + } + for child in filter(is_element, node.childNodes): + match child.tagName: + case "description": + interface |= parse_description(child) + case "request": + interface["requests"].append(parse_message(child)) + case "event": + interface["events"].append(parse_message(child)) + case "enum": + interface["enums"].append(parse_enum(child)) + return interface + +def parse_message(node): + message = { + "name": node.getAttribute("name"), + "type": node.getAttribute("type"), + "since": node.getAttribute("since"), + "deprecated-since": node.getAttribute("deprecated-since"), + "summary": None, + "description": None, + "args": [], + } + for child in filter(is_element, node.childNodes): + match child.tagName: + case "description": + message |= parse_description(child) + case "arg": + message["args"].append(parse_arg(child)) + return message + +def parse_arg(node): + arg = { + "name": node.getAttribute("name"), + "type": node.getAttribute("type"), + "summary": node.getAttribute("summary"), + "description": None, + "interface": node.getAttribute("interface"), + "allow-null": node.getAttribute("allow-null") == "true", + "enum": node.getAttribute("enum"), + } + for child in filter(is_element, node.childNodes): + if child.tagName == "description": + arg |= parse_description(child) + return arg + +def parse_enum(node): + enum = { + "name": node.getAttribute("name"), + "since": node.getAttribute("since"), + "bitfield": node.getAttribute("bitfield") == "true", + "summary": None, + "description": None, + "entries": [], + } + for child in filter(is_element, node.childNodes): + match child.tagName: + case "description": + enum |= parse_description(child) + case "entry": + enum["entries"].append(parse_entry(child)) + return enum + +def parse_entry(node): + entry = { + "name": node.getAttribute("name"), + "value": node.getAttribute("value"), + "summary": node.getAttribute("summary"), + "since": node.getAttribute("since"), + "deprecated-since": node.getAttribute("deprecated-since"), + } + for child in filter(is_element, node.childNodes): + if child.tagName == "description": + entry |= parse_description(child) + return entry + +def parse_description(node): + return { + "summary": node.getAttribute("summary"), + "description": parse_text(node), + } + +def parse_text(node): + raw = "".join([child.data for child in node.childNodes]) + raw = raw.lstrip("\n") + + start = len(raw) - len(raw.lstrip()) + indent = raw[:start] + + lines = [l.removeprefix(indent) for l in raw.split("\n")] + return "\n".join(lines).strip() + +def is_element(node): + return node.nodeType == Node.ELEMENT_NODE + +def render_protocol(protocol): + if protocol["copyright"]: + print("```") + print(protocol["copyright"]) + print("```") + + if protocol["description"]: + print(protocol["description"]) + + for interface in protocol["interfaces"]: + render_interface(interface) + +def render_interface(interface): + render_header("##", interface) + + for request in interface["requests"]: + render_message(request) + for event in interface["events"]: + render_message(event) + for enum in interface["enums"]: + render_enum(enum) + +def render_message(message): + render_header("###", message) + + for arg in message["args"]: + print(f"`{arg["name"]}`") + print(f" : `{arg["type"]}` — {arg["summary"]}") + print("") + +def render_enum(enum): + render_header("###", enum) + + for entry in enum["entries"]: + print(f"`{entry["name"]}`") + print(f" : `{entry["value"]}` — {entry["summary"]}") + print("") + +def render_header(heading, element): + print("") + print(f"{heading} `{element["name"]}`", end="") + if element["summary"]: + print(f" — {element["summary"]}") + else: + print("") + print("") + if element["description"]: + print(element["description"]) + print("") + +protocol = parse_protocol(doc.documentElement) +render_protocol(protocol) diff --git a/doc/book/src/SUMMARY.md b/doc/book/src/SUMMARY.md index 713b4e02..a42224f6 100644 --- a/doc/book/src/SUMMARY.md +++ b/doc/book/src/SUMMARY.md @@ -10,3 +10,4 @@ - [X11 Application Support](./Xwayland.md) - [Content Updates](./Content_Updates.md) - [Color management](./Color.md) +- [Specification](./Specification.md)