diff --git a/README.md b/README.md index 24e31a0..2b85c82 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,11 @@ See the [tests](/tests/test_py_d2) for more detailed usage examples. - [x] Markdown / block strings / code in shapes - [x] Icons in shapes - [x] Support for empty labels +- [x] Shape links - [ ] SQL table shapes - [ ] Class shapes - [ ] Comments +- [ ] Layers ## Development diff --git a/src/py_d2/__init__.py b/src/py_d2/__init__.py index 4e74756..409d59f 100644 --- a/src/py_d2/__init__.py +++ b/src/py_d2/__init__.py @@ -1,10 +1,20 @@ from .connection import D2Connection from .connection import Direction from .diagram import D2Diagram +from .diagram import Layer from .shape import D2Shape from .shape import D2Text from .shape import Shape from .style import D2Style -__all__ = ["Direction", "D2Connection", "D2Diagram", "D2Shape", "D2Text", "D2Style", "Shape"] +__all__ = [ + "Direction", + "D2Connection", + "D2Diagram", + "D2Shape", + "D2Text", + "D2Style", + "Shape", + "Layer", +] diff --git a/src/py_d2/diagram.py b/src/py_d2/diagram.py index 4e26a19..110e25e 100644 --- a/src/py_d2/diagram.py +++ b/src/py_d2/diagram.py @@ -1,19 +1,60 @@ # -*- coding: utf-8 -*- +from __future__ import annotations + from typing import List from typing import Optional from py_d2.connection import D2Connection +from py_d2.helpers import indent +from py_d2.helpers import indent_lines from py_d2.shape import D2Shape +class Layer: + def __init__( + self, + name: str, + diagram: Optional["D2Diagram"] = None, + ): + self.name = name + self.diagram = diagram or D2Diagram() + + def set_diagram(self, diagram: "D2Diagram"): + self.diagram = diagram + + def lines(self, depth=1) -> List[str]: + lines = self.diagram.lines() + + if len(lines) == 0: + return [] + + outer_indent_size = depth * 2 + inner_indent_size = outer_indent_size + 2 + + # Wrap lines with layer { } and add indentation + wrapped_lines = [ + indent(f"{self.name}: {{", outer_indent_size), + *indent_lines(lines, inner_indent_size), + indent("}", outer_indent_size), + ] + + return wrapped_lines + + def __repr__(self) -> str: + lines = self.lines() + return "\n".join(lines) + + class D2Diagram: def __init__( self, shapes: Optional[List[D2Shape]] = None, connections: Optional[List[D2Connection]] = None, + layers: Optional[List[Layer]] = None, ): self.shapes = shapes or [] self.connections = connections or [] + self.layers = layers or [] def add_shape(self, shape: D2Shape): self.shapes.append(shape) @@ -21,8 +62,17 @@ def add_shape(self, shape: D2Shape): def add_connection(self, connection: D2Connection): self.connections.append(connection) - def __repr__(self) -> str: + def add_layer(self, layer: Layer): + self.layers.append(layer) + + def lines(self) -> List[str]: shapes = [str(shape) for shape in self.shapes] connections = [str(connection) for connection in self.connections] + layers = [line for layer in self.layers for line in layer.lines() if layer.lines()] + layers = ["layers: {"] + layers + ["}"] if layers else [] - return "\n".join(shapes + connections) + return shapes + connections + layers + + def __repr__(self) -> str: + lines = self.lines() + return "\n".join(lines) diff --git a/src/py_d2/helpers.py b/src/py_d2/helpers.py index dd03205..0e86066 100644 --- a/src/py_d2/helpers.py +++ b/src/py_d2/helpers.py @@ -3,8 +3,12 @@ from typing import Optional -def indent(items: List[str], n: int = 2) -> List[str]: - return [f"{' '*n}{item}" for item in items] +def indent(line, n: int = 2) -> str: + return f"{' '*n}{line}" + + +def indent_lines(items: List[str], n: int = 2) -> List[str]: + return [indent(item, n) for item in items] def add_label_and_properties( @@ -26,7 +30,7 @@ def add_label_and_properties( first_line += " {" if properties and has_properties: - return [first_line, *indent(properties), "}"] + return [first_line, *indent_lines(properties), "}"] return [first_line] diff --git a/src/py_d2/shape.py b/src/py_d2/shape.py index d61e837..c5c11b3 100644 --- a/src/py_d2/shape.py +++ b/src/py_d2/shape.py @@ -8,7 +8,7 @@ from py_d2.connection import D2Connection from py_d2.helpers import add_label_and_properties from py_d2.helpers import flatten -from py_d2.helpers import indent +from py_d2.helpers import indent_lines from py_d2.style import D2Style @@ -78,6 +78,8 @@ def __init__( connections: Optional[List[D2Connection]] = None, # A shape this is near near: Optional[str] = None, + # A link for a shape (when clicked) + link: Optional[str] = None, **kwargs: D2Text, ): self.name = name @@ -88,6 +90,7 @@ def __init__( self.icon = icon self.connections = connections or [] self.near = near + self.link = link self.kwargs = kwargs def add_shape(self, shape: D2Shape): @@ -107,6 +110,9 @@ def lines(self) -> List[str]: if self.near: properties.append(f"near: {self.near}") + if self.link: + properties.append(f"link: {self.link}") + if self.style: properties += self.style.lines() @@ -120,7 +126,7 @@ def lines(self) -> List[str]: other_property_line_end = other_property[-1] properties += [ f"{key}: {other_property_line_1}", - *indent(other_property_lines_other), + *indent_lines(other_property_lines_other), other_property_line_end, ] diff --git a/tests/test_py_d2/test_d2_layer.py b/tests/test_py_d2/test_d2_layer.py new file mode 100644 index 0000000..b897852 --- /dev/null +++ b/tests/test_py_d2/test_d2_layer.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +from py_d2.connection import D2Connection +from py_d2.diagram import D2Diagram +from py_d2.diagram import Layer + + +def test_d2_layer(): + layer = Layer(name="my_layer") + diagram = D2Diagram(layers=[layer]) + assert str(diagram) == "" + + +def test_d2_layer_single_subdiagram(): + """Test a root diagram with a connection and a single layer containing its own connection.""" + root_connection = D2Connection(shape_1="x", shape_2="y") + + layer_connection = D2Connection(shape_1="1", shape_2="2") + layer_diagram = D2Diagram(connections=[layer_connection]) + + layer = Layer(name="numbers", diagram=layer_diagram) + + root_diagram = D2Diagram(connections=[root_connection], layers=[layer]) + + expected_output = "x -> y\n" "layers: {\n" " numbers: {\n" " 1 -> 2\n" " }\n" "}" + + assert str(root_diagram) == expected_output + + +def test_d2_layer_two_layers_depth_1(): + """Test diagram with two layers at depth 1""" + connection1 = D2Connection(shape_1="a", shape_2="b") + layer1_diagram = D2Diagram(connections=[connection1]) + layer1 = Layer(name="layer1", diagram=layer1_diagram) + + connection2 = D2Connection(shape_1="c", shape_2="d") + layer2_diagram = D2Diagram(connections=[connection2]) + layer2 = Layer(name="layer2", diagram=layer2_diagram) + + root_diagram = D2Diagram(layers=[layer1, layer2]) + expected_output = "layers: {\n" " layer1: {\n" " a -> b\n" " }\n" " layer2: {\n" " c -> d\n" " }\n" "}" + assert str(root_diagram) == expected_output + + +def test_d2_layer_nested_layer_depth_2(): + """Test diagram with a layer nested inside another layer (depth 2)""" + inner_connection = D2Connection(shape_1="e", shape_2="f") + inner_diagram = D2Diagram(connections=[inner_connection]) + inner_layer = Layer(name="inner_layer", diagram=inner_diagram) + + outer_connection = D2Connection(shape_1="g", shape_2="h") + outer_diagram = D2Diagram(connections=[outer_connection], layers=[inner_layer]) + outer_layer = Layer(name="outer_layer", diagram=outer_diagram) + + root_diagram = D2Diagram(layers=[outer_layer]) + expected_output = ( + "layers: {\n" + " outer_layer: {\n" + " g -> h\n" + " layers: {\n" + " inner_layer: {\n" + " e -> f\n" + " }\n" + " }\n" + " }\n" + "}" + ) + assert str(root_diagram) == expected_output diff --git a/tests/test_py_d2/test_d2_shape.py b/tests/test_py_d2/test_d2_shape.py index 03ecb83..5ac2c1e 100644 --- a/tests/test_py_d2/test_d2_shape.py +++ b/tests/test_py_d2/test_d2_shape.py @@ -152,6 +152,11 @@ def test_d2_shape_near(): assert str(shape) == "shape_name: {\n near: some_other_shape\n}" +def test_d2_shape_link(): + shape = D2Shape(name="shape_name", link="https://github.com/MrBlenny/py-d2") + assert str(shape) == "shape_name: {\n link: https://github.com/MrBlenny/py-d2\n}" + + def test_d2_shape_other_properties(): text = "Some text" shape = D2Shape(name="shape_name", thing=D2Text(text=text, formatting="md"))