mirror of
https://gitlab.gnome.org/jwestman/blueprint-compiler.git
synced 2025-05-04 15:59:08 -04:00
291 lines
9 KiB
Python
291 lines
9 KiB
Python
# decompiler.py
|
|
#
|
|
# Copyright 2021 James Westman <james@jwestman.net>
|
|
#
|
|
# This file is free software; you can redistribute it and/or modify it
|
|
# under the terms of the GNU Lesser General Public License as
|
|
# published by the Free Software Foundation; either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This file is distributed in the hope that it will be useful, but
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public
|
|
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
|
|
import re
|
|
from enum import Enum
|
|
import typing as T
|
|
from dataclasses import dataclass
|
|
|
|
from .xml_reader import Element, parse
|
|
from .gir import *
|
|
from .utils import Colors
|
|
|
|
|
|
__all__ = ["decompile"]
|
|
|
|
|
|
_DECOMPILERS: T.Dict = {}
|
|
_CLOSING = {
|
|
"{": "}",
|
|
"[": "]",
|
|
}
|
|
_NAMESPACES = [
|
|
("GLib", "2.0"),
|
|
("GObject", "2.0"),
|
|
("Gio", "2.0"),
|
|
("Adw", "1.0"),
|
|
]
|
|
|
|
|
|
class LineType(Enum):
|
|
NONE = 1
|
|
STMT = 2
|
|
BLOCK_START = 3
|
|
BLOCK_END = 4
|
|
|
|
|
|
class DecompileCtx:
|
|
def __init__(self):
|
|
self._result = ""
|
|
self.gir = GirContext()
|
|
self._indent = 0
|
|
self._blocks_need_end = []
|
|
self._last_line_type = LineType.NONE
|
|
|
|
self.gir.add_namespace(get_namespace("Gtk", "4.0"))
|
|
|
|
|
|
@property
|
|
def result(self):
|
|
imports = "\n".join([
|
|
f"using {ns} {namespace.version};"
|
|
for ns, namespace in self.gir.namespaces.items()
|
|
])
|
|
return imports + "\n" + self._result
|
|
|
|
|
|
def type_by_cname(self, cname):
|
|
if type := self.gir.get_type_by_cname(cname):
|
|
return type
|
|
|
|
for ns, version in _NAMESPACES:
|
|
try:
|
|
namespace = get_namespace(ns, version)
|
|
if type := namespace.get_type_by_cname(cname):
|
|
self.gir.add_namespace(namespace)
|
|
return type
|
|
except:
|
|
pass
|
|
|
|
|
|
def start_block(self):
|
|
self._blocks_need_end.append(None)
|
|
|
|
def end_block(self):
|
|
if close := self._blocks_need_end.pop():
|
|
self.print(close)
|
|
|
|
def end_block_with(self, text):
|
|
self._blocks_need_end[-1] = text
|
|
|
|
|
|
def print(self, line, newline=True):
|
|
if line == "}" or line == "]":
|
|
self._indent -= 1
|
|
|
|
# Add blank lines between different types of lines, for neatness
|
|
if newline:
|
|
if line == "}" or line == "]":
|
|
line_type = LineType.BLOCK_END
|
|
elif line.endswith("{") or line.endswith("]"):
|
|
line_type = LineType.BLOCK_START
|
|
elif line.endswith(";"):
|
|
line_type = LineType.STMT
|
|
else:
|
|
line_type = LineType.NONE
|
|
if line_type != self._last_line_type and self._last_line_type != LineType.BLOCK_START and line_type != LineType.BLOCK_END:
|
|
self._result += "\n"
|
|
self._last_line_type = line_type
|
|
|
|
self._result += (" " * self._indent) + line
|
|
if newline:
|
|
self._result += "\n"
|
|
|
|
if line.endswith("{") or line.endswith("["):
|
|
if len(self._blocks_need_end):
|
|
self._blocks_need_end[-1] = _CLOSING[line[-1]]
|
|
self._indent += 1
|
|
|
|
|
|
def print_attribute(self, name, value, type):
|
|
if type is None:
|
|
self.print(f"{name}: \"{escape_quote(value)}\";")
|
|
elif type.assignable_to(FloatType()):
|
|
self.print(f"{name}: {value};")
|
|
elif type.assignable_to(BoolType()):
|
|
val = truthy(value)
|
|
self.print(f"{name}: {'true' if val else 'false'};")
|
|
elif (
|
|
type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Pixbuf"))
|
|
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Texture"))
|
|
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable"))
|
|
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutAction"))
|
|
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger"))
|
|
):
|
|
self.print(f"{name}: \"{escape_quote(value)}\";")
|
|
elif type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("GObject.Object")):
|
|
self.print(f"{name}: {value};")
|
|
elif isinstance(type, Enumeration):
|
|
for member in type.members.values():
|
|
if member.nick == value or member.c_ident == value:
|
|
self.print(f"{name}: {member.name};")
|
|
break
|
|
else:
|
|
self.print(f"{name}: {value.replace('-', '_')};")
|
|
elif isinstance(type, Bitfield):
|
|
flags = re.sub(r"\s*\|\s*", " | ", value).replace("-", "_")
|
|
self.print(f"{name}: {flags};")
|
|
else:
|
|
self.print(f"{name}: \"{escape_quote(value)}\";")
|
|
|
|
|
|
def _decompile_element(ctx: DecompileCtx, gir, xml):
|
|
try:
|
|
decompiler = _DECOMPILERS.get(xml.tag)
|
|
if decompiler is None:
|
|
raise UnsupportedError(f"unsupported XML tag: <{xml.tag}>")
|
|
|
|
args = {canon(name): value for name, value in xml.attrs.items()}
|
|
if decompiler._cdata:
|
|
if len(xml.children):
|
|
args["cdata"] = None
|
|
else:
|
|
args["cdata"] = xml.cdata
|
|
|
|
ctx.start_block()
|
|
gir = decompiler(ctx, gir, **args)
|
|
|
|
for child_type in xml.children.values():
|
|
for child in child_type:
|
|
_decompile_element(ctx, gir, child)
|
|
|
|
ctx.end_block()
|
|
|
|
except UnsupportedError as e:
|
|
raise e
|
|
except TypeError as e:
|
|
raise UnsupportedError(tag=xml.tag)
|
|
|
|
|
|
def decompile(data):
|
|
ctx = DecompileCtx()
|
|
|
|
xml = parse(data)
|
|
_decompile_element(ctx, None, xml)
|
|
|
|
return ctx.result
|
|
|
|
|
|
|
|
def canon(string: str) -> str:
|
|
if string == "class":
|
|
return "klass"
|
|
else:
|
|
return string.replace("-", "_").lower()
|
|
|
|
def truthy(string: str) -> bool:
|
|
return string.lower() in ["yes", "true", "t", "y", "1"]
|
|
|
|
def full_name(gir):
|
|
return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name
|
|
|
|
def lookup_by_cname(gir, cname: str):
|
|
if isinstance(gir, GirContext):
|
|
return gir.get_type_by_cname(cname)
|
|
else:
|
|
return gir.get_containing(Repository).get_type_by_cname(cname)
|
|
|
|
|
|
def decompiler(tag, cdata=False):
|
|
def decorator(func):
|
|
func._cdata = cdata
|
|
_DECOMPILERS[tag] = func
|
|
return func
|
|
return decorator
|
|
|
|
|
|
def escape_quote(string: str) -> str:
|
|
return (string
|
|
.replace("\\", "\\\\")
|
|
.replace("\'", "\\'")
|
|
.replace("\"", "\\\"")
|
|
.replace("\n", "\\n"))
|
|
|
|
|
|
@decompiler("interface")
|
|
def decompile_interface(ctx, gir):
|
|
return gir
|
|
|
|
|
|
@decompiler("requires")
|
|
def decompile_requires(ctx, gir, lib=None, version=None):
|
|
return gir
|
|
|
|
|
|
@decompiler("property", cdata=True)
|
|
def decompile_property(ctx, gir, name, cdata, bind_source=None, bind_property=None, bind_flags=None, translatable="false", comments=None, context=None):
|
|
name = name.replace("_", "-")
|
|
if comments is not None:
|
|
ctx.print(f"/* Translators: {comments} */")
|
|
|
|
if cdata is None:
|
|
ctx.print(f"{name}: ", False)
|
|
ctx.end_block_with(";")
|
|
elif bind_source:
|
|
flags = ""
|
|
if bind_flags:
|
|
if "sync-create" in bind_flags:
|
|
flags += " sync-create"
|
|
if "invert-boolean" in bind_flags:
|
|
flags += " inverted"
|
|
if "bidirectional" in bind_flags:
|
|
flags += " bidirectional"
|
|
ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};")
|
|
elif truthy(translatable):
|
|
if context is not None:
|
|
ctx.print(f"{name}: C_(\"{escape_quote(context)}\", \"{escape_quote(cdata)}\");")
|
|
else:
|
|
ctx.print(f"{name}: _(\"{escape_quote(cdata)}\");")
|
|
elif gir is None or gir.properties.get(name) is None:
|
|
ctx.print(f"{name}: \"{escape_quote(cdata)}\";")
|
|
else:
|
|
ctx.print_attribute(name, cdata, gir.properties.get(name).type)
|
|
return gir
|
|
|
|
@decompiler("attribute", cdata=True)
|
|
def decompile_attribute(ctx, gir, name, cdata, translatable="false", comments=None, context=None):
|
|
decompile_property(ctx, gir, name, cdata, translatable=translatable, comments=comments, context=context)
|
|
|
|
@decompiler("attributes")
|
|
def decompile_attributes(ctx, gir):
|
|
ctx.print("attributes {")
|
|
|
|
|
|
@dataclass
|
|
class UnsupportedError(Exception):
|
|
message: str = "unsupported feature"
|
|
tag: T.Optional[str] = None
|
|
|
|
def print(self, filename: str):
|
|
print(f"\n{Colors.RED}{Colors.BOLD}error: {self.message}{Colors.CLEAR}")
|
|
print(f"in {Colors.UNDERLINE}{filename}{Colors.NO_UNDERLINE}")
|
|
if self.tag:
|
|
print(f"in tag {Colors.BLUE}{self.tag}{Colors.CLEAR}")
|
|
print(f"""{Colors.FAINT}The gtk-blueprint-tool compiler might support this feature, but the
|
|
porting tool does not. You probably need to port this file manually.{Colors.CLEAR}\n""")
|