rdf-construct 0.2.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.
Files changed (88) hide show
  1. rdf_construct/__init__.py +12 -0
  2. rdf_construct/__main__.py +0 -0
  3. rdf_construct/cli.py +1762 -0
  4. rdf_construct/core/__init__.py +33 -0
  5. rdf_construct/core/config.py +116 -0
  6. rdf_construct/core/ordering.py +219 -0
  7. rdf_construct/core/predicate_order.py +212 -0
  8. rdf_construct/core/profile.py +157 -0
  9. rdf_construct/core/selector.py +64 -0
  10. rdf_construct/core/serialiser.py +232 -0
  11. rdf_construct/core/utils.py +89 -0
  12. rdf_construct/cq/__init__.py +77 -0
  13. rdf_construct/cq/expectations.py +365 -0
  14. rdf_construct/cq/formatters/__init__.py +45 -0
  15. rdf_construct/cq/formatters/json.py +104 -0
  16. rdf_construct/cq/formatters/junit.py +104 -0
  17. rdf_construct/cq/formatters/text.py +146 -0
  18. rdf_construct/cq/loader.py +300 -0
  19. rdf_construct/cq/runner.py +321 -0
  20. rdf_construct/diff/__init__.py +59 -0
  21. rdf_construct/diff/change_types.py +214 -0
  22. rdf_construct/diff/comparator.py +338 -0
  23. rdf_construct/diff/filters.py +133 -0
  24. rdf_construct/diff/formatters/__init__.py +71 -0
  25. rdf_construct/diff/formatters/json.py +192 -0
  26. rdf_construct/diff/formatters/markdown.py +210 -0
  27. rdf_construct/diff/formatters/text.py +195 -0
  28. rdf_construct/docs/__init__.py +60 -0
  29. rdf_construct/docs/config.py +238 -0
  30. rdf_construct/docs/extractors.py +603 -0
  31. rdf_construct/docs/generator.py +360 -0
  32. rdf_construct/docs/renderers/__init__.py +7 -0
  33. rdf_construct/docs/renderers/html.py +803 -0
  34. rdf_construct/docs/renderers/json.py +390 -0
  35. rdf_construct/docs/renderers/markdown.py +628 -0
  36. rdf_construct/docs/search.py +278 -0
  37. rdf_construct/docs/templates/html/base.html.jinja +44 -0
  38. rdf_construct/docs/templates/html/class.html.jinja +152 -0
  39. rdf_construct/docs/templates/html/hierarchy.html.jinja +28 -0
  40. rdf_construct/docs/templates/html/index.html.jinja +110 -0
  41. rdf_construct/docs/templates/html/instance.html.jinja +90 -0
  42. rdf_construct/docs/templates/html/namespaces.html.jinja +37 -0
  43. rdf_construct/docs/templates/html/property.html.jinja +124 -0
  44. rdf_construct/docs/templates/html/single_page.html.jinja +169 -0
  45. rdf_construct/lint/__init__.py +75 -0
  46. rdf_construct/lint/config.py +214 -0
  47. rdf_construct/lint/engine.py +396 -0
  48. rdf_construct/lint/formatters.py +327 -0
  49. rdf_construct/lint/rules.py +692 -0
  50. rdf_construct/main.py +6 -0
  51. rdf_construct/puml2rdf/__init__.py +103 -0
  52. rdf_construct/puml2rdf/config.py +230 -0
  53. rdf_construct/puml2rdf/converter.py +420 -0
  54. rdf_construct/puml2rdf/merger.py +200 -0
  55. rdf_construct/puml2rdf/model.py +202 -0
  56. rdf_construct/puml2rdf/parser.py +565 -0
  57. rdf_construct/puml2rdf/validators.py +451 -0
  58. rdf_construct/shacl/__init__.py +56 -0
  59. rdf_construct/shacl/config.py +166 -0
  60. rdf_construct/shacl/converters.py +520 -0
  61. rdf_construct/shacl/generator.py +364 -0
  62. rdf_construct/shacl/namespaces.py +93 -0
  63. rdf_construct/stats/__init__.py +29 -0
  64. rdf_construct/stats/collector.py +178 -0
  65. rdf_construct/stats/comparator.py +298 -0
  66. rdf_construct/stats/formatters/__init__.py +83 -0
  67. rdf_construct/stats/formatters/json.py +38 -0
  68. rdf_construct/stats/formatters/markdown.py +153 -0
  69. rdf_construct/stats/formatters/text.py +186 -0
  70. rdf_construct/stats/metrics/__init__.py +26 -0
  71. rdf_construct/stats/metrics/basic.py +147 -0
  72. rdf_construct/stats/metrics/complexity.py +137 -0
  73. rdf_construct/stats/metrics/connectivity.py +130 -0
  74. rdf_construct/stats/metrics/documentation.py +128 -0
  75. rdf_construct/stats/metrics/hierarchy.py +207 -0
  76. rdf_construct/stats/metrics/properties.py +88 -0
  77. rdf_construct/uml/__init__.py +22 -0
  78. rdf_construct/uml/context.py +194 -0
  79. rdf_construct/uml/mapper.py +371 -0
  80. rdf_construct/uml/odm_renderer.py +789 -0
  81. rdf_construct/uml/renderer.py +684 -0
  82. rdf_construct/uml/uml_layout.py +393 -0
  83. rdf_construct/uml/uml_style.py +613 -0
  84. rdf_construct-0.2.0.dist-info/METADATA +431 -0
  85. rdf_construct-0.2.0.dist-info/RECORD +88 -0
  86. rdf_construct-0.2.0.dist-info/WHEEL +4 -0
  87. rdf_construct-0.2.0.dist-info/entry_points.txt +3 -0
  88. rdf_construct-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,628 @@
1
+ """Markdown documentation renderer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ if TYPE_CHECKING:
9
+ from ..config import DocsConfig
10
+ from ..extractors import ClassInfo, ExtractedEntities, InstanceInfo, PropertyInfo
11
+
12
+
13
+ class MarkdownRenderer:
14
+ """Renders ontology documentation as Markdown files.
15
+
16
+ Generates GitHub/GitLab-compatible Markdown with optional
17
+ Jekyll/Hugo frontmatter.
18
+ """
19
+
20
+ def __init__(self, config: "DocsConfig") -> None:
21
+ """Initialise the Markdown renderer.
22
+
23
+ Args:
24
+ config: Documentation configuration.
25
+ """
26
+ self.config = config
27
+
28
+ def _get_output_path(self, filename: str, subdir: str | None = None) -> Path:
29
+ """Get the full output path for a file.
30
+
31
+ Args:
32
+ filename: Name of the file.
33
+ subdir: Optional subdirectory.
34
+
35
+ Returns:
36
+ Full output path.
37
+ """
38
+ if subdir:
39
+ path = self.config.output_dir / subdir / filename
40
+ else:
41
+ path = self.config.output_dir / filename
42
+
43
+ path.parent.mkdir(parents=True, exist_ok=True)
44
+ return path
45
+
46
+ def _write_file(self, path: Path, content: str) -> Path:
47
+ """Write content to a file.
48
+
49
+ Args:
50
+ path: Output path.
51
+ content: Content to write.
52
+
53
+ Returns:
54
+ Path to the written file.
55
+ """
56
+ path.parent.mkdir(parents=True, exist_ok=True)
57
+ path.write_text(content, encoding="utf-8")
58
+ return path
59
+
60
+ def _entity_link(self, qname: str, entity_type: str, label: str | None = None) -> str:
61
+ """Generate a markdown link to an entity.
62
+
63
+ Args:
64
+ qname: Entity qualified name.
65
+ entity_type: Type of entity.
66
+ label: Optional display label.
67
+
68
+ Returns:
69
+ Markdown link string.
70
+ """
71
+ from ..config import entity_to_path
72
+
73
+ display = label or qname
74
+ path = entity_to_path(qname, entity_type, self.config, extension=".md")
75
+ # Make path relative from root
76
+ return f"[{display}]({path})"
77
+
78
+ def _frontmatter(self, **kwargs: Any) -> str:
79
+ """Generate YAML frontmatter.
80
+
81
+ Args:
82
+ **kwargs: Frontmatter fields.
83
+
84
+ Returns:
85
+ Frontmatter string.
86
+ """
87
+ lines = ["---"]
88
+ for key, value in kwargs.items():
89
+ if isinstance(value, list):
90
+ lines.append(f"{key}:")
91
+ for item in value:
92
+ lines.append(f" - {item}")
93
+ else:
94
+ lines.append(f"{key}: {value}")
95
+ lines.append("---")
96
+ lines.append("")
97
+ return "\n".join(lines)
98
+
99
+ def render_index(self, entities: "ExtractedEntities") -> Path:
100
+ """Render the main index page.
101
+
102
+ Args:
103
+ entities: All extracted entities.
104
+
105
+ Returns:
106
+ Path to the rendered file.
107
+ """
108
+ lines = []
109
+
110
+ # Frontmatter
111
+ lines.append(self._frontmatter(
112
+ title=entities.ontology.title or "Ontology Documentation",
113
+ layout="default",
114
+ ))
115
+
116
+ # Header
117
+ lines.append(f"# {entities.ontology.title or 'Ontology Documentation'}")
118
+ lines.append("")
119
+
120
+ if entities.ontology.description:
121
+ lines.append(entities.ontology.description)
122
+ lines.append("")
123
+
124
+ # Statistics
125
+ lines.append("## Overview")
126
+ lines.append("")
127
+ lines.append(f"- **Classes:** {len(entities.classes)}")
128
+ lines.append(f"- **Object Properties:** {len(entities.object_properties)}")
129
+ lines.append(f"- **Datatype Properties:** {len(entities.datatype_properties)}")
130
+ lines.append(f"- **Annotation Properties:** {len(entities.annotation_properties)}")
131
+ if entities.instances:
132
+ lines.append(f"- **Instances:** {len(entities.instances)}")
133
+ lines.append("")
134
+
135
+ # Navigation
136
+ lines.append("## Quick Links")
137
+ lines.append("")
138
+ lines.append("- [Class Hierarchy](hierarchy.md)")
139
+ lines.append("- [Namespaces](namespaces.md)")
140
+ lines.append("")
141
+
142
+ # Classes section
143
+ if entities.classes:
144
+ lines.append("## Classes")
145
+ lines.append("")
146
+ for c in entities.classes:
147
+ link = self._entity_link(c.qname, "class", c.label or c.qname)
148
+ if c.definition:
149
+ # Truncate long definitions
150
+ desc = c.definition[:100] + "..." if len(c.definition) > 100 else c.definition
151
+ lines.append(f"- {link} — {desc}")
152
+ else:
153
+ lines.append(f"- {link}")
154
+ lines.append("")
155
+
156
+ # Properties section
157
+ if entities.object_properties:
158
+ lines.append("## Object Properties")
159
+ lines.append("")
160
+ for p in entities.object_properties:
161
+ link = self._entity_link(p.qname, "object_property", p.label or p.qname)
162
+ lines.append(f"- {link}")
163
+ lines.append("")
164
+
165
+ if entities.datatype_properties:
166
+ lines.append("## Datatype Properties")
167
+ lines.append("")
168
+ for p in entities.datatype_properties:
169
+ link = self._entity_link(p.qname, "datatype_property", p.label or p.qname)
170
+ lines.append(f"- {link}")
171
+ lines.append("")
172
+
173
+ content = "\n".join(lines)
174
+ return self._write_file(self._get_output_path("index.md"), content)
175
+
176
+ def render_hierarchy(self, entities: "ExtractedEntities") -> Path:
177
+ """Render the class hierarchy page.
178
+
179
+ Args:
180
+ entities: All extracted entities.
181
+
182
+ Returns:
183
+ Path to the rendered file.
184
+ """
185
+ lines = []
186
+
187
+ lines.append(self._frontmatter(title="Class Hierarchy"))
188
+ lines.append("# Class Hierarchy")
189
+ lines.append("")
190
+
191
+ # Build and render tree
192
+ hierarchy = self._build_hierarchy_tree(entities.classes)
193
+
194
+ def render_tree(nodes: list[dict[str, Any]], indent: int = 0) -> None:
195
+ prefix = " " * indent
196
+ for node in nodes:
197
+ c = node["class"]
198
+ link = self._entity_link(c.qname, "class", c.label or c.qname)
199
+ lines.append(f"{prefix}- {link}")
200
+ if node["children"]:
201
+ render_tree(node["children"], indent + 1)
202
+
203
+ render_tree(hierarchy)
204
+ lines.append("")
205
+
206
+ content = "\n".join(lines)
207
+ return self._write_file(self._get_output_path("hierarchy.md"), content)
208
+
209
+ def _build_hierarchy_tree(
210
+ self,
211
+ classes: list["ClassInfo"],
212
+ ) -> list[dict[str, Any]]:
213
+ """Build a tree structure for the class hierarchy.
214
+
215
+ Args:
216
+ classes: List of all classes.
217
+
218
+ Returns:
219
+ Nested list structure representing the hierarchy.
220
+ """
221
+ class_by_uri = {str(c.uri): c for c in classes}
222
+ internal_uris = set(class_by_uri.keys())
223
+ root_classes = []
224
+
225
+ for c in classes:
226
+ has_internal_parent = any(
227
+ str(parent) in internal_uris for parent in c.superclasses
228
+ )
229
+ if not has_internal_parent:
230
+ root_classes.append(c)
231
+
232
+ def build_node(class_info: "ClassInfo") -> dict[str, Any]:
233
+ children = []
234
+ for child_uri in class_info.subclasses:
235
+ child_key = str(child_uri)
236
+ if child_key in class_by_uri:
237
+ children.append(build_node(class_by_uri[child_key]))
238
+
239
+ return {
240
+ "class": class_info,
241
+ "children": sorted(children, key=lambda n: n["class"].qname),
242
+ }
243
+
244
+ return sorted(
245
+ [build_node(c) for c in root_classes],
246
+ key=lambda n: n["class"].qname,
247
+ )
248
+
249
+ def render_class(
250
+ self,
251
+ class_info: "ClassInfo",
252
+ entities: "ExtractedEntities",
253
+ ) -> Path:
254
+ """Render a class documentation page.
255
+
256
+ Args:
257
+ class_info: Class to render.
258
+ entities: All extracted entities.
259
+
260
+ Returns:
261
+ Path to the rendered file.
262
+ """
263
+ lines = []
264
+
265
+ lines.append(self._frontmatter(
266
+ title=class_info.label or class_info.qname,
267
+ type="class",
268
+ ))
269
+
270
+ lines.append(f"# {class_info.label or class_info.qname}")
271
+ lines.append("")
272
+ lines.append(f"**URI:** `{class_info.uri}`")
273
+ lines.append("")
274
+
275
+ if class_info.definition:
276
+ lines.append(class_info.definition)
277
+ lines.append("")
278
+
279
+ # Superclasses
280
+ if class_info.superclasses:
281
+ lines.append("## Superclasses")
282
+ lines.append("")
283
+ for uri in class_info.superclasses:
284
+ # Try to make a link if we have this class
285
+ qname = self._uri_to_display(uri, entities)
286
+ lines.append(f"- {qname}")
287
+ lines.append("")
288
+
289
+ # Subclasses
290
+ if class_info.subclasses:
291
+ lines.append("## Subclasses")
292
+ lines.append("")
293
+ for uri in class_info.subclasses:
294
+ qname = self._uri_to_display(uri, entities)
295
+ lines.append(f"- {qname}")
296
+ lines.append("")
297
+
298
+ # Domain of (properties where this is domain)
299
+ if class_info.domain_of:
300
+ lines.append("## Properties")
301
+ lines.append("")
302
+ for p in class_info.domain_of:
303
+ link = self._entity_link(p.qname, f"{p.property_type}_property")
304
+ lines.append(f"- {link}")
305
+ lines.append("")
306
+
307
+ # Range of (properties where this is range)
308
+ if class_info.range_of:
309
+ lines.append("## Used as Range")
310
+ lines.append("")
311
+ for p in class_info.range_of:
312
+ link = self._entity_link(p.qname, f"{p.property_type}_property")
313
+ lines.append(f"- {link}")
314
+ lines.append("")
315
+
316
+ # Instances
317
+ if class_info.instances:
318
+ lines.append("## Instances")
319
+ lines.append("")
320
+ for uri in class_info.instances:
321
+ qname = self._uri_to_display(uri, entities, "instance")
322
+ lines.append(f"- {qname}")
323
+ lines.append("")
324
+
325
+ # Annotations
326
+ if class_info.annotations:
327
+ lines.append("## Annotations")
328
+ lines.append("")
329
+ for name, values in class_info.annotations.items():
330
+ for value in values:
331
+ lines.append(f"- **{name}:** {value}")
332
+ lines.append("")
333
+
334
+ content = "\n".join(lines)
335
+ from ..config import entity_to_path
336
+ rel_path = entity_to_path(class_info.qname, "class", self.config, extension=".md")
337
+ return self._write_file(self.config.output_dir / rel_path, content)
338
+
339
+ def _uri_to_display(
340
+ self,
341
+ uri: Any,
342
+ entities: "ExtractedEntities",
343
+ default_type: str = "class",
344
+ ) -> str:
345
+ """Convert a URI to a display string, linking if possible.
346
+
347
+ Args:
348
+ uri: URI to convert.
349
+ entities: All entities for lookups.
350
+ default_type: Entity type if not found.
351
+
352
+ Returns:
353
+ Display string with link if available.
354
+ """
355
+ uri_str = str(uri)
356
+
357
+ # Check if it's a known class
358
+ for c in entities.classes:
359
+ if str(c.uri) == uri_str:
360
+ return self._entity_link(c.qname, "class", c.label or c.qname)
361
+
362
+ # Check if it's a known instance
363
+ for i in entities.instances:
364
+ if str(i.uri) == uri_str:
365
+ return self._entity_link(i.qname, "instance", i.label or i.qname)
366
+
367
+ # Fall back to extracting local name
368
+ if "#" in uri_str:
369
+ return f"`{uri_str.split('#')[-1]}`"
370
+ elif "/" in uri_str:
371
+ return f"`{uri_str.split('/')[-1]}`"
372
+ return f"`{uri_str}`"
373
+
374
+ def render_property(
375
+ self,
376
+ prop_info: "PropertyInfo",
377
+ entities: "ExtractedEntities",
378
+ ) -> Path:
379
+ """Render a property documentation page.
380
+
381
+ Args:
382
+ prop_info: Property to render.
383
+ entities: All extracted entities.
384
+
385
+ Returns:
386
+ Path to the rendered file.
387
+ """
388
+ lines = []
389
+
390
+ type_label = prop_info.property_type.replace("_", " ").title()
391
+ lines.append(self._frontmatter(
392
+ title=prop_info.label or prop_info.qname,
393
+ type=prop_info.property_type,
394
+ ))
395
+
396
+ lines.append(f"# {prop_info.label or prop_info.qname}")
397
+ lines.append("")
398
+ lines.append(f"**Type:** {type_label} Property")
399
+ lines.append("")
400
+ lines.append(f"**URI:** `{prop_info.uri}`")
401
+ lines.append("")
402
+
403
+ if prop_info.definition:
404
+ lines.append(prop_info.definition)
405
+ lines.append("")
406
+
407
+ # Domain
408
+ if prop_info.domain:
409
+ lines.append("## Domain")
410
+ lines.append("")
411
+ for uri in prop_info.domain:
412
+ display = self._uri_to_display(uri, entities)
413
+ lines.append(f"- {display}")
414
+ lines.append("")
415
+
416
+ # Range
417
+ if prop_info.range:
418
+ lines.append("## Range")
419
+ lines.append("")
420
+ for uri in prop_info.range:
421
+ display = self._uri_to_display(uri, entities)
422
+ lines.append(f"- {display}")
423
+ lines.append("")
424
+
425
+ # Characteristics
426
+ characteristics = []
427
+ if prop_info.is_functional:
428
+ characteristics.append("Functional")
429
+ if prop_info.is_inverse_functional:
430
+ characteristics.append("Inverse Functional")
431
+ if prop_info.inverse_of:
432
+ inv_display = self._uri_to_display(prop_info.inverse_of, entities)
433
+ characteristics.append(f"Inverse of {inv_display}")
434
+
435
+ if characteristics:
436
+ lines.append("## Characteristics")
437
+ lines.append("")
438
+ for char in characteristics:
439
+ lines.append(f"- {char}")
440
+ lines.append("")
441
+
442
+ # Super/subproperties
443
+ if prop_info.superproperties:
444
+ lines.append("## Superproperties")
445
+ lines.append("")
446
+ for uri in prop_info.superproperties:
447
+ lines.append(f"- `{uri}`")
448
+ lines.append("")
449
+
450
+ if prop_info.subproperties:
451
+ lines.append("## Subproperties")
452
+ lines.append("")
453
+ for uri in prop_info.subproperties:
454
+ lines.append(f"- `{uri}`")
455
+ lines.append("")
456
+
457
+ content = "\n".join(lines)
458
+ entity_type = f"{prop_info.property_type}_property"
459
+ from ..config import entity_to_path
460
+ rel_path = entity_to_path(prop_info.qname, entity_type, self.config, extension=".md")
461
+ return self._write_file(self.config.output_dir / rel_path, content)
462
+
463
+ def render_instance(
464
+ self,
465
+ instance_info: "InstanceInfo",
466
+ entities: "ExtractedEntities",
467
+ ) -> Path:
468
+ """Render an instance documentation page.
469
+
470
+ Args:
471
+ instance_info: Instance to render.
472
+ entities: All extracted entities.
473
+
474
+ Returns:
475
+ Path to the rendered file.
476
+ """
477
+ lines = []
478
+
479
+ lines.append(self._frontmatter(
480
+ title=instance_info.label or instance_info.qname,
481
+ type="instance",
482
+ ))
483
+
484
+ lines.append(f"# {instance_info.label or instance_info.qname}")
485
+ lines.append("")
486
+ lines.append(f"**URI:** `{instance_info.uri}`")
487
+ lines.append("")
488
+
489
+ if instance_info.definition:
490
+ lines.append(instance_info.definition)
491
+ lines.append("")
492
+
493
+ # Types
494
+ if instance_info.types:
495
+ lines.append("## Types")
496
+ lines.append("")
497
+ for uri in instance_info.types:
498
+ display = self._uri_to_display(uri, entities)
499
+ lines.append(f"- {display}")
500
+ lines.append("")
501
+
502
+ # Properties
503
+ if instance_info.properties:
504
+ lines.append("## Properties")
505
+ lines.append("")
506
+ for pred, values in instance_info.properties.items():
507
+ pred_name = str(pred).split("#")[-1] if "#" in str(pred) else str(pred).split("/")[-1]
508
+ for value in values:
509
+ if isinstance(value, str):
510
+ lines.append(f"- **{pred_name}:** {value}")
511
+ else:
512
+ display = self._uri_to_display(value, entities)
513
+ lines.append(f"- **{pred_name}:** {display}")
514
+ lines.append("")
515
+
516
+ content = "\n".join(lines)
517
+ from ..config import entity_to_path
518
+ rel_path = entity_to_path(instance_info.qname, "instance", self.config, extension=".md")
519
+ return self._write_file(self.config.output_dir / rel_path, content)
520
+
521
+ def render_namespaces(self, entities: "ExtractedEntities") -> Path:
522
+ """Render the namespace reference page.
523
+
524
+ Args:
525
+ entities: All extracted entities.
526
+
527
+ Returns:
528
+ Path to the rendered file.
529
+ """
530
+ lines = []
531
+
532
+ lines.append(self._frontmatter(title="Namespaces"))
533
+ lines.append("# Namespaces")
534
+ lines.append("")
535
+
536
+ if entities.ontology.namespaces:
537
+ lines.append("| Prefix | Namespace |")
538
+ lines.append("|--------|-----------|")
539
+ for prefix, namespace in sorted(entities.ontology.namespaces.items()):
540
+ lines.append(f"| `{prefix}` | `{namespace}` |")
541
+ lines.append("")
542
+
543
+ content = "\n".join(lines)
544
+ return self._write_file(self._get_output_path("namespaces.md"), content)
545
+
546
+ def render_single_page(self, entities: "ExtractedEntities") -> Path:
547
+ """Render all documentation as a single page.
548
+
549
+ Args:
550
+ entities: All extracted entities.
551
+
552
+ Returns:
553
+ Path to the rendered file.
554
+ """
555
+ lines = []
556
+
557
+ lines.append(self._frontmatter(
558
+ title=entities.ontology.title or "Ontology Documentation",
559
+ ))
560
+
561
+ # Header
562
+ lines.append(f"# {entities.ontology.title or 'Ontology Documentation'}")
563
+ lines.append("")
564
+
565
+ if entities.ontology.description:
566
+ lines.append(entities.ontology.description)
567
+ lines.append("")
568
+
569
+ # TOC
570
+ lines.append("## Table of Contents")
571
+ lines.append("")
572
+ lines.append("- [Classes](#classes)")
573
+ lines.append("- [Object Properties](#object-properties)")
574
+ lines.append("- [Datatype Properties](#datatype-properties)")
575
+ lines.append("- [Namespaces](#namespaces)")
576
+ lines.append("")
577
+
578
+ # Classes
579
+ lines.append("## Classes")
580
+ lines.append("")
581
+ for c in entities.classes:
582
+ lines.append(f"### {c.label or c.qname}")
583
+ lines.append("")
584
+ lines.append(f"**URI:** `{c.uri}`")
585
+ lines.append("")
586
+ if c.definition:
587
+ lines.append(c.definition)
588
+ lines.append("")
589
+
590
+ # Properties
591
+ lines.append("## Object Properties")
592
+ lines.append("")
593
+ for p in entities.object_properties:
594
+ lines.append(f"### {p.label or p.qname}")
595
+ lines.append("")
596
+ lines.append(f"**URI:** `{p.uri}`")
597
+ lines.append("")
598
+ if p.definition:
599
+ lines.append(p.definition)
600
+ lines.append("")
601
+
602
+ lines.append("## Datatype Properties")
603
+ lines.append("")
604
+ for p in entities.datatype_properties:
605
+ lines.append(f"### {p.label or p.qname}")
606
+ lines.append("")
607
+ lines.append(f"**URI:** `{p.uri}`")
608
+ lines.append("")
609
+ if p.definition:
610
+ lines.append(p.definition)
611
+ lines.append("")
612
+
613
+ # Namespaces
614
+ lines.append("## Namespaces")
615
+ lines.append("")
616
+ if entities.ontology.namespaces:
617
+ lines.append("| Prefix | Namespace |")
618
+ lines.append("|--------|-----------|")
619
+ for prefix, namespace in sorted(entities.ontology.namespaces.items()):
620
+ lines.append(f"| `{prefix}` | `{namespace}` |")
621
+ lines.append("")
622
+
623
+ content = "\n".join(lines)
624
+ return self._write_file(self._get_output_path("index.md"), content)
625
+
626
+ def copy_assets(self) -> None:
627
+ """Copy static assets. No assets needed for Markdown."""
628
+ pass