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.
- rdf_construct/__init__.py +12 -0
- rdf_construct/__main__.py +0 -0
- rdf_construct/cli.py +1762 -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/main.py +6 -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/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.2.0.dist-info/METADATA +431 -0
- rdf_construct-0.2.0.dist-info/RECORD +88 -0
- rdf_construct-0.2.0.dist-info/WHEEL +4 -0
- rdf_construct-0.2.0.dist-info/entry_points.txt +3 -0
- rdf_construct-0.2.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")
|