rdf-construct 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rdf_construct/__init__.py +12 -0
- rdf_construct/__main__.py +0 -0
- rdf_construct/cli.py +3429 -0
- rdf_construct/core/__init__.py +33 -0
- rdf_construct/core/config.py +116 -0
- rdf_construct/core/ordering.py +219 -0
- rdf_construct/core/predicate_order.py +212 -0
- rdf_construct/core/profile.py +157 -0
- rdf_construct/core/selector.py +64 -0
- rdf_construct/core/serialiser.py +232 -0
- rdf_construct/core/utils.py +89 -0
- rdf_construct/cq/__init__.py +77 -0
- rdf_construct/cq/expectations.py +365 -0
- rdf_construct/cq/formatters/__init__.py +45 -0
- rdf_construct/cq/formatters/json.py +104 -0
- rdf_construct/cq/formatters/junit.py +104 -0
- rdf_construct/cq/formatters/text.py +146 -0
- rdf_construct/cq/loader.py +300 -0
- rdf_construct/cq/runner.py +321 -0
- rdf_construct/diff/__init__.py +59 -0
- rdf_construct/diff/change_types.py +214 -0
- rdf_construct/diff/comparator.py +338 -0
- rdf_construct/diff/filters.py +133 -0
- rdf_construct/diff/formatters/__init__.py +71 -0
- rdf_construct/diff/formatters/json.py +192 -0
- rdf_construct/diff/formatters/markdown.py +210 -0
- rdf_construct/diff/formatters/text.py +195 -0
- rdf_construct/docs/__init__.py +60 -0
- rdf_construct/docs/config.py +238 -0
- rdf_construct/docs/extractors.py +603 -0
- rdf_construct/docs/generator.py +360 -0
- rdf_construct/docs/renderers/__init__.py +7 -0
- rdf_construct/docs/renderers/html.py +803 -0
- rdf_construct/docs/renderers/json.py +390 -0
- rdf_construct/docs/renderers/markdown.py +628 -0
- rdf_construct/docs/search.py +278 -0
- rdf_construct/docs/templates/html/base.html.jinja +44 -0
- rdf_construct/docs/templates/html/class.html.jinja +152 -0
- rdf_construct/docs/templates/html/hierarchy.html.jinja +28 -0
- rdf_construct/docs/templates/html/index.html.jinja +110 -0
- rdf_construct/docs/templates/html/instance.html.jinja +90 -0
- rdf_construct/docs/templates/html/namespaces.html.jinja +37 -0
- rdf_construct/docs/templates/html/property.html.jinja +124 -0
- rdf_construct/docs/templates/html/single_page.html.jinja +169 -0
- rdf_construct/lint/__init__.py +75 -0
- rdf_construct/lint/config.py +214 -0
- rdf_construct/lint/engine.py +396 -0
- rdf_construct/lint/formatters.py +327 -0
- rdf_construct/lint/rules.py +692 -0
- rdf_construct/localise/__init__.py +114 -0
- rdf_construct/localise/config.py +508 -0
- rdf_construct/localise/extractor.py +427 -0
- rdf_construct/localise/formatters/__init__.py +36 -0
- rdf_construct/localise/formatters/markdown.py +229 -0
- rdf_construct/localise/formatters/text.py +224 -0
- rdf_construct/localise/merger.py +346 -0
- rdf_construct/localise/reporter.py +356 -0
- rdf_construct/main.py +6 -0
- rdf_construct/merge/__init__.py +165 -0
- rdf_construct/merge/config.py +354 -0
- rdf_construct/merge/conflicts.py +281 -0
- rdf_construct/merge/formatters.py +426 -0
- rdf_construct/merge/merger.py +425 -0
- rdf_construct/merge/migrator.py +339 -0
- rdf_construct/merge/rules.py +377 -0
- rdf_construct/merge/splitter.py +1102 -0
- rdf_construct/puml2rdf/__init__.py +103 -0
- rdf_construct/puml2rdf/config.py +230 -0
- rdf_construct/puml2rdf/converter.py +420 -0
- rdf_construct/puml2rdf/merger.py +200 -0
- rdf_construct/puml2rdf/model.py +202 -0
- rdf_construct/puml2rdf/parser.py +565 -0
- rdf_construct/puml2rdf/validators.py +451 -0
- rdf_construct/refactor/__init__.py +72 -0
- rdf_construct/refactor/config.py +362 -0
- rdf_construct/refactor/deprecator.py +328 -0
- rdf_construct/refactor/formatters/__init__.py +8 -0
- rdf_construct/refactor/formatters/text.py +311 -0
- rdf_construct/refactor/renamer.py +294 -0
- rdf_construct/shacl/__init__.py +56 -0
- rdf_construct/shacl/config.py +166 -0
- rdf_construct/shacl/converters.py +520 -0
- rdf_construct/shacl/generator.py +364 -0
- rdf_construct/shacl/namespaces.py +93 -0
- rdf_construct/stats/__init__.py +29 -0
- rdf_construct/stats/collector.py +178 -0
- rdf_construct/stats/comparator.py +298 -0
- rdf_construct/stats/formatters/__init__.py +83 -0
- rdf_construct/stats/formatters/json.py +38 -0
- rdf_construct/stats/formatters/markdown.py +153 -0
- rdf_construct/stats/formatters/text.py +186 -0
- rdf_construct/stats/metrics/__init__.py +26 -0
- rdf_construct/stats/metrics/basic.py +147 -0
- rdf_construct/stats/metrics/complexity.py +137 -0
- rdf_construct/stats/metrics/connectivity.py +130 -0
- rdf_construct/stats/metrics/documentation.py +128 -0
- rdf_construct/stats/metrics/hierarchy.py +207 -0
- rdf_construct/stats/metrics/properties.py +88 -0
- rdf_construct/uml/__init__.py +22 -0
- rdf_construct/uml/context.py +194 -0
- rdf_construct/uml/mapper.py +371 -0
- rdf_construct/uml/odm_renderer.py +789 -0
- rdf_construct/uml/renderer.py +684 -0
- rdf_construct/uml/uml_layout.py +393 -0
- rdf_construct/uml/uml_style.py +613 -0
- rdf_construct-0.3.0.dist-info/METADATA +496 -0
- rdf_construct-0.3.0.dist-info/RECORD +110 -0
- rdf_construct-0.3.0.dist-info/WHEEL +4 -0
- rdf_construct-0.3.0.dist-info/entry_points.txt +3 -0
- rdf_construct-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
"""PlantUML styling configuration for RDF class diagrams.
|
|
2
|
+
|
|
3
|
+
Provides color schemes, arrow styles, and visual formatting for different
|
|
4
|
+
RDF entity types based on their semantic roles.
|
|
5
|
+
|
|
6
|
+
Added instance-specific styling based on rdf:type hierarchy.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
from rdflib import Graph, URIRef, RDF, RDFS
|
|
14
|
+
from rdflib.namespace import OWL
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ColorPalette:
|
|
18
|
+
"""Color definitions for a single entity type.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
border: Border/line color (hex or PlantUML color name)
|
|
22
|
+
fill: Fill/background color (hex or PlantUML color name)
|
|
23
|
+
text: Text color (optional, defaults to black)
|
|
24
|
+
line_style: Line style (e.g., 'bold', 'dashed', 'dotted')
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, config: dict[str, Any]):
|
|
28
|
+
"""Initialise colour palette from configuration.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
config: Dictionary with colour specifications
|
|
32
|
+
"""
|
|
33
|
+
self.border = config.get("border", "#000000")
|
|
34
|
+
self.fill = config.get("fill", "#FFFFFF")
|
|
35
|
+
self.text = config.get("text")
|
|
36
|
+
self.line_style = config.get("line_style")
|
|
37
|
+
|
|
38
|
+
def to_plantuml(self) -> str:
|
|
39
|
+
"""Generate PlantUML color specification.
|
|
40
|
+
|
|
41
|
+
Returns string in format: #back:FILL;line:BORDER;line.STYLE;text:TEXT
|
|
42
|
+
Not just #FILL
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
PlantUML color spec string with # prefix, or empty if no styling
|
|
46
|
+
"""
|
|
47
|
+
if not self.fill and not self.border and not self.text:
|
|
48
|
+
return ""
|
|
49
|
+
|
|
50
|
+
parts = []
|
|
51
|
+
|
|
52
|
+
# Background fill (note: PlantUML uses 'back:', not just fill)
|
|
53
|
+
if self.fill:
|
|
54
|
+
fill_hex = self.fill.lstrip('#')
|
|
55
|
+
parts.append(f"back:{fill_hex}")
|
|
56
|
+
|
|
57
|
+
# Border colour and style
|
|
58
|
+
if self.border:
|
|
59
|
+
border_hex = self.border.lstrip('#')
|
|
60
|
+
parts.append(f"line:{border_hex}")
|
|
61
|
+
|
|
62
|
+
if self.line_style:
|
|
63
|
+
parts.append(f"line.{self.line_style}")
|
|
64
|
+
|
|
65
|
+
# Text colour
|
|
66
|
+
if self.text:
|
|
67
|
+
text_hex = self.text.lstrip('#')
|
|
68
|
+
parts.append(f"text:{text_hex}")
|
|
69
|
+
|
|
70
|
+
return f"#{';'.join(parts)}" if parts else ""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ArrowStyle:
|
|
74
|
+
"""Style specification for relationship arrows.
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
color: Arrow line color
|
|
78
|
+
thickness: Line thickness (e.g., 1, 2, 3)
|
|
79
|
+
style: Line style ('bold', 'dashed', 'dotted', 'hidden')
|
|
80
|
+
label_color: Color for relationship labels
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, config: dict[str, Any]):
|
|
84
|
+
"""Initialize arrow style from configuration.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
config: Dictionary with arrow style specifications
|
|
88
|
+
"""
|
|
89
|
+
self.color = config.get("color", "#000000")
|
|
90
|
+
self.thickness = config.get("thickness")
|
|
91
|
+
self.style = config.get("style")
|
|
92
|
+
self.label_color = config.get("label_color")
|
|
93
|
+
|
|
94
|
+
def to_plantuml_directive(self) -> Optional[str]:
|
|
95
|
+
"""Generate PlantUML skinparam directive for this arrow style.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Skinparam directive or None if no customization needed
|
|
99
|
+
"""
|
|
100
|
+
if not (self.color or self.thickness or self.style):
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
parts = []
|
|
104
|
+
if self.color:
|
|
105
|
+
parts.append(f"skinparam arrowColor {self.color}")
|
|
106
|
+
if self.thickness:
|
|
107
|
+
parts.append(f"skinparam arrowThickness {self.thickness}")
|
|
108
|
+
|
|
109
|
+
return "\n".join(parts) if parts else None
|
|
110
|
+
|
|
111
|
+
def __repr__(self) -> str:
|
|
112
|
+
return f"ArrowStyle(color={self.color}, style={self.style})"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ArrowColorConfig:
|
|
116
|
+
"""Configuration for arrow colors based on relationship type.
|
|
117
|
+
|
|
118
|
+
Attributes:
|
|
119
|
+
type_arrow_color: Color for rdf:type relationships (default red)
|
|
120
|
+
subclass_arrow_color: Color for rdfs:subClassOf relationships
|
|
121
|
+
property_arrow_color: Color for object property relationships
|
|
122
|
+
domain_range_arrow_color: Color for domain/range relationships
|
|
123
|
+
datatype_arrow_color: Color for datatype property relationships
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(self, config: dict[str, str]):
|
|
127
|
+
"""Initialize arrow colors from configuration.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
config: Dictionary mapping relationship types to hex colors
|
|
131
|
+
"""
|
|
132
|
+
self.type_arrow_color = config.get("type", "#FF0000") # Red
|
|
133
|
+
self.subclass_arrow_color = config.get("subclass", "#000000") # Black
|
|
134
|
+
self.property_arrow_color = config.get("property", "#000000") # Black
|
|
135
|
+
self.domain_range_arrow_color = config.get("domain_range", "#000000") # Black
|
|
136
|
+
self.datatype_arrow_color = config.get("datatype", "#000000") # Black
|
|
137
|
+
|
|
138
|
+
def get_color(self, relationship_type: str) -> str:
|
|
139
|
+
"""Get color for a specific relationship type.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
relationship_type: Type of relationship
|
|
143
|
+
('type', 'subclass', 'property', 'domain_range', 'datatype')
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Hex color code
|
|
147
|
+
"""
|
|
148
|
+
color_map = {
|
|
149
|
+
"type": self.type_arrow_color,
|
|
150
|
+
"subclass": self.subclass_arrow_color,
|
|
151
|
+
"property": self.property_arrow_color,
|
|
152
|
+
"domain_range": self.domain_range_arrow_color,
|
|
153
|
+
"datatype": self.datatype_arrow_color,
|
|
154
|
+
}
|
|
155
|
+
return color_map.get(relationship_type, "#000000")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class StyleScheme:
|
|
159
|
+
"""Complete styling scheme for UML diagrams.
|
|
160
|
+
|
|
161
|
+
Attributes:
|
|
162
|
+
name: Scheme identifier
|
|
163
|
+
description: Human-readable description
|
|
164
|
+
class_styles: Mapping of class patterns to color palettes
|
|
165
|
+
instance_styles: Mapping of instance type patterns to colour palettes
|
|
166
|
+
instance_style_default: Default style for instances (fallback)
|
|
167
|
+
arrow_styles: Mapping of relationship types to arrow styles
|
|
168
|
+
show_stereotypes: Whether to display UML stereotypes
|
|
169
|
+
stereotype_map: Mapping of RDF types to stereotype labels
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def __init__(self, name: str, config: dict[str, Any]):
|
|
173
|
+
"""Initialise style scheme from configuration.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
name: Scheme identifier
|
|
177
|
+
config: Style configuration dictionary from YAML
|
|
178
|
+
"""
|
|
179
|
+
self.name = name
|
|
180
|
+
self.description = config.get("description", "")
|
|
181
|
+
|
|
182
|
+
# Class styling
|
|
183
|
+
class_config = config.get("classes", {})
|
|
184
|
+
self.class_styles = {}
|
|
185
|
+
|
|
186
|
+
# By namespace
|
|
187
|
+
by_namespace = class_config.get("by_namespace", {})
|
|
188
|
+
for ns_prefix, palette_config in by_namespace.items():
|
|
189
|
+
self.class_styles[f"ns:{ns_prefix}"] = ColorPalette(palette_config)
|
|
190
|
+
|
|
191
|
+
# By type (for specific classes)
|
|
192
|
+
by_type = class_config.get("by_type", {})
|
|
193
|
+
for type_key, palette_config in by_type.items():
|
|
194
|
+
self.class_styles[f"type:{type_key}"] = ColorPalette(palette_config)
|
|
195
|
+
|
|
196
|
+
# Default class style
|
|
197
|
+
if "default" in class_config:
|
|
198
|
+
self.class_styles["default"] = ColorPalette(class_config["default"])
|
|
199
|
+
|
|
200
|
+
# Instance styling
|
|
201
|
+
instance_config = config.get("instances", {})
|
|
202
|
+
self.instance_styles = {}
|
|
203
|
+
|
|
204
|
+
# Load by_type instance styles
|
|
205
|
+
by_type_instances = instance_config.get("by_type", {})
|
|
206
|
+
for type_key, palette_config in by_type_instances.items():
|
|
207
|
+
self.instance_styles[f"type:{type_key}"] = ColorPalette(palette_config)
|
|
208
|
+
|
|
209
|
+
# Default instance style (fallback if no by_type match)
|
|
210
|
+
if "default" in instance_config:
|
|
211
|
+
self.instance_style_default = ColorPalette(instance_config["default"])
|
|
212
|
+
else:
|
|
213
|
+
# Legacy support: if no 'default' key but instance_config has color keys
|
|
214
|
+
# treat the whole config as a palette
|
|
215
|
+
if any(k in instance_config for k in ["border", "fill", "text"]):
|
|
216
|
+
self.instance_style_default = ColorPalette(instance_config)
|
|
217
|
+
else:
|
|
218
|
+
self.instance_style_default = None
|
|
219
|
+
|
|
220
|
+
# Legacy support for inherit_class_border flag
|
|
221
|
+
self.instance_inherit_class_border = instance_config.get(
|
|
222
|
+
"inherit_class_border", False
|
|
223
|
+
)
|
|
224
|
+
# Inherit_class_text flag (for text colour matching class fill)
|
|
225
|
+
self.instance_inherit_class_text = instance_config.get(
|
|
226
|
+
"inherit_class_text", False
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Arrow styling
|
|
230
|
+
arrow_config = config.get("arrows", {})
|
|
231
|
+
self.arrow_styles = {}
|
|
232
|
+
for arrow_type, arrow_cfg in arrow_config.items():
|
|
233
|
+
self.arrow_styles[arrow_type] = ArrowStyle(arrow_cfg)
|
|
234
|
+
|
|
235
|
+
# Arrow color configuration
|
|
236
|
+
arrow_color_config = config.get("arrow_colors", {})
|
|
237
|
+
self.arrow_colors = ArrowColorConfig(arrow_color_config)
|
|
238
|
+
|
|
239
|
+
# Stereotype configuration
|
|
240
|
+
self.show_stereotypes = config.get("show_stereotypes", False)
|
|
241
|
+
self.stereotype_map = config.get("stereotype_map", {})
|
|
242
|
+
|
|
243
|
+
def get_class_style(
|
|
244
|
+
self, graph: Graph, cls: URIRef, is_instance: bool = False
|
|
245
|
+
) -> Optional[ColorPalette]:
|
|
246
|
+
"""Get color palette for a specific class or instance.
|
|
247
|
+
|
|
248
|
+
Selection priority:
|
|
249
|
+
1. Instance-specific styling (if is_instance=True) via get_instance_style()
|
|
250
|
+
2. Explicit type mapping (by_type)
|
|
251
|
+
3. Inheritance-based lookup (traverse rdfs:subClassOf)
|
|
252
|
+
4. Namespace-based coloring (by_namespace)
|
|
253
|
+
5. Default class style
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
graph: RDF graph containing the class
|
|
257
|
+
cls: Class URI
|
|
258
|
+
is_instance: Whether this is an instance rather than a class
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
ColorPalette or None if no style defined
|
|
262
|
+
"""
|
|
263
|
+
# Priority 1: Instance-specific styling
|
|
264
|
+
if is_instance:
|
|
265
|
+
return self.get_instance_style(graph, cls)
|
|
266
|
+
|
|
267
|
+
# Priority 2: Check for explicit type mapping
|
|
268
|
+
qn = graph.namespace_manager.normalizeUri(cls)
|
|
269
|
+
type_key = f"type:{qn}"
|
|
270
|
+
if type_key in self.class_styles:
|
|
271
|
+
return self.class_styles[type_key]
|
|
272
|
+
|
|
273
|
+
# Priority 3: INHERITANCE-BASED LOOKUP
|
|
274
|
+
# Walk up rdfs:subClassOf hierarchy to find styled superclass
|
|
275
|
+
style = self._get_inherited_style(graph, cls)
|
|
276
|
+
if style:
|
|
277
|
+
return style
|
|
278
|
+
|
|
279
|
+
# Priority 4: Namespace-based coloring
|
|
280
|
+
if ":" in qn:
|
|
281
|
+
ns_prefix = qn.split(":")[0]
|
|
282
|
+
ns_key = f"ns:{ns_prefix}"
|
|
283
|
+
if ns_key in self.class_styles:
|
|
284
|
+
return self.class_styles[ns_key]
|
|
285
|
+
|
|
286
|
+
# Priority 5: Default
|
|
287
|
+
return self.class_styles.get("default")
|
|
288
|
+
|
|
289
|
+
def get_instance_style(
|
|
290
|
+
self, graph: Graph, instance: URIRef
|
|
291
|
+
) -> Optional[ColorPalette]:
|
|
292
|
+
"""Get color palette for an instance based on its rdf:type hierarchy.
|
|
293
|
+
|
|
294
|
+
Selection priority:
|
|
295
|
+
1. Explicit type mapping in instances.by_type (using first rdf:type)
|
|
296
|
+
2. Walk up rdf:type's superclass hierarchy to find styled class
|
|
297
|
+
3. Default instance style
|
|
298
|
+
4. Fall back to None
|
|
299
|
+
|
|
300
|
+
If inherit_class_text is enabled, text color matches the class border color.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
graph: RDF graph containing the instance
|
|
304
|
+
instance: Instance URI
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
ColorPalette for the instance, or None if no style defined
|
|
308
|
+
"""
|
|
309
|
+
# Get all rdf:type declarations for this instance
|
|
310
|
+
instance_types = list(graph.objects(instance, RDF.type))
|
|
311
|
+
|
|
312
|
+
# Filter out metaclass types that shouldn't affect instance styling
|
|
313
|
+
metaclass_types = {
|
|
314
|
+
OWL.Class, RDFS.Class,
|
|
315
|
+
OWL.ObjectProperty, OWL.DatatypeProperty,
|
|
316
|
+
OWL.AnnotationProperty, RDF.Property
|
|
317
|
+
}
|
|
318
|
+
valid_types = [t for t in instance_types if t not in metaclass_types]
|
|
319
|
+
|
|
320
|
+
if not valid_types:
|
|
321
|
+
# No valid types - use default
|
|
322
|
+
return self.instance_style_default
|
|
323
|
+
|
|
324
|
+
# Use the first declared type as primary
|
|
325
|
+
primary_type = valid_types[0]
|
|
326
|
+
primary_type_qn = graph.namespace_manager.normalizeUri(primary_type)
|
|
327
|
+
|
|
328
|
+
# Priority 1: Check for explicit instance type styling
|
|
329
|
+
type_key = f"type:{primary_type_qn}"
|
|
330
|
+
if type_key in self.instance_styles:
|
|
331
|
+
palette = self.instance_styles[type_key]
|
|
332
|
+
|
|
333
|
+
# Apply text color inheritance if enabled
|
|
334
|
+
if self.instance_inherit_class_text and palette:
|
|
335
|
+
return self._apply_class_text_inheritance(
|
|
336
|
+
graph, primary_type, palette
|
|
337
|
+
)
|
|
338
|
+
return palette
|
|
339
|
+
|
|
340
|
+
# Priority 2: Walk up the type's class hierarchy to find styled class
|
|
341
|
+
# This allows instances to inherit colors from their class hierarchy
|
|
342
|
+
styled_class_palette = self._get_inherited_style(graph, primary_type)
|
|
343
|
+
if styled_class_palette:
|
|
344
|
+
# Create instance-specific palette based on class colors
|
|
345
|
+
instance_palette = ColorPalette({
|
|
346
|
+
"border": styled_class_palette.border,
|
|
347
|
+
"fill": "#000000", # Instances have black fill
|
|
348
|
+
"text": styled_class_palette.border
|
|
349
|
+
})
|
|
350
|
+
return instance_palette
|
|
351
|
+
|
|
352
|
+
# Priority 3: Default instance style
|
|
353
|
+
if self.instance_style_default:
|
|
354
|
+
if self.instance_inherit_class_text:
|
|
355
|
+
return self._apply_class_text_inheritance(
|
|
356
|
+
graph, primary_type, self.instance_style_default
|
|
357
|
+
)
|
|
358
|
+
return self.instance_style_default
|
|
359
|
+
|
|
360
|
+
# No styling found
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
def _apply_class_text_inheritance(
|
|
364
|
+
self, graph: Graph, class_uri: URIRef, base_palette: ColorPalette
|
|
365
|
+
) -> ColorPalette:
|
|
366
|
+
"""Apply class text color inheritance to an instance palette.
|
|
367
|
+
|
|
368
|
+
Looks up the class's border color and applies it as text color.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
graph: RDF graph
|
|
372
|
+
class_uri: Class URI to get colors from
|
|
373
|
+
base_palette: Base instance palette to modify
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
New ColorPalette with inherited text color
|
|
377
|
+
"""
|
|
378
|
+
# Get the class's styling
|
|
379
|
+
class_palette = self.get_class_style(graph, class_uri, is_instance=False)
|
|
380
|
+
|
|
381
|
+
if class_palette and class_palette.border:
|
|
382
|
+
# Use class border color as instance text color
|
|
383
|
+
return ColorPalette({
|
|
384
|
+
"border": base_palette.border,
|
|
385
|
+
"fill": base_palette.fill,
|
|
386
|
+
"text": class_palette.border,
|
|
387
|
+
"line_style": base_palette.line_style
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
return base_palette
|
|
391
|
+
|
|
392
|
+
def _get_inherited_style(
|
|
393
|
+
self, graph: Graph, cls: URIRef, visited: Optional[set] = None
|
|
394
|
+
) -> Optional[ColorPalette]:
|
|
395
|
+
"""Walk up rdfs:subClassOf hierarchy to find styled superclass.
|
|
396
|
+
|
|
397
|
+
This enables classes to inherit styles from their superclasses.
|
|
398
|
+
For example, building:Structure inherits from ies:Entity,
|
|
399
|
+
so it should get Entity's yellow color.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
graph: RDF graph containing the class hierarchy
|
|
403
|
+
cls: Class URI to find style for
|
|
404
|
+
visited: Set of already-visited classes (prevents infinite loops)
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
ColorPalette from nearest styled superclass, or None
|
|
408
|
+
"""
|
|
409
|
+
if visited is None:
|
|
410
|
+
visited = set()
|
|
411
|
+
|
|
412
|
+
# Prevent infinite loops in case of circular inheritance
|
|
413
|
+
if cls in visited:
|
|
414
|
+
return None
|
|
415
|
+
visited.add(cls)
|
|
416
|
+
|
|
417
|
+
# Get all direct superclasses
|
|
418
|
+
superclasses = list(graph.objects(cls, RDFS.subClassOf))
|
|
419
|
+
|
|
420
|
+
# Check each superclass
|
|
421
|
+
for superclass in superclasses:
|
|
422
|
+
# Skip if not a proper URI (could be blank node)
|
|
423
|
+
if not isinstance(superclass, URIRef):
|
|
424
|
+
continue
|
|
425
|
+
|
|
426
|
+
# Check if this superclass has explicit styling
|
|
427
|
+
super_qn = graph.namespace_manager.normalizeUri(superclass)
|
|
428
|
+
type_key = f"type:{super_qn}"
|
|
429
|
+
|
|
430
|
+
if type_key in self.class_styles:
|
|
431
|
+
# Found a styled superclass!
|
|
432
|
+
return self.class_styles[type_key]
|
|
433
|
+
|
|
434
|
+
# Recursively check this superclass's parents
|
|
435
|
+
inherited = self._get_inherited_style(graph, superclass, visited)
|
|
436
|
+
if inherited:
|
|
437
|
+
return inherited
|
|
438
|
+
|
|
439
|
+
# No styled superclass found
|
|
440
|
+
return None
|
|
441
|
+
|
|
442
|
+
def get_property_style(
|
|
443
|
+
self, graph: Graph, prop: URIRef
|
|
444
|
+
) -> Optional[ColorPalette]:
|
|
445
|
+
"""Get colour palette for property class.
|
|
446
|
+
|
|
447
|
+
Properties render as classes with specific styling (typically gray).
|
|
448
|
+
Can be customised by namespace or specific property types.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
graph: RDF graph containing the property
|
|
452
|
+
prop: Property URI
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Color palette for the property, or None for default styling
|
|
456
|
+
"""
|
|
457
|
+
# Check for property-specific styling first
|
|
458
|
+
# Get QName using graph's namespace manager
|
|
459
|
+
prop_qname = graph.namespace_manager.normalizeUri(prop)
|
|
460
|
+
if prop_qname in self.class_styles:
|
|
461
|
+
return self.class_styles[prop_qname]
|
|
462
|
+
|
|
463
|
+
# Check namespace-based styling
|
|
464
|
+
if ":" in prop_qname:
|
|
465
|
+
ns_prefix = prop_qname.split(":")[0]
|
|
466
|
+
ns_key = f"ns:{ns_prefix}"
|
|
467
|
+
if ns_key in self.class_styles:
|
|
468
|
+
return self.class_styles[ns_key]
|
|
469
|
+
|
|
470
|
+
# Check for property type styling
|
|
471
|
+
# (e.g., different colors for ObjectProperty vs DatatypeProperty)
|
|
472
|
+
for prop_type in graph.objects(prop, RDF.type):
|
|
473
|
+
# Get QName using graph's namespace manager
|
|
474
|
+
type_qname = graph.namespace_manager.normalizeUri(prop_type)
|
|
475
|
+
type_key = f"type:{type_qname}"
|
|
476
|
+
if type_key in self.class_styles:
|
|
477
|
+
return self.class_styles[type_key]
|
|
478
|
+
|
|
479
|
+
# Default: gray for all properties
|
|
480
|
+
return ColorPalette({
|
|
481
|
+
"fill": "#CCCCCC",
|
|
482
|
+
"border": "#666666",
|
|
483
|
+
"text": "#000000"
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
def get_arrow_style(self, relationship_type: str) -> Optional[ArrowStyle]:
|
|
487
|
+
"""Get arrow style for a relationship type.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
relationship_type: Type of relationship ('subclass', 'instance',
|
|
491
|
+
'object_property', 'rdf_type', etc.)
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
ArrowStyle or None if no specific style defined
|
|
495
|
+
"""
|
|
496
|
+
return self.arrow_styles.get(relationship_type)
|
|
497
|
+
|
|
498
|
+
def get_stereotype(
|
|
499
|
+
self, graph: Graph, entity: URIRef, is_instance: bool = False
|
|
500
|
+
) -> Optional[str]:
|
|
501
|
+
"""Get stereotype label for entity."""
|
|
502
|
+
if not self.show_stereotypes:
|
|
503
|
+
return None
|
|
504
|
+
|
|
505
|
+
# Handle instances with multiple types
|
|
506
|
+
if is_instance:
|
|
507
|
+
types = []
|
|
508
|
+
metaclass_types = {
|
|
509
|
+
"owl:Class", "rdfs:Class",
|
|
510
|
+
"owl:ObjectProperty", "owl:DatatypeProperty",
|
|
511
|
+
"owl:AnnotationProperty", "rdf:Property"
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
for rdf_type in graph.objects(entity, RDF.type):
|
|
515
|
+
# Get QName using graph's namespace manager
|
|
516
|
+
type_qname = graph.namespace_manager.normalizeUri(rdf_type)
|
|
517
|
+
if type_qname not in metaclass_types:
|
|
518
|
+
types.append(type_qname)
|
|
519
|
+
|
|
520
|
+
if types:
|
|
521
|
+
types.sort()
|
|
522
|
+
return f"<<{', '.join(types)}>>"
|
|
523
|
+
return "<<owl:NamedIndividual>>"
|
|
524
|
+
|
|
525
|
+
# Handle classes/properties (existing logic)
|
|
526
|
+
for rdf_type in graph.objects(entity, RDF.type):
|
|
527
|
+
# Get QName using graph's namespace manager
|
|
528
|
+
type_qname = graph.namespace_manager.normalizeUri(rdf_type)
|
|
529
|
+
if type_qname in self.stereotype_map:
|
|
530
|
+
return self.stereotype_map[type_qname]
|
|
531
|
+
if type_qname in ("owl:Class", "rdfs:Class",
|
|
532
|
+
"owl:ObjectProperty", "owl:DatatypeProperty",
|
|
533
|
+
"owl:AnnotationProperty", "rdf:Property"):
|
|
534
|
+
return f"<<{type_qname}>>"
|
|
535
|
+
|
|
536
|
+
return None
|
|
537
|
+
|
|
538
|
+
def __repr__(self) -> str:
|
|
539
|
+
return (
|
|
540
|
+
f"StyleScheme(name={self.name!r}, "
|
|
541
|
+
f"classes={len(self.class_styles)}, "
|
|
542
|
+
f"instances={len(self.instance_styles)})"
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class StyleConfig:
|
|
547
|
+
"""Configuration for PlantUML styling.
|
|
548
|
+
|
|
549
|
+
Loads and manages YAML-based style specifications with support
|
|
550
|
+
for multiple schemes and shared configuration via YAML anchors.
|
|
551
|
+
|
|
552
|
+
Attributes:
|
|
553
|
+
defaults: Default styling settings
|
|
554
|
+
schemes: Dictionary of available style schemes
|
|
555
|
+
"""
|
|
556
|
+
|
|
557
|
+
def __init__(self, yaml_path: Path | str):
|
|
558
|
+
"""Load style configuration from a YAML file.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
yaml_path: Path to YAML style configuration file
|
|
562
|
+
"""
|
|
563
|
+
yaml_path = Path(yaml_path)
|
|
564
|
+
self.config = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
|
|
565
|
+
|
|
566
|
+
self.defaults = self.config.get("defaults", {}) or {}
|
|
567
|
+
|
|
568
|
+
# Load schemes
|
|
569
|
+
self.schemes = {}
|
|
570
|
+
for scheme_name, scheme_config in (self.config.get("schemes", {}) or {}).items():
|
|
571
|
+
self.schemes[scheme_name] = StyleScheme(scheme_name, scheme_config)
|
|
572
|
+
|
|
573
|
+
def get_scheme(self, name: str) -> StyleScheme:
|
|
574
|
+
"""Get a style scheme by name.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
name: Scheme identifier
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
StyleScheme instance
|
|
581
|
+
|
|
582
|
+
Raises:
|
|
583
|
+
KeyError: If scheme name not found
|
|
584
|
+
"""
|
|
585
|
+
if name not in self.schemes:
|
|
586
|
+
raise KeyError(
|
|
587
|
+
f"Style scheme '{name}' not found. Available schemes: "
|
|
588
|
+
f"{', '.join(self.schemes.keys())}"
|
|
589
|
+
)
|
|
590
|
+
return self.schemes[name]
|
|
591
|
+
|
|
592
|
+
def list_schemes(self) -> list[str]:
|
|
593
|
+
"""Get list of available scheme names.
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
List of scheme identifier strings
|
|
597
|
+
"""
|
|
598
|
+
return list(self.schemes.keys())
|
|
599
|
+
|
|
600
|
+
def __repr__(self) -> str:
|
|
601
|
+
return f"StyleConfig(schemes={list(self.schemes.keys())})"
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def load_style_config(path: Path | str) -> StyleConfig:
|
|
605
|
+
"""Load style configuration from a YAML file.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
path: Path to YAML style configuration file
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
StyleConfig instance
|
|
612
|
+
"""
|
|
613
|
+
return StyleConfig(path)
|