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.
Files changed (110) hide show
  1. rdf_construct/__init__.py +12 -0
  2. rdf_construct/__main__.py +0 -0
  3. rdf_construct/cli.py +3429 -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/localise/__init__.py +114 -0
  51. rdf_construct/localise/config.py +508 -0
  52. rdf_construct/localise/extractor.py +427 -0
  53. rdf_construct/localise/formatters/__init__.py +36 -0
  54. rdf_construct/localise/formatters/markdown.py +229 -0
  55. rdf_construct/localise/formatters/text.py +224 -0
  56. rdf_construct/localise/merger.py +346 -0
  57. rdf_construct/localise/reporter.py +356 -0
  58. rdf_construct/main.py +6 -0
  59. rdf_construct/merge/__init__.py +165 -0
  60. rdf_construct/merge/config.py +354 -0
  61. rdf_construct/merge/conflicts.py +281 -0
  62. rdf_construct/merge/formatters.py +426 -0
  63. rdf_construct/merge/merger.py +425 -0
  64. rdf_construct/merge/migrator.py +339 -0
  65. rdf_construct/merge/rules.py +377 -0
  66. rdf_construct/merge/splitter.py +1102 -0
  67. rdf_construct/puml2rdf/__init__.py +103 -0
  68. rdf_construct/puml2rdf/config.py +230 -0
  69. rdf_construct/puml2rdf/converter.py +420 -0
  70. rdf_construct/puml2rdf/merger.py +200 -0
  71. rdf_construct/puml2rdf/model.py +202 -0
  72. rdf_construct/puml2rdf/parser.py +565 -0
  73. rdf_construct/puml2rdf/validators.py +451 -0
  74. rdf_construct/refactor/__init__.py +72 -0
  75. rdf_construct/refactor/config.py +362 -0
  76. rdf_construct/refactor/deprecator.py +328 -0
  77. rdf_construct/refactor/formatters/__init__.py +8 -0
  78. rdf_construct/refactor/formatters/text.py +311 -0
  79. rdf_construct/refactor/renamer.py +294 -0
  80. rdf_construct/shacl/__init__.py +56 -0
  81. rdf_construct/shacl/config.py +166 -0
  82. rdf_construct/shacl/converters.py +520 -0
  83. rdf_construct/shacl/generator.py +364 -0
  84. rdf_construct/shacl/namespaces.py +93 -0
  85. rdf_construct/stats/__init__.py +29 -0
  86. rdf_construct/stats/collector.py +178 -0
  87. rdf_construct/stats/comparator.py +298 -0
  88. rdf_construct/stats/formatters/__init__.py +83 -0
  89. rdf_construct/stats/formatters/json.py +38 -0
  90. rdf_construct/stats/formatters/markdown.py +153 -0
  91. rdf_construct/stats/formatters/text.py +186 -0
  92. rdf_construct/stats/metrics/__init__.py +26 -0
  93. rdf_construct/stats/metrics/basic.py +147 -0
  94. rdf_construct/stats/metrics/complexity.py +137 -0
  95. rdf_construct/stats/metrics/connectivity.py +130 -0
  96. rdf_construct/stats/metrics/documentation.py +128 -0
  97. rdf_construct/stats/metrics/hierarchy.py +207 -0
  98. rdf_construct/stats/metrics/properties.py +88 -0
  99. rdf_construct/uml/__init__.py +22 -0
  100. rdf_construct/uml/context.py +194 -0
  101. rdf_construct/uml/mapper.py +371 -0
  102. rdf_construct/uml/odm_renderer.py +789 -0
  103. rdf_construct/uml/renderer.py +684 -0
  104. rdf_construct/uml/uml_layout.py +393 -0
  105. rdf_construct/uml/uml_style.py +613 -0
  106. rdf_construct-0.3.0.dist-info/METADATA +496 -0
  107. rdf_construct-0.3.0.dist-info/RECORD +110 -0
  108. rdf_construct-0.3.0.dist-info/WHEEL +4 -0
  109. rdf_construct-0.3.0.dist-info/entry_points.txt +3 -0
  110. rdf_construct-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,803 @@
1
+ """HTML documentation renderer using Jinja2 templates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from jinja2 import Environment, FileSystemLoader, PackageLoader, select_autoescape
10
+
11
+ if TYPE_CHECKING:
12
+ from ..config import DocsConfig
13
+ from ..extractors import ClassInfo, ExtractedEntities, InstanceInfo, PropertyInfo
14
+
15
+
16
+ class HTMLRenderer:
17
+ """Renders ontology documentation as HTML pages using Jinja2 templates."""
18
+
19
+ def __init__(self, config: "DocsConfig") -> None:
20
+ """Initialise the HTML renderer.
21
+
22
+ Args:
23
+ config: Documentation configuration.
24
+ """
25
+ self.config = config
26
+ self._env: Environment | None = None
27
+
28
+ @property
29
+ def env(self) -> Environment:
30
+ """Get the Jinja2 environment.
31
+
32
+ Returns:
33
+ Configured Jinja2 Environment.
34
+ """
35
+ if self._env is None:
36
+ self._env = self._create_environment()
37
+ return self._env
38
+
39
+ def _create_environment(self) -> Environment:
40
+ """Create and configure the Jinja2 environment.
41
+
42
+ Returns:
43
+ Configured Environment.
44
+ """
45
+ # Use custom template directory if provided, otherwise package templates
46
+ if self.config.template_dir and self.config.template_dir.exists():
47
+ loader = FileSystemLoader(str(self.config.template_dir / "html"))
48
+ else:
49
+ # Use package templates
50
+ loader = PackageLoader("rdf_construct.docs", "templates/html")
51
+
52
+ env = Environment(
53
+ loader=loader,
54
+ autoescape=select_autoescape(["html", "xml"]),
55
+ trim_blocks=True,
56
+ lstrip_blocks=True,
57
+ )
58
+
59
+ # Add custom filters
60
+ env.filters["entity_url"] = self._entity_url_filter
61
+ env.filters["qname_local"] = self._qname_local_filter
62
+
63
+ # Add global context
64
+ env.globals["config"] = self.config
65
+
66
+ return env
67
+
68
+ def _entity_url_filter(self, uri_or_qname: str, entity_type: str = "class") -> str:
69
+ """Jinja2 filter to generate entity URLs.
70
+
71
+ Args:
72
+ uri_or_qname: URI or QName of the entity.
73
+ entity_type: Type of entity.
74
+
75
+ Returns:
76
+ URL to the entity's documentation page.
77
+ """
78
+ from ..config import entity_to_url
79
+
80
+ # If it looks like a full URI, try to extract local name
81
+ if uri_or_qname.startswith("http"):
82
+ if "#" in uri_or_qname:
83
+ qname = uri_or_qname.split("#")[-1]
84
+ elif "/" in uri_or_qname:
85
+ qname = uri_or_qname.split("/")[-1]
86
+ else:
87
+ qname = uri_or_qname
88
+ else:
89
+ qname = uri_or_qname
90
+
91
+ return entity_to_url(qname, entity_type, self.config)
92
+
93
+ def _qname_local_filter(self, qname: str) -> str:
94
+ """Jinja2 filter to get the local part of a QName.
95
+
96
+ Args:
97
+ qname: Qualified name like 'ex:Building'.
98
+
99
+ Returns:
100
+ Local name like 'Building'.
101
+ """
102
+ if ":" in qname:
103
+ return qname.split(":", 1)[1]
104
+ return qname
105
+
106
+ def _get_output_path(self, filename: str, subdir: str | None = None) -> Path:
107
+ """Get the full output path for a file.
108
+
109
+ Args:
110
+ filename: Name of the file.
111
+ subdir: Optional subdirectory.
112
+
113
+ Returns:
114
+ Full output path.
115
+ """
116
+ if subdir:
117
+ path = self.config.output_dir / subdir / filename
118
+ else:
119
+ path = self.config.output_dir / filename
120
+
121
+ path.parent.mkdir(parents=True, exist_ok=True)
122
+ return path
123
+
124
+ def _write_file(self, path: Path, content: str) -> Path:
125
+ """Write content to a file.
126
+
127
+ Args:
128
+ path: Output path.
129
+ content: Content to write.
130
+
131
+ Returns:
132
+ Path to the written file.
133
+ """
134
+ path.parent.mkdir(parents=True, exist_ok=True)
135
+ path.write_text(content, encoding="utf-8")
136
+ return path
137
+
138
+ def _build_context(
139
+ self,
140
+ entities: "ExtractedEntities",
141
+ **extra: Any,
142
+ ) -> dict[str, Any]:
143
+ """Build the template context.
144
+
145
+ Args:
146
+ entities: All extracted entities.
147
+ **extra: Additional context variables.
148
+
149
+ Returns:
150
+ Template context dictionary.
151
+ """
152
+ return {
153
+ "ontology": entities.ontology,
154
+ "classes": entities.classes,
155
+ "object_properties": entities.object_properties,
156
+ "datatype_properties": entities.datatype_properties,
157
+ "annotation_properties": entities.annotation_properties,
158
+ "instances": entities.instances,
159
+ "config": self.config,
160
+ **extra,
161
+ }
162
+
163
+ def render_index(self, entities: "ExtractedEntities") -> Path:
164
+ """Render the main index page.
165
+
166
+ Args:
167
+ entities: All extracted entities.
168
+
169
+ Returns:
170
+ Path to the rendered file.
171
+ """
172
+ template = self.env.get_template("index.html.jinja")
173
+ context = self._build_context(
174
+ entities,
175
+ total_classes=len(entities.classes),
176
+ total_properties=len(entities.properties),
177
+ total_instances=len(entities.instances),
178
+ )
179
+ content = template.render(context)
180
+ return self._write_file(self._get_output_path("index.html"), content)
181
+
182
+ def render_hierarchy(self, entities: "ExtractedEntities") -> Path:
183
+ """Render the class hierarchy page.
184
+
185
+ Args:
186
+ entities: All extracted entities.
187
+
188
+ Returns:
189
+ Path to the rendered file.
190
+ """
191
+ # Build hierarchy tree structure
192
+ hierarchy = self._build_hierarchy_tree(entities.classes)
193
+
194
+ template = self.env.get_template("hierarchy.html.jinja")
195
+ context = self._build_context(entities, hierarchy=hierarchy)
196
+ content = template.render(context)
197
+ return self._write_file(self._get_output_path("hierarchy.html"), content)
198
+
199
+ def _build_hierarchy_tree(
200
+ self,
201
+ classes: list["ClassInfo"],
202
+ ) -> list[dict[str, Any]]:
203
+ """Build a tree structure for the class hierarchy.
204
+
205
+ Args:
206
+ classes: List of all classes.
207
+
208
+ Returns:
209
+ Nested list structure representing the hierarchy.
210
+ """
211
+ # Index classes by URI for lookup
212
+ class_by_uri: dict[str, "ClassInfo"] = {
213
+ str(c.uri): c for c in classes
214
+ }
215
+
216
+ # Find root classes (no superclasses in our ontology)
217
+ internal_uris = set(class_by_uri.keys())
218
+ root_classes = []
219
+
220
+ for c in classes:
221
+ # A class is a root if none of its superclasses are in our ontology
222
+ has_internal_parent = any(
223
+ str(parent) in internal_uris for parent in c.superclasses
224
+ )
225
+ if not has_internal_parent:
226
+ root_classes.append(c)
227
+
228
+ def build_node(class_info: "ClassInfo") -> dict[str, Any]:
229
+ """Recursively build a tree node."""
230
+ children = []
231
+ for child_uri in class_info.subclasses:
232
+ child_key = str(child_uri)
233
+ if child_key in class_by_uri:
234
+ children.append(build_node(class_by_uri[child_key]))
235
+
236
+ return {
237
+ "class": class_info,
238
+ "children": sorted(children, key=lambda n: n["class"].qname),
239
+ }
240
+
241
+ return sorted(
242
+ [build_node(c) for c in root_classes],
243
+ key=lambda n: n["class"].qname,
244
+ )
245
+
246
+ def render_class(
247
+ self,
248
+ class_info: "ClassInfo",
249
+ entities: "ExtractedEntities",
250
+ ) -> Path:
251
+ """Render a class documentation page.
252
+
253
+ Args:
254
+ class_info: Class to render.
255
+ entities: All extracted entities.
256
+
257
+ Returns:
258
+ Path to the rendered file.
259
+ """
260
+ # Find inherited properties (from superclasses)
261
+ inherited = self._collect_inherited_properties(class_info, entities)
262
+
263
+ template = self.env.get_template("class.html.jinja")
264
+ context = self._build_context(
265
+ entities,
266
+ class_info=class_info,
267
+ inherited_properties=inherited,
268
+ )
269
+ content = template.render(context)
270
+
271
+ from ..config import entity_to_path
272
+ rel_path = entity_to_path(class_info.qname, "class", self.config)
273
+ return self._write_file(self.config.output_dir / rel_path, content)
274
+
275
+ def _collect_inherited_properties(
276
+ self,
277
+ class_info: "ClassInfo",
278
+ entities: "ExtractedEntities",
279
+ ) -> list["PropertyInfo"]:
280
+ """Collect properties inherited from superclasses.
281
+
282
+ Args:
283
+ class_info: Class to collect for.
284
+ entities: All entities for lookups.
285
+
286
+ Returns:
287
+ List of inherited properties.
288
+ """
289
+ # Index classes by URI
290
+ class_by_uri = {str(c.uri): c for c in entities.classes}
291
+
292
+ inherited: list["PropertyInfo"] = []
293
+ seen_props: set[str] = set()
294
+ visited_classes: set[str] = set()
295
+
296
+ # Direct properties
297
+ for prop in class_info.domain_of:
298
+ seen_props.add(str(prop.uri))
299
+
300
+ def collect_from_ancestors(uri: str) -> None:
301
+ """Recursively collect from ancestor classes."""
302
+ if uri not in class_by_uri:
303
+ return
304
+ if uri in visited_classes:
305
+ return # Avoid circular hierarchies
306
+ visited_classes.add(uri)
307
+
308
+ ancestor = class_by_uri[uri]
309
+ for prop in ancestor.domain_of:
310
+ if str(prop.uri) not in seen_props:
311
+ seen_props.add(str(prop.uri))
312
+ inherited.append(prop)
313
+
314
+ for parent_uri in ancestor.superclasses:
315
+ collect_from_ancestors(str(parent_uri))
316
+
317
+ for parent_uri in class_info.superclasses:
318
+ collect_from_ancestors(str(parent_uri))
319
+
320
+ return inherited
321
+
322
+ def render_property(
323
+ self,
324
+ prop_info: "PropertyInfo",
325
+ entities: "ExtractedEntities",
326
+ ) -> Path:
327
+ """Render a property documentation page.
328
+
329
+ Args:
330
+ prop_info: Property to render.
331
+ entities: All extracted entities.
332
+
333
+ Returns:
334
+ Path to the rendered file.
335
+ """
336
+ template = self.env.get_template("property.html.jinja")
337
+ context = self._build_context(entities, property_info=prop_info)
338
+ content = template.render(context)
339
+
340
+ entity_type = f"{prop_info.property_type}_property"
341
+ from ..config import entity_to_path
342
+ rel_path = entity_to_path(prop_info.qname, entity_type, self.config)
343
+ return self._write_file(self.config.output_dir / rel_path, content)
344
+
345
+ def render_instance(
346
+ self,
347
+ instance_info: "InstanceInfo",
348
+ entities: "ExtractedEntities",
349
+ ) -> Path:
350
+ """Render an instance documentation page.
351
+
352
+ Args:
353
+ instance_info: Instance to render.
354
+ entities: All extracted entities.
355
+
356
+ Returns:
357
+ Path to the rendered file.
358
+ """
359
+ template = self.env.get_template("instance.html.jinja")
360
+ context = self._build_context(entities, instance_info=instance_info)
361
+ content = template.render(context)
362
+
363
+ from ..config import entity_to_path
364
+ rel_path = entity_to_path(instance_info.qname, "instance", self.config)
365
+ return self._write_file(self.config.output_dir / rel_path, content)
366
+
367
+ def render_namespaces(self, entities: "ExtractedEntities") -> Path:
368
+ """Render the namespace reference page.
369
+
370
+ Args:
371
+ entities: All extracted entities.
372
+
373
+ Returns:
374
+ Path to the rendered file.
375
+ """
376
+ template = self.env.get_template("namespaces.html.jinja")
377
+ context = self._build_context(entities)
378
+ content = template.render(context)
379
+ return self._write_file(self._get_output_path("namespaces.html"), content)
380
+
381
+ def render_single_page(self, entities: "ExtractedEntities") -> Path:
382
+ """Render all documentation as a single page.
383
+
384
+ Args:
385
+ entities: All extracted entities.
386
+
387
+ Returns:
388
+ Path to the rendered file.
389
+ """
390
+ hierarchy = self._build_hierarchy_tree(entities.classes)
391
+
392
+ template = self.env.get_template("single_page.html.jinja")
393
+ context = self._build_context(entities, hierarchy=hierarchy)
394
+ content = template.render(context)
395
+ return self._write_file(self._get_output_path("index.html"), content)
396
+
397
+ def copy_assets(self) -> None:
398
+ """Copy static assets (CSS, JS) to the output directory."""
399
+ assets_dir = self.config.output_dir / "assets"
400
+ assets_dir.mkdir(parents=True, exist_ok=True)
401
+
402
+ # If using custom templates with custom assets, copy those
403
+ if self.config.template_dir:
404
+ custom_assets = self.config.template_dir / "assets"
405
+ if custom_assets.exists():
406
+ for asset in custom_assets.iterdir():
407
+ if asset.is_file():
408
+ shutil.copy(asset, assets_dir / asset.name)
409
+ return
410
+
411
+ # Write default CSS
412
+ self._write_default_css(assets_dir)
413
+
414
+ # Write default search JS
415
+ if self.config.include_search:
416
+ self._write_default_search_js(assets_dir)
417
+
418
+ def _write_default_css(self, assets_dir: Path) -> None:
419
+ """Write the default stylesheet.
420
+
421
+ Args:
422
+ assets_dir: Assets directory.
423
+ """
424
+ css = """/* rdf-construct documentation styles */
425
+ :root {
426
+ --primary-colour: #2563eb;
427
+ --secondary-colour: #64748b;
428
+ --background: #ffffff;
429
+ --surface: #f8fafc;
430
+ --text: #1e293b;
431
+ --text-muted: #64748b;
432
+ --border: #e2e8f0;
433
+ --code-bg: #f1f5f9;
434
+ --success: #22c55e;
435
+ --warning: #eab308;
436
+ --error: #ef4444;
437
+ }
438
+
439
+ * {
440
+ box-sizing: border-box;
441
+ margin: 0;
442
+ padding: 0;
443
+ }
444
+
445
+ body {
446
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
447
+ line-height: 1.6;
448
+ color: var(--text);
449
+ background: var(--background);
450
+ max-width: 1200px;
451
+ margin: 0 auto;
452
+ padding: 2rem;
453
+ }
454
+
455
+ a {
456
+ color: var(--primary-colour);
457
+ text-decoration: none;
458
+ }
459
+
460
+ a:hover {
461
+ text-decoration: underline;
462
+ }
463
+
464
+ h1, h2, h3, h4 {
465
+ margin-top: 1.5rem;
466
+ margin-bottom: 0.75rem;
467
+ line-height: 1.3;
468
+ }
469
+
470
+ h1 { font-size: 2rem; }
471
+ h2 { font-size: 1.5rem; }
472
+ h3 { font-size: 1.25rem; }
473
+ h4 { font-size: 1.1rem; }
474
+
475
+ .header {
476
+ border-bottom: 1px solid var(--border);
477
+ padding-bottom: 1rem;
478
+ margin-bottom: 2rem;
479
+ }
480
+
481
+ .header h1 {
482
+ margin-top: 0;
483
+ }
484
+
485
+ .header .description {
486
+ color: var(--text-muted);
487
+ font-size: 1.1rem;
488
+ }
489
+
490
+ .nav {
491
+ display: flex;
492
+ gap: 1rem;
493
+ margin-bottom: 1rem;
494
+ padding: 0.75rem 0;
495
+ border-bottom: 1px solid var(--border);
496
+ }
497
+
498
+ .nav a {
499
+ padding: 0.5rem 1rem;
500
+ border-radius: 0.375rem;
501
+ transition: background 0.2s;
502
+ }
503
+
504
+ .nav a:hover {
505
+ background: var(--surface);
506
+ text-decoration: none;
507
+ }
508
+
509
+ .nav a.active {
510
+ background: var(--primary-colour);
511
+ color: white;
512
+ }
513
+
514
+ .search-box {
515
+ margin-bottom: 1.5rem;
516
+ }
517
+
518
+ .search-box input {
519
+ width: 100%;
520
+ padding: 0.75rem 1rem;
521
+ border: 1px solid var(--border);
522
+ border-radius: 0.375rem;
523
+ font-size: 1rem;
524
+ }
525
+
526
+ .search-box input:focus {
527
+ outline: none;
528
+ border-colour: var(--primary-colour);
529
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
530
+ }
531
+
532
+ .search-results {
533
+ list-style: none;
534
+ padding: 0;
535
+ margin-top: 1rem;
536
+ }
537
+
538
+ .search-results li {
539
+ padding: 0.5rem 0;
540
+ border-bottom: 1px solid var(--border);
541
+ }
542
+
543
+ .stats {
544
+ display: grid;
545
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
546
+ gap: 1rem;
547
+ margin: 1.5rem 0;
548
+ }
549
+
550
+ .stat-card {
551
+ background: var(--surface);
552
+ border: 1px solid var(--border);
553
+ border-radius: 0.5rem;
554
+ padding: 1rem;
555
+ text-align: center;
556
+ }
557
+
558
+ .stat-card .number {
559
+ font-size: 2rem;
560
+ font-weight: 600;
561
+ color: var(--primary-colour);
562
+ }
563
+
564
+ .stat-card .label {
565
+ color: var(--text-muted);
566
+ font-size: 0.875rem;
567
+ }
568
+
569
+ .entity-card {
570
+ background: var(--surface);
571
+ border: 1px solid var(--border);
572
+ border-radius: 0.5rem;
573
+ padding: 1.5rem;
574
+ margin-bottom: 1.5rem;
575
+ }
576
+
577
+ .entity-card h2 {
578
+ margin-top: 0;
579
+ }
580
+
581
+ .entity-type {
582
+ display: inline-block;
583
+ padding: 0.25rem 0.5rem;
584
+ border-radius: 0.25rem;
585
+ font-size: 0.75rem;
586
+ font-weight: 600;
587
+ text-transform: uppercase;
588
+ background: var(--primary-colour);
589
+ color: white;
590
+ margin-left: 0.5rem;
591
+ }
592
+
593
+ .entity-type.object { background: #8b5cf6; }
594
+ .entity-type.datatype { background: #06b6d4; }
595
+ .entity-type.annotation { background: #f59e0b; }
596
+ .entity-type.instance { background: #10b981; }
597
+
598
+ .definition {
599
+ color: var(--text-muted);
600
+ font-style: italic;
601
+ margin-bottom: 1rem;
602
+ }
603
+
604
+ .uri {
605
+ font-family: monospace;
606
+ font-size: 0.875rem;
607
+ color: var(--text-muted);
608
+ word-break: break-all;
609
+ }
610
+
611
+ .section {
612
+ margin: 1.5rem 0;
613
+ }
614
+
615
+ .section h3 {
616
+ font-size: 1rem;
617
+ color: var(--text-muted);
618
+ text-transform: uppercase;
619
+ letter-spacing: 0.05em;
620
+ border-bottom: 1px solid var(--border);
621
+ padding-bottom: 0.5rem;
622
+ }
623
+
624
+ .entity-list {
625
+ list-style: none;
626
+ padding: 0;
627
+ }
628
+
629
+ .entity-list li {
630
+ padding: 0.5rem 0;
631
+ border-bottom: 1px solid var(--border);
632
+ }
633
+
634
+ .entity-list li:last-child {
635
+ border-bottom: none;
636
+ }
637
+
638
+ .hierarchy-tree {
639
+ list-style: none;
640
+ padding-left: 1.5rem;
641
+ }
642
+
643
+ .hierarchy-tree > li {
644
+ padding-left: 0;
645
+ }
646
+
647
+ .hierarchy-tree li {
648
+ position: relative;
649
+ padding: 0.25rem 0;
650
+ }
651
+
652
+ .hierarchy-tree li::before {
653
+ content: '';
654
+ position: absolute;
655
+ left: -1rem;
656
+ top: 0;
657
+ border-left: 1px solid var(--border);
658
+ height: 100%;
659
+ }
660
+
661
+ .hierarchy-tree li::after {
662
+ content: '';
663
+ position: absolute;
664
+ left: -1rem;
665
+ top: 0.75rem;
666
+ border-bottom: 1px solid var(--border);
667
+ width: 0.75rem;
668
+ }
669
+
670
+ .hierarchy-tree li:last-child::before {
671
+ height: 0.75rem;
672
+ }
673
+
674
+ .annotation {
675
+ background: var(--code-bg);
676
+ padding: 0.125rem 0.375rem;
677
+ border-radius: 0.25rem;
678
+ font-size: 0.875rem;
679
+ }
680
+
681
+ table {
682
+ width: 100%;
683
+ border-collapse: collapse;
684
+ margin: 1rem 0;
685
+ }
686
+
687
+ th, td {
688
+ padding: 0.75rem;
689
+ text-align: left;
690
+ border-bottom: 1px solid var(--border);
691
+ }
692
+
693
+ th {
694
+ background: var(--surface);
695
+ font-weight: 600;
696
+ }
697
+
698
+ code {
699
+ font-family: 'SF Mono', Consolas, monospace;
700
+ font-size: 0.875em;
701
+ background: var(--code-bg);
702
+ padding: 0.125rem 0.375rem;
703
+ border-radius: 0.25rem;
704
+ }
705
+
706
+ .footer {
707
+ margin-top: 3rem;
708
+ padding-top: 1rem;
709
+ border-top: 1px solid var(--border);
710
+ color: var(--text-muted);
711
+ font-size: 0.875rem;
712
+ text-align: center;
713
+ }
714
+
715
+ @media (max-width: 768px) {
716
+ body {
717
+ padding: 1rem;
718
+ }
719
+
720
+ .stats {
721
+ grid-template-columns: repeat(2, 1fr);
722
+ }
723
+
724
+ .nav {
725
+ flex-wrap: wrap;
726
+ }
727
+ }
728
+ """
729
+ (assets_dir / "style.css").write_text(css, encoding="utf-8")
730
+
731
+ def _write_default_search_js(self, assets_dir: Path) -> None:
732
+ """Write the default search JavaScript.
733
+
734
+ Args:
735
+ assets_dir: Assets directory.
736
+ """
737
+ js = """// rdf-construct documentation search
738
+ document.addEventListener('DOMContentLoaded', function() {
739
+ const searchInput = document.getElementById('search-input');
740
+ const resultsContainer = document.getElementById('search-results');
741
+
742
+ if (!searchInput || !resultsContainer) return;
743
+
744
+ let searchIndex = null;
745
+
746
+ // Load search index
747
+ fetch('search.json')
748
+ .then(response => response.json())
749
+ .then(data => {
750
+ searchIndex = data.entities;
751
+ })
752
+ .catch(err => console.error('Failed to load search index:', err));
753
+
754
+ // Search function
755
+ function search(query) {
756
+ if (!searchIndex || query.length < 2) {
757
+ resultsContainer.innerHTML = '';
758
+ return;
759
+ }
760
+
761
+ const terms = query.toLowerCase().split(/\\s+/);
762
+ const results = searchIndex
763
+ .map(entity => {
764
+ const score = terms.reduce((acc, term) => {
765
+ // Check label
766
+ if (entity.label.toLowerCase().includes(term)) {
767
+ return acc + 10;
768
+ }
769
+ // Check qname
770
+ if (entity.qname.toLowerCase().includes(term)) {
771
+ return acc + 5;
772
+ }
773
+ // Check keywords
774
+ if (entity.keywords.some(k => k.includes(term))) {
775
+ return acc + 1;
776
+ }
777
+ return acc;
778
+ }, 0);
779
+ return { entity, score };
780
+ })
781
+ .filter(r => r.score > 0)
782
+ .sort((a, b) => b.score - a.score)
783
+ .slice(0, 20);
784
+
785
+ if (results.length === 0) {
786
+ resultsContainer.innerHTML = '<li>No results found</li>';
787
+ return;
788
+ }
789
+
790
+ resultsContainer.innerHTML = results
791
+ .map(r => `<li><a href="${r.entity.url}">${r.entity.label}</a> <span class="entity-type ${r.entity.entity_type}">${r.entity.entity_type}</span></li>`)
792
+ .join('');
793
+ }
794
+
795
+ // Debounce search
796
+ let timeout;
797
+ searchInput.addEventListener('input', function() {
798
+ clearTimeout(timeout);
799
+ timeout = setTimeout(() => search(this.value), 150);
800
+ });
801
+ });
802
+ """
803
+ (assets_dir / "search.js").write_text(js, encoding="utf-8")