diff --git a/pytiled_parser/parsers/json/tiled_object.py b/pytiled_parser/parsers/json/tiled_object.py index 0f961a0..33cae00 100644 --- a/pytiled_parser/parsers/json/tiled_object.py +++ b/pytiled_parser/parsers/json/tiled_object.py @@ -1,332 +1,332 @@ -"""Object parsing for the JSON Map Format. -""" -from pathlib import Path -from typing import Any, Callable, Dict, List, Optional - -from typing_extensions import TypedDict - -from pytiled_parser.common_types import OrderedPair, Size -from pytiled_parser.parsers.json.properties import RawProperty -from pytiled_parser.parsers.json.properties import parse as parse_properties -from pytiled_parser.tiled_object import ( - Ellipse, - Point, - Polygon, - Polyline, - Rectangle, - Text, - Tile, - TiledObject, -) -from pytiled_parser.util import load_object_template, parse_color - -RawText = TypedDict( - "RawText", - { - "text": str, - "color": str, - "fontfamily": str, - "pixelsize": float, # this is `font_size` in Text - "bold": bool, - "italic": bool, - "strikeout": bool, - "underline": bool, - "kerning": bool, - "halign": str, - "valign": str, - "wrap": bool, - }, -) -RawText.__doc__ = """ - The keys and their types that appear in a Tiled JSON Text Object. - - Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#text-example -""" - - -RawObject = TypedDict( - "RawObject", - { - "id": int, - "gid": int, - "template": str, - "x": float, - "y": float, - "width": float, - "height": float, - "rotation": float, - "visible": bool, - "name": str, - "class": str, - "type": str, - "properties": List[RawProperty], - "ellipse": bool, - "point": bool, - "polygon": List[Dict[str, float]], - "polyline": List[Dict[str, float]], - "text": RawText, - }, -) -RawObject.__doc__ = """ - The keys and their types that appear in a Tiled JSON Object. - - Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#object -""" - - -def _parse_common(raw_object: RawObject) -> TiledObject: - """Create an Object containing all the attributes common to all types of objects. - - Args: - raw_object: Raw object to get common attributes from - - Returns: - Object: The attributes in common of all types of objects - """ - - common = TiledObject( - id=raw_object["id"], - coordinates=OrderedPair(raw_object["x"], raw_object["y"]), - visible=raw_object["visible"], - size=Size(raw_object["width"], raw_object["height"]), - rotation=raw_object["rotation"], - name=raw_object["name"], - ) - - if raw_object.get("type") is not None: - common.class_ = raw_object["type"] - - if raw_object.get("class") is not None: - common.class_ = raw_object["class"] - - if raw_object.get("properties") is not None: - common.properties = parse_properties(raw_object["properties"]) - - return common - - -def _parse_ellipse(raw_object: RawObject) -> Ellipse: - """Parse the raw object into an Ellipse. - - Args: - raw_object: Raw object to be parsed to an Ellipse - - Returns: - Ellipse: The Ellipse object created from the raw object - """ - return Ellipse(**_parse_common(raw_object).__dict__) - - -def _parse_rectangle(raw_object: RawObject) -> Rectangle: - """Parse the raw object into a Rectangle. - - Args: - raw_object: Raw object to be parsed to a Rectangle - - Returns: - Rectangle: The Rectangle object created from the raw object - """ - return Rectangle(**_parse_common(raw_object).__dict__) - - -def _parse_point(raw_object: RawObject) -> Point: - """Parse the raw object into a Point. - - Args: - raw_object: Raw object to be parsed to a Point - - Returns: - Point: The Point object created from the raw object - """ - return Point(**_parse_common(raw_object).__dict__) - - -def _parse_polygon(raw_object: RawObject) -> Polygon: - """Parse the raw object into a Polygon. - - Args: - raw_object: Raw object to be parsed to a Polygon - - Returns: - Polygon: The Polygon object created from the raw object - """ - polygon = [] - for point in raw_object["polygon"]: - polygon.append(OrderedPair(point["x"], point["y"])) - - return Polygon(points=polygon, **_parse_common(raw_object).__dict__) - - -def _parse_polyline(raw_object: RawObject) -> Polyline: - """Parse the raw object into a Polyline. - - Args: - raw_object: Raw object to be parsed to a Polyline - - Returns: - Polyline: The Polyline object created from the raw object - """ - polyline = [] - for point in raw_object["polyline"]: - polyline.append(OrderedPair(point["x"], point["y"])) - - return Polyline(points=polyline, **_parse_common(raw_object).__dict__) - - -def _parse_tile( - raw_object: RawObject, - new_tileset: Optional[Dict[str, Any]] = None, - new_tileset_path: Optional[Path] = None, -) -> Tile: - """Parse the raw object into a Tile. - - Args: - raw_object: Raw object to be parsed to a Tile - - Returns: - Tile: The Tile object created from the raw object - """ - gid = raw_object["gid"] - - return Tile( - gid=gid, - new_tileset=new_tileset, - new_tileset_path=new_tileset_path, - **_parse_common(raw_object).__dict__ - ) - - -def _parse_text(raw_object: RawObject) -> Text: - """Parse the raw object into Text. - - Args: - raw_object: Raw object to be parsed to a Text - - Returns: - Text: The Text object created from the raw object - """ - # required attributes - raw_text: RawText = raw_object["text"] - text = raw_text["text"] - - # create base Text object - text_object = Text(text=text, **_parse_common(raw_object).__dict__) - - # optional attributes - if raw_text.get("color") is not None: - text_object.color = parse_color(raw_text["color"]) - - if raw_text.get("fontfamily") is not None: - text_object.font_family = raw_text["fontfamily"] - - if raw_text.get("pixelsize") is not None: - text_object.font_size = raw_text["pixelsize"] - - if raw_text.get("bold") is not None: - text_object.bold = raw_text["bold"] - - if raw_text.get("italic") is not None: - text_object.italic = raw_text["italic"] - - if raw_text.get("kerning") is not None: - text_object.kerning = raw_text["kerning"] - - if raw_text.get("strikeout") is not None: - text_object.strike_out = raw_text["strikeout"] - - if raw_text.get("underline") is not None: - text_object.underline = raw_text["underline"] - - if raw_text.get("halign") is not None: - text_object.horizontal_align = raw_text["halign"] - - if raw_text.get("valign") is not None: - text_object.vertical_align = raw_text["valign"] - - if raw_text.get("wrap") is not None: - text_object.wrap = raw_text["wrap"] - - return text_object - - -def _get_parser(raw_object: RawObject) -> Callable[[RawObject], TiledObject]: - """Get the parser function for a given raw object. - - Only used internally by the JSON parser. - - Args: - raw_object: Raw object that is analyzed to determine the parser function. - - Returns: - Callable[[RawObject], Object]: The parser function. - """ - if raw_object.get("ellipse"): - return _parse_ellipse - - if raw_object.get("point"): - return _parse_point - - # This is excluded from tests because the coverage is broken. I promise - # there are tests for Tile objects, but for some reason the coverage - # isn't picking up this if statement(though it does pickup the _parse_tile) - # function so who knows - if raw_object.get("gid"): # pragma: no cover - # Only tile objects have the `gid` key - return _parse_tile - - if raw_object.get("polygon"): - return _parse_polygon - - if raw_object.get("polyline"): - return _parse_polyline - - if raw_object.get("text"): - return _parse_text - - # If it's none of the above, rectangle is the only one left. - # Rectangle is the only object which has no special properties to signify that. - return _parse_rectangle - - -def parse( - raw_object: RawObject, - parent_dir: Optional[Path] = None, -) -> TiledObject: - """Parse the raw object into a pytiled_parser version - - Args: - raw_object: Raw object that is to be cast. - parent_dir: The parent directory that the map file is in. - - Returns: - Object: A parsed Object. - - Raises: - RuntimeError: When a parameter that is conditionally required was not sent. - """ - new_tileset = None - new_tileset_path = None - - if raw_object.get("template"): - if not parent_dir: - raise RuntimeError( - "A parent directory must be specified when using object templates." - ) - template_path = Path(parent_dir / raw_object["template"]) - template, new_tileset, new_tileset_path = load_object_template(template_path) - - if isinstance(template, dict): - loaded_template = template["object"] - for key in loaded_template: - if key != "id": - raw_object[key] = loaded_template[key] # type: ignore - else: - raise NotImplementedError( - "Loading TMX object templates inside a JSON map is currently not supported, " - "but will be in a future release." - ) - - if raw_object.get("gid"): - return _parse_tile(raw_object, new_tileset, new_tileset_path) - - return _get_parser(raw_object)(raw_object) +"""Object parsing for the JSON Map Format. +""" +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional + +from typing_extensions import TypedDict + +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.parsers.json.properties import RawProperty +from pytiled_parser.parsers.json.properties import parse as parse_properties +from pytiled_parser.tiled_object import ( + Ellipse, + Point, + Polygon, + Polyline, + Rectangle, + Text, + Tile, + TiledObject, +) +from pytiled_parser.util import load_object_template, parse_color + +RawText = TypedDict( + "RawText", + { + "text": str, + "color": str, + "fontfamily": str, + "pixelsize": float, # this is `font_size` in Text + "bold": bool, + "italic": bool, + "strikeout": bool, + "underline": bool, + "kerning": bool, + "halign": str, + "valign": str, + "wrap": bool, + }, +) +RawText.__doc__ = """ + The keys and their types that appear in a Tiled JSON Text Object. + + Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#text-example +""" + + +RawObject = TypedDict( + "RawObject", + { + "id": int, + "gid": int, + "template": str, + "x": float, + "y": float, + "width": float, + "height": float, + "rotation": float, + "visible": bool, + "name": str, + "class": str, + "type": str, + "properties": List[RawProperty], + "ellipse": bool, + "point": bool, + "polygon": List[Dict[str, float]], + "polyline": List[Dict[str, float]], + "text": RawText, + }, +) +RawObject.__doc__ = """ + The keys and their types that appear in a Tiled JSON Object. + + Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#object +""" + + +def _parse_common(raw_object: RawObject) -> TiledObject: + """Create an Object containing all the attributes common to all types of objects. + + Args: + raw_object: Raw object to get common attributes from + + Returns: + Object: The attributes in common of all types of objects + """ + + common = TiledObject( + id=raw_object["id"], + coordinates=OrderedPair(raw_object["x"], raw_object["y"]), + visible=raw_object["visible"], + size=Size(raw_object["width"], raw_object["height"]), + rotation=raw_object["rotation"], + name=raw_object["name"], + ) + + if raw_object.get("type") is not None: + common.class_ = raw_object["type"] + + if raw_object.get("class") is not None: + common.class_ = raw_object["class"] + + if raw_object.get("properties") is not None: + common.properties = parse_properties(raw_object["properties"]) + + return common + + +def _parse_ellipse(raw_object: RawObject) -> Ellipse: + """Parse the raw object into an Ellipse. + + Args: + raw_object: Raw object to be parsed to an Ellipse + + Returns: + Ellipse: The Ellipse object created from the raw object + """ + return Ellipse(**_parse_common(raw_object).__dict__) + + +def _parse_rectangle(raw_object: RawObject) -> Rectangle: + """Parse the raw object into a Rectangle. + + Args: + raw_object: Raw object to be parsed to a Rectangle + + Returns: + Rectangle: The Rectangle object created from the raw object + """ + return Rectangle(**_parse_common(raw_object).__dict__) + + +def _parse_point(raw_object: RawObject) -> Point: + """Parse the raw object into a Point. + + Args: + raw_object: Raw object to be parsed to a Point + + Returns: + Point: The Point object created from the raw object + """ + return Point(**_parse_common(raw_object).__dict__) + + +def _parse_polygon(raw_object: RawObject) -> Polygon: + """Parse the raw object into a Polygon. + + Args: + raw_object: Raw object to be parsed to a Polygon + + Returns: + Polygon: The Polygon object created from the raw object + """ + polygon = [] + for point in raw_object["polygon"]: + polygon.append(OrderedPair(point["x"], point["y"])) + + return Polygon(points=polygon, **_parse_common(raw_object).__dict__) + + +def _parse_polyline(raw_object: RawObject) -> Polyline: + """Parse the raw object into a Polyline. + + Args: + raw_object: Raw object to be parsed to a Polyline + + Returns: + Polyline: The Polyline object created from the raw object + """ + polyline = [] + for point in raw_object["polyline"]: + polyline.append(OrderedPair(point["x"], point["y"])) + + return Polyline(points=polyline, **_parse_common(raw_object).__dict__) + + +def _parse_tile( + raw_object: RawObject, + new_tileset: Optional[Dict[str, Any]] = None, + new_tileset_path: Optional[Path] = None, +) -> Tile: + """Parse the raw object into a Tile. + + Args: + raw_object: Raw object to be parsed to a Tile + + Returns: + Tile: The Tile object created from the raw object + """ + gid = raw_object["gid"] + + return Tile( + gid=gid, + new_tileset=new_tileset, + new_tileset_path=new_tileset_path, + **_parse_common(raw_object).__dict__, + ) + + +def _parse_text(raw_object: RawObject) -> Text: + """Parse the raw object into Text. + + Args: + raw_object: Raw object to be parsed to a Text + + Returns: + Text: The Text object created from the raw object + """ + # required attributes + raw_text: RawText = raw_object["text"] + text = raw_text["text"] + + # create base Text object + text_object = Text(text=text, **_parse_common(raw_object).__dict__) + + # optional attributes + if raw_text.get("color") is not None: + text_object.color = parse_color(raw_text["color"]) + + if raw_text.get("fontfamily") is not None: + text_object.font_family = raw_text["fontfamily"] + + if raw_text.get("pixelsize") is not None: + text_object.font_size = raw_text["pixelsize"] + + if raw_text.get("bold") is not None: + text_object.bold = raw_text["bold"] + + if raw_text.get("italic") is not None: + text_object.italic = raw_text["italic"] + + if raw_text.get("kerning") is not None: + text_object.kerning = raw_text["kerning"] + + if raw_text.get("strikeout") is not None: + text_object.strike_out = raw_text["strikeout"] + + if raw_text.get("underline") is not None: + text_object.underline = raw_text["underline"] + + if raw_text.get("halign") is not None: + text_object.horizontal_align = raw_text["halign"] + + if raw_text.get("valign") is not None: + text_object.vertical_align = raw_text["valign"] + + if raw_text.get("wrap") is not None: + text_object.wrap = raw_text["wrap"] + + return text_object + + +def _get_parser(raw_object: RawObject) -> Callable[[RawObject], TiledObject]: + """Get the parser function for a given raw object. + + Only used internally by the JSON parser. + + Args: + raw_object: Raw object that is analyzed to determine the parser function. + + Returns: + Callable[[RawObject], Object]: The parser function. + """ + if raw_object.get("ellipse"): + return _parse_ellipse + + if raw_object.get("point"): + return _parse_point + + # This is excluded from tests because the coverage is broken. I promise + # there are tests for Tile objects, but for some reason the coverage + # isn't picking up this if statement(though it does pickup the _parse_tile) + # function so who knows + if raw_object.get("gid"): # pragma: no cover + # Only tile objects have the `gid` key + return _parse_tile + + if raw_object.get("polygon"): + return _parse_polygon + + if raw_object.get("polyline"): + return _parse_polyline + + if raw_object.get("text"): + return _parse_text + + # If it's none of the above, rectangle is the only one left. + # Rectangle is the only object which has no special properties to signify that. + return _parse_rectangle + + +def parse( + raw_object: RawObject, + parent_dir: Optional[Path] = None, +) -> TiledObject: + """Parse the raw object into a pytiled_parser version + + Args: + raw_object: Raw object that is to be cast. + parent_dir: The parent directory that the map file is in. + + Returns: + Object: A parsed Object. + + Raises: + RuntimeError: When a parameter that is conditionally required was not sent. + """ + new_tileset = None + new_tileset_path = None + + if raw_object.get("template"): + if not parent_dir: + raise RuntimeError( + "A parent directory must be specified when using object templates." + ) + template_path = Path(parent_dir / raw_object["template"]) + template, new_tileset, new_tileset_path = load_object_template(template_path) + + if isinstance(template, dict): + loaded_template = template["object"] + for key in loaded_template: + if key != "id": + raw_object[key] = loaded_template[key] # type: ignore + else: + raise NotImplementedError( + "Loading TMX object templates inside a JSON map is currently not supported, " + "but will be in a future release." + ) + + if raw_object.get("gid"): + return _parse_tile(raw_object, new_tileset, new_tileset_path) + + return _get_parser(raw_object)(raw_object) diff --git a/pytiled_parser/parsers/tmx/properties.py b/pytiled_parser/parsers/tmx/properties.py index fd29543..1b20e3d 100644 --- a/pytiled_parser/parsers/tmx/properties.py +++ b/pytiled_parser/parsers/tmx/properties.py @@ -1,37 +1,35 @@ -import xml.etree.ElementTree as etree -from pathlib import Path - -from pytiled_parser.properties import Properties, Property -from pytiled_parser.util import parse_color - - -def parse(raw_properties: etree.Element) -> Properties: - - final: Properties = {} - value: Property - - for raw_property in raw_properties.findall("property"): - - type_ = raw_property.attrib.get("type") - - if "value" not in raw_property.attrib: - continue - - value_ = raw_property.attrib["value"] - - if type_ == "file": - value = Path(value_) - elif type_ == "color": - value = parse_color(value_) - elif type_ == "int" or type_ == "float": - value = float(value_) - elif type_ == "bool": - if value_ == "true": - value = True - else: - value = False - else: - value = value_ - final[raw_property.attrib["name"]] = value - - return final +import xml.etree.ElementTree as etree +from pathlib import Path + +from pytiled_parser.properties import Properties, Property +from pytiled_parser.util import parse_color + + +def parse(raw_properties: etree.Element) -> Properties: + final: Properties = {} + value: Property + + for raw_property in raw_properties.findall("property"): + type_ = raw_property.attrib.get("type") + + if "value" not in raw_property.attrib: + continue + + value_ = raw_property.attrib["value"] + + if type_ == "file": + value = Path(value_) + elif type_ == "color": + value = parse_color(value_) + elif type_ == "int" or type_ == "float": + value = float(value_) + elif type_ == "bool": + if value_ == "true": + value = True + else: + value = False + else: + value = value_ + final[raw_property.attrib["name"]] = value + + return final diff --git a/pytiled_parser/parsers/tmx/tiled_object.py b/pytiled_parser/parsers/tmx/tiled_object.py index f8c3aa0..8f9fb17 100644 --- a/pytiled_parser/parsers/tmx/tiled_object.py +++ b/pytiled_parser/parsers/tmx/tiled_object.py @@ -1,295 +1,295 @@ -import xml.etree.ElementTree as etree -from pathlib import Path -from typing import Callable, Optional - -from pytiled_parser.common_types import OrderedPair, Size -from pytiled_parser.parsers.tmx.properties import parse as parse_properties -from pytiled_parser.tiled_object import ( - Ellipse, - Point, - Polygon, - Polyline, - Rectangle, - Text, - Tile, - TiledObject, -) -from pytiled_parser.util import load_object_template, parse_color - - -def _parse_common(raw_object: etree.Element) -> TiledObject: - """Create an Object containing all the attributes common to all types of objects. - - Args: - raw_object: XML Element to get common attributes from - - Returns: - Object: The attributes in common of all types of objects - """ - - common = TiledObject( - id=int(raw_object.attrib["id"]), - coordinates=OrderedPair( - float(raw_object.attrib["x"]), float(raw_object.attrib["y"]) - ), - ) - - if raw_object.attrib.get("width") is not None: - common.size = Size( - float(raw_object.attrib["width"]), float(raw_object.attrib["height"]) - ) - - if raw_object.attrib.get("visible") is not None: - common.visible = bool(int(raw_object.attrib["visible"])) - - if raw_object.attrib.get("rotation") is not None: - common.rotation = float(raw_object.attrib["rotation"]) - - if raw_object.attrib.get("name") is not None: - common.name = raw_object.attrib["name"] - - if raw_object.attrib.get("type") is not None: - common.class_ = raw_object.attrib["type"] - - if raw_object.attrib.get("class") is not None: - common.class_ = raw_object.attrib["class"] - - properties_element = raw_object.find("./properties") - if properties_element: - common.properties = parse_properties(properties_element) - - return common - - -def _parse_ellipse(raw_object: etree.Element) -> Ellipse: - """Parse the raw object into an Ellipse. - - Args: - raw_object: XML Element to be parsed to an Ellipse - - Returns: - Ellipse: The Ellipse object created from the raw object - """ - return Ellipse(**_parse_common(raw_object).__dict__) - - -def _parse_rectangle(raw_object: etree.Element) -> Rectangle: - """Parse the raw object into a Rectangle. - - Args: - raw_object: XML Element to be parsed to a Rectangle - - Returns: - Rectangle: The Rectangle object created from the raw object - """ - return Rectangle(**_parse_common(raw_object).__dict__) - - -def _parse_point(raw_object: etree.Element) -> Point: - """Parse the raw object into a Point. - - Args: - raw_object: XML Element to be parsed to a Point - - Returns: - Point: The Point object created from the raw object - """ - return Point(**_parse_common(raw_object).__dict__) - - -def _parse_polygon(raw_object: etree.Element) -> Polygon: - """Parse the raw object into a Polygon. - - Args: - raw_object: XML Element to be parsed to a Polygon - - Returns: - Polygon: The Polygon object created from the raw object - """ - polygon = [] - polygon_element = raw_object.find("./polygon") - if polygon_element is not None: - for raw_point in polygon_element.attrib["points"].split(" "): - point = raw_point.split(",") - polygon.append(OrderedPair(float(point[0]), float(point[1]))) - - return Polygon(points=polygon, **_parse_common(raw_object).__dict__) - - -def _parse_polyline(raw_object: etree.Element) -> Polyline: - """Parse the raw object into a Polyline. - - Args: - raw_object: Raw object to be parsed to a Polyline - - Returns: - Polyline: The Polyline object created from the raw object - """ - polyline = [] - polyline_element = raw_object.find("./polyline") - if polyline_element is not None: - for raw_point in polyline_element.attrib["points"].split(" "): - point = raw_point.split(",") - polyline.append(OrderedPair(float(point[0]), float(point[1]))) - - return Polyline(points=polyline, **_parse_common(raw_object).__dict__) - - -def _parse_tile( - raw_object: etree.Element, - new_tileset: Optional[etree.Element] = None, - new_tileset_path: Optional[Path] = None, -) -> Tile: - """Parse the raw object into a Tile. - - Args: - raw_object: XML Element to be parsed to a Tile - - Returns: - Tile: The Tile object created from the raw object - """ - return Tile( - gid=int(raw_object.attrib["gid"]), - new_tileset=new_tileset, - new_tileset_path=new_tileset_path, - **_parse_common(raw_object).__dict__ - ) - - -def _parse_text(raw_object: etree.Element) -> Text: - """Parse the raw object into Text. - - Args: - raw_object: XML Element to be parsed to a Text - - Returns: - Text: The Text object created from the raw object - """ - # required attributes - text_element = raw_object.find("./text") - - if text_element is not None: - text = text_element.text - - if not text: - text = "" - # create base Text object - text_object = Text(text=text, **_parse_common(raw_object).__dict__) - - # optional attributes - - if text_element.attrib.get("color") is not None: - text_object.color = parse_color(text_element.attrib["color"]) - - if text_element.attrib.get("fontfamily") is not None: - text_object.font_family = text_element.attrib["fontfamily"] - - if text_element.attrib.get("pixelsize") is not None: - text_object.font_size = float(text_element.attrib["pixelsize"]) - - if text_element.attrib.get("bold") is not None: - text_object.bold = bool(int(text_element.attrib["bold"])) - - if text_element.attrib.get("italic") is not None: - text_object.italic = bool(int(text_element.attrib["italic"])) - - if text_element.attrib.get("kerning") is not None: - text_object.kerning = bool(int(text_element.attrib["kerning"])) - - if text_element.attrib.get("strikeout") is not None: - text_object.strike_out = bool(int(text_element.attrib["strikeout"])) - - if text_element.attrib.get("underline") is not None: - text_object.underline = bool(int(text_element.attrib["underline"])) - - if text_element.attrib.get("halign") is not None: - text_object.horizontal_align = text_element.attrib["halign"] - - if text_element.attrib.get("valign") is not None: - text_object.vertical_align = text_element.attrib["valign"] - - if text_element.attrib.get("wrap") is not None: - text_object.wrap = bool(int(text_element.attrib["wrap"])) - - return text_object - - -def _get_parser(raw_object: etree.Element) -> Callable[[etree.Element], TiledObject]: - """Get the parser function for a given raw object. - - Only used internally by the TMX parser. - - Args: - raw_object: XML Element that is analyzed to determine the parser function. - - Returns: - Callable[[Element], Object]: The parser function. - """ - if raw_object.find("./ellipse") is not None: - return _parse_ellipse - - if raw_object.find("./point") is not None: - return _parse_point - - if raw_object.find("./polygon") is not None: - return _parse_polygon - - if raw_object.find("./polyline") is not None: - return _parse_polyline - - if raw_object.find("./text") is not None: - return _parse_text - - # If it's none of the above, rectangle is the only one left. - # Rectangle is the only object which has no properties to signify that. - return _parse_rectangle - - -def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> TiledObject: - """Parse the raw object into a pytiled_parser version - - Args: - raw_object: XML Element that is to be parsed. - parent_dir: The parent directory that the map file is in. - - Returns: - TiledObject: A parsed Object. - - Raises: - RuntimeError: When a parameter that is conditionally required was not sent. - """ - new_tileset = None - new_tileset_path = None - - if raw_object.attrib.get("template"): - if not parent_dir: - raise RuntimeError( - "A parent directory must be specified when using object templates." - ) - template_path = Path(parent_dir / raw_object.attrib["template"]) - template, new_tileset, new_tileset_path = load_object_template(template_path) - - if isinstance(template, etree.Element): - new_object = template.find("./object") - if new_object is not None: - if raw_object.attrib.get("id") is not None: - new_object.attrib["id"] = raw_object.attrib["id"] - - if raw_object.attrib.get("x") is not None: - new_object.attrib["x"] = raw_object.attrib["x"] - - if raw_object.attrib.get("y") is not None: - new_object.attrib["y"] = raw_object.attrib["y"] - - raw_object = new_object - elif isinstance(template, dict): - # load the JSON object into the XML object - raise NotImplementedError( - "Loading JSON object templates inside a TMX map is currently not supported, " - "but will be in a future release." - ) - - if raw_object.attrib.get("gid"): - return _parse_tile(raw_object, new_tileset, new_tileset_path) - - return _get_parser(raw_object)(raw_object) +import xml.etree.ElementTree as etree +from pathlib import Path +from typing import Callable, Optional + +from pytiled_parser.common_types import OrderedPair, Size +from pytiled_parser.parsers.tmx.properties import parse as parse_properties +from pytiled_parser.tiled_object import ( + Ellipse, + Point, + Polygon, + Polyline, + Rectangle, + Text, + Tile, + TiledObject, +) +from pytiled_parser.util import load_object_template, parse_color + + +def _parse_common(raw_object: etree.Element) -> TiledObject: + """Create an Object containing all the attributes common to all types of objects. + + Args: + raw_object: XML Element to get common attributes from + + Returns: + Object: The attributes in common of all types of objects + """ + + common = TiledObject( + id=int(raw_object.attrib["id"]), + coordinates=OrderedPair( + float(raw_object.attrib["x"]), float(raw_object.attrib["y"]) + ), + ) + + if raw_object.attrib.get("width") is not None: + common.size = Size( + float(raw_object.attrib["width"]), float(raw_object.attrib["height"]) + ) + + if raw_object.attrib.get("visible") is not None: + common.visible = bool(int(raw_object.attrib["visible"])) + + if raw_object.attrib.get("rotation") is not None: + common.rotation = float(raw_object.attrib["rotation"]) + + if raw_object.attrib.get("name") is not None: + common.name = raw_object.attrib["name"] + + if raw_object.attrib.get("type") is not None: + common.class_ = raw_object.attrib["type"] + + if raw_object.attrib.get("class") is not None: + common.class_ = raw_object.attrib["class"] + + properties_element = raw_object.find("./properties") + if properties_element: + common.properties = parse_properties(properties_element) + + return common + + +def _parse_ellipse(raw_object: etree.Element) -> Ellipse: + """Parse the raw object into an Ellipse. + + Args: + raw_object: XML Element to be parsed to an Ellipse + + Returns: + Ellipse: The Ellipse object created from the raw object + """ + return Ellipse(**_parse_common(raw_object).__dict__) + + +def _parse_rectangle(raw_object: etree.Element) -> Rectangle: + """Parse the raw object into a Rectangle. + + Args: + raw_object: XML Element to be parsed to a Rectangle + + Returns: + Rectangle: The Rectangle object created from the raw object + """ + return Rectangle(**_parse_common(raw_object).__dict__) + + +def _parse_point(raw_object: etree.Element) -> Point: + """Parse the raw object into a Point. + + Args: + raw_object: XML Element to be parsed to a Point + + Returns: + Point: The Point object created from the raw object + """ + return Point(**_parse_common(raw_object).__dict__) + + +def _parse_polygon(raw_object: etree.Element) -> Polygon: + """Parse the raw object into a Polygon. + + Args: + raw_object: XML Element to be parsed to a Polygon + + Returns: + Polygon: The Polygon object created from the raw object + """ + polygon = [] + polygon_element = raw_object.find("./polygon") + if polygon_element is not None: + for raw_point in polygon_element.attrib["points"].split(" "): + point = raw_point.split(",") + polygon.append(OrderedPair(float(point[0]), float(point[1]))) + + return Polygon(points=polygon, **_parse_common(raw_object).__dict__) + + +def _parse_polyline(raw_object: etree.Element) -> Polyline: + """Parse the raw object into a Polyline. + + Args: + raw_object: Raw object to be parsed to a Polyline + + Returns: + Polyline: The Polyline object created from the raw object + """ + polyline = [] + polyline_element = raw_object.find("./polyline") + if polyline_element is not None: + for raw_point in polyline_element.attrib["points"].split(" "): + point = raw_point.split(",") + polyline.append(OrderedPair(float(point[0]), float(point[1]))) + + return Polyline(points=polyline, **_parse_common(raw_object).__dict__) + + +def _parse_tile( + raw_object: etree.Element, + new_tileset: Optional[etree.Element] = None, + new_tileset_path: Optional[Path] = None, +) -> Tile: + """Parse the raw object into a Tile. + + Args: + raw_object: XML Element to be parsed to a Tile + + Returns: + Tile: The Tile object created from the raw object + """ + return Tile( + gid=int(raw_object.attrib["gid"]), + new_tileset=new_tileset, + new_tileset_path=new_tileset_path, + **_parse_common(raw_object).__dict__, + ) + + +def _parse_text(raw_object: etree.Element) -> Text: + """Parse the raw object into Text. + + Args: + raw_object: XML Element to be parsed to a Text + + Returns: + Text: The Text object created from the raw object + """ + # required attributes + text_element = raw_object.find("./text") + + if text_element is not None: + text = text_element.text + + if not text: + text = "" + # create base Text object + text_object = Text(text=text, **_parse_common(raw_object).__dict__) + + # optional attributes + + if text_element.attrib.get("color") is not None: + text_object.color = parse_color(text_element.attrib["color"]) + + if text_element.attrib.get("fontfamily") is not None: + text_object.font_family = text_element.attrib["fontfamily"] + + if text_element.attrib.get("pixelsize") is not None: + text_object.font_size = float(text_element.attrib["pixelsize"]) + + if text_element.attrib.get("bold") is not None: + text_object.bold = bool(int(text_element.attrib["bold"])) + + if text_element.attrib.get("italic") is not None: + text_object.italic = bool(int(text_element.attrib["italic"])) + + if text_element.attrib.get("kerning") is not None: + text_object.kerning = bool(int(text_element.attrib["kerning"])) + + if text_element.attrib.get("strikeout") is not None: + text_object.strike_out = bool(int(text_element.attrib["strikeout"])) + + if text_element.attrib.get("underline") is not None: + text_object.underline = bool(int(text_element.attrib["underline"])) + + if text_element.attrib.get("halign") is not None: + text_object.horizontal_align = text_element.attrib["halign"] + + if text_element.attrib.get("valign") is not None: + text_object.vertical_align = text_element.attrib["valign"] + + if text_element.attrib.get("wrap") is not None: + text_object.wrap = bool(int(text_element.attrib["wrap"])) + + return text_object + + +def _get_parser(raw_object: etree.Element) -> Callable[[etree.Element], TiledObject]: + """Get the parser function for a given raw object. + + Only used internally by the TMX parser. + + Args: + raw_object: XML Element that is analyzed to determine the parser function. + + Returns: + Callable[[Element], Object]: The parser function. + """ + if raw_object.find("./ellipse") is not None: + return _parse_ellipse + + if raw_object.find("./point") is not None: + return _parse_point + + if raw_object.find("./polygon") is not None: + return _parse_polygon + + if raw_object.find("./polyline") is not None: + return _parse_polyline + + if raw_object.find("./text") is not None: + return _parse_text + + # If it's none of the above, rectangle is the only one left. + # Rectangle is the only object which has no properties to signify that. + return _parse_rectangle + + +def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> TiledObject: + """Parse the raw object into a pytiled_parser version + + Args: + raw_object: XML Element that is to be parsed. + parent_dir: The parent directory that the map file is in. + + Returns: + TiledObject: A parsed Object. + + Raises: + RuntimeError: When a parameter that is conditionally required was not sent. + """ + new_tileset = None + new_tileset_path = None + + if raw_object.attrib.get("template"): + if not parent_dir: + raise RuntimeError( + "A parent directory must be specified when using object templates." + ) + template_path = Path(parent_dir / raw_object.attrib["template"]) + template, new_tileset, new_tileset_path = load_object_template(template_path) + + if isinstance(template, etree.Element): + new_object = template.find("./object") + if new_object is not None: + if raw_object.attrib.get("id") is not None: + new_object.attrib["id"] = raw_object.attrib["id"] + + if raw_object.attrib.get("x") is not None: + new_object.attrib["x"] = raw_object.attrib["x"] + + if raw_object.attrib.get("y") is not None: + new_object.attrib["y"] = raw_object.attrib["y"] + + raw_object = new_object + elif isinstance(template, dict): + # load the JSON object into the XML object + raise NotImplementedError( + "Loading JSON object templates inside a TMX map is currently not supported, " + "but will be in a future release." + ) + + if raw_object.attrib.get("gid"): + return _parse_tile(raw_object, new_tileset, new_tileset_path) + + return _get_parser(raw_object)(raw_object)