linkml 1.7.8__py3-none-any.whl → 1.7.10__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.
@@ -1,7 +1,6 @@
1
1
  import os
2
- from contextlib import redirect_stdout
2
+ import re
3
3
  from dataclasses import dataclass
4
- from io import StringIO
5
4
  from typing import Any, Callable, Dict, List, Optional, Set, Union
6
5
 
7
6
  import click
@@ -63,7 +62,7 @@ class MarkdownGenerator(Generator):
63
62
  index_file: str = "index.md",
64
63
  noimages: bool = False,
65
64
  **_,
66
- ) -> None:
65
+ ) -> str:
67
66
  self.gen_classes = classes if classes else []
68
67
  for cls in self.gen_classes:
69
68
  if cls not in self.schema.classes:
@@ -85,248 +84,270 @@ class MarkdownGenerator(Generator):
85
84
  os.makedirs(os.path.join(directory, "types"), exist_ok=True)
86
85
 
87
86
  with open(self.exist_warning(directory, index_file), "w", encoding="UTF-8") as ixfile:
88
- with redirect_stdout(ixfile):
89
- self.frontmatter(f"{self.schema.name}")
87
+ items = []
88
+ items.append(self.frontmatter(f"{self.schema.name}"))
89
+ items.append(
90
90
  self.para(
91
91
  f"**metamodel version:** {self.schema.metamodel_version}\n\n**version:** {self.schema.version}"
92
92
  )
93
- self.para(be(self.schema.description))
94
-
95
- self.header(3, "Classes")
96
- for cls in sorted(self.schema.classes.values(), key=lambda c: c.name):
97
- if not cls.is_a and not cls.mixin and self.is_secondary_ref(cls.name):
98
- self.class_hier(cls)
99
-
100
- self.header(3, "Mixins")
101
- for cls in sorted(self.schema.classes.values(), key=lambda c: c.name):
102
- if cls.mixin and self.is_secondary_ref(cls.name):
103
- self.class_hier(cls)
104
-
105
- self.header(3, "Slots")
106
- for slot in sorted(self.schema.slots.values(), key=lambda s: s.name):
107
- if not slot.is_a and self.is_secondary_ref(slot.name):
108
- self.pred_hier(slot)
109
-
110
- self.header(3, "Enums")
111
- for enu in sorted(self.schema.enums.values(), key=lambda e: e.name):
112
- self.enum_hier(enu)
113
-
114
- self.header(3, "Subsets")
115
- for subset in sorted(self.schema.subsets.values(), key=lambda s: s.name):
116
- self.bullet(self.subset_link(subset, use_desc=True), 0)
117
-
118
- self.header(3, "Types")
119
- self.header(4, "Built in")
120
- for builtin_name in sorted(self.synopsis.typebases.keys()):
121
- self.bullet(f"**{builtin_name}**")
122
- self.header(4, "Defined")
123
- for typ in sorted(self.schema.types.values(), key=lambda t: t.name):
124
- if self.is_secondary_ref(typ.name):
125
- if typ.typeof:
126
- typ_typ = self.type_link(typ.typeof)
127
- else:
128
- typ_typ = f"**{typ.base}**"
93
+ )
94
+ items.append(self.para(be(self.schema.description)))
95
+
96
+ items.append(self.header(3, "Classes"))
97
+ for cls in sorted(self.schema.classes.values(), key=lambda c: c.name):
98
+ if not cls.is_a and not cls.mixin and self.is_secondary_ref(cls.name):
99
+ items.append(self.class_hier(cls))
100
+
101
+ items.append(self.header(3, "Mixins"))
102
+ for cls in sorted(self.schema.classes.values(), key=lambda c: c.name):
103
+ if cls.mixin and self.is_secondary_ref(cls.name):
104
+ items.append(self.class_hier(cls))
105
+
106
+ items.append(self.header(3, "Slots"))
107
+ for slot in sorted(self.schema.slots.values(), key=lambda s: s.name):
108
+ if not slot.is_a and self.is_secondary_ref(slot.name):
109
+ items.append(self.pred_hier(slot))
110
+
111
+ items.append(self.header(3, "Enums"))
112
+ for enu in sorted(self.schema.enums.values(), key=lambda e: e.name):
113
+ items.append(self.enum_hier(enu))
114
+
115
+ items.append(self.header(3, "Subsets"))
116
+ for subset in sorted(self.schema.subsets.values(), key=lambda s: s.name):
117
+ items.append(self.bullet(self.subset_link(subset, use_desc=True), 0))
118
+
119
+ items.append(self.header(3, "Types"))
120
+ items.append(self.header(4, "Built in"))
121
+ for builtin_name in sorted(self.synopsis.typebases.keys()):
122
+ items.append(self.bullet(f"**{builtin_name}**"))
123
+ items.append(self.header(4, "Defined"))
124
+ for typ in sorted(self.schema.types.values(), key=lambda t: t.name):
125
+ if self.is_secondary_ref(typ.name):
126
+ if typ.typeof:
127
+ typ_typ = self.type_link(typ.typeof)
128
+ else:
129
+ typ_typ = f"**{typ.base}**"
129
130
 
130
- self.bullet(self.type_link(typ, after_link=f" ({typ_typ})", use_desc=True))
131
+ items.append(self.bullet(self.type_link(typ, after_link=f" ({typ_typ})", use_desc=True)))
132
+ items = [i for i in items if i is not None]
133
+ out = "\n".join(items) + "\n"
134
+ out = pad_heading(out)
135
+ ixfile.write(out)
136
+ return out
131
137
 
132
- def visit_class(self, cls: ClassDefinition) -> bool:
138
+ def visit_class(self, cls: ClassDefinition) -> str:
133
139
  # allow client to relabel metamodel
134
140
  mixin_local_name = self.get_metamodel_slot_name("Mixin")
135
141
  class_local_name = self.get_metamodel_slot_name("Class")
136
142
 
137
143
  if self.gen_classes and cls.name not in self.gen_classes:
138
- return False
144
+ return ""
139
145
 
140
146
  with open(self.exist_warning(self.dir_path(cls)), "w", encoding="UTF-8") as clsfile:
141
- with redirect_stdout(clsfile):
142
- class_curi = self.namespaces.uri_or_curie_for(str(self.namespaces._base), camelcase(cls.name))
143
- class_uri = self.namespaces.uri_for(class_curi)
144
- self.element_header(cls, cls.name, class_curi, class_uri)
145
- print()
146
- if not self.noyuml:
147
- if self.image_directory:
148
- yg = YumlGenerator(self)
149
- yg.serialize(
150
- classes=[cls.name],
151
- directory=self.image_directory,
152
- load_image=not self.noimages,
153
- )
154
- img_url = os.path.join("images", os.path.basename(yg.output_file_name))
155
- else:
156
- yg = YumlGenerator(self)
157
- img_url = (
158
- yg.serialize(classes=[cls.name])
159
- .replace("?", "%3F")
160
- .replace(" ", "%20")
161
- .replace("|", "|")
162
- )
163
-
164
- print(f"[![img]({img_url})]({img_url})")
165
-
166
- self.mappings(cls)
167
-
168
- if cls.id_prefixes:
169
- self.header(2, "Identifier prefixes")
170
- for p in cls.id_prefixes:
171
- self.bullet(f"{p}")
172
-
173
- if cls.is_a is not None:
174
- self.header(2, "Parents")
175
- self.bullet(f" is_a: {self.class_link(cls.is_a, use_desc=True)}")
176
- if cls.mixins:
177
- self.header(2, f"Uses {mixin_local_name}")
178
- for mixin in cls.mixins:
179
- self.bullet(f" mixin: {self.class_link(mixin, use_desc=True)}")
180
-
181
- if cls.name in self.synopsis.isarefs:
182
- self.header(2, "Children")
183
- for child in sorted(self.synopsis.isarefs[cls.name].classrefs):
184
- self.bullet(f"{self.class_link(child, use_desc=True)}")
185
-
186
- if cls.name in self.synopsis.mixinrefs:
187
- self.header(2, f"{mixin_local_name} for")
188
- for mixin in sorted(self.synopsis.mixinrefs[cls.name].classrefs):
189
- self.bullet(f'{self.class_link(mixin, use_desc=True, after_link="(mixin)")}')
190
-
191
- if cls.name in self.synopsis.classrefs:
192
- self.header(2, f"Referenced by {class_local_name}")
193
- for sn in sorted(self.synopsis.classrefs[cls.name].slotrefs):
194
- slot = self.schema.slots[sn]
195
- if slot.range == cls.name:
147
+ items = []
148
+ class_curi = self.namespaces.uri_or_curie_for(str(self.namespaces._base), camelcase(cls.name))
149
+ class_uri = self.namespaces.uri_for(class_curi)
150
+ items.append(self.element_header(cls, cls.name, class_curi, class_uri))
151
+ items.append("")
152
+ if not self.noyuml:
153
+ if self.image_directory:
154
+ yg = YumlGenerator(self)
155
+ yg.serialize(
156
+ classes=[cls.name],
157
+ directory=self.image_directory,
158
+ load_image=not self.noimages,
159
+ )
160
+ img_url = os.path.join("images", os.path.basename(yg.output_file_name))
161
+ else:
162
+ yg = YumlGenerator(self)
163
+ img_url = (
164
+ yg.serialize(classes=[cls.name]).replace("?", "%3F").replace(" ", "%20").replace("|", "|")
165
+ )
166
+
167
+ items.append(f"[![img]({img_url})]({img_url})")
168
+
169
+ if cls.id_prefixes:
170
+ items.append(self.header(2, "Identifier prefixes"))
171
+ for p in cls.id_prefixes:
172
+ items.append(self.bullet(f"{p}"))
173
+
174
+ if cls.is_a is not None:
175
+ items.append(self.header(2, "Parents"))
176
+ items.append(self.bullet(f" is_a: {self.class_link(cls.is_a, use_desc=True)}"))
177
+ if cls.mixins:
178
+ items.append(self.header(2, f"Uses {mixin_local_name}"))
179
+ for mixin in cls.mixins:
180
+ items.append(self.bullet(f" mixin: {self.class_link(mixin, use_desc=True)}"))
181
+
182
+ if cls.name in self.synopsis.isarefs:
183
+ items.append(self.header(2, "Children"))
184
+ for child in sorted(self.synopsis.isarefs[cls.name].classrefs):
185
+ items.append(self.bullet(f"{self.class_link(child, use_desc=True)}"))
186
+
187
+ if cls.name in self.synopsis.mixinrefs:
188
+ items.append(self.header(2, f"{mixin_local_name} for"))
189
+ for mixin in sorted(self.synopsis.mixinrefs[cls.name].classrefs):
190
+ items.append(self.bullet(f'{self.class_link(mixin, use_desc=True, after_link="(mixin)")}'))
191
+
192
+ if cls.name in self.synopsis.classrefs:
193
+ items.append(self.header(2, f"Referenced by {class_local_name}"))
194
+ for sn in sorted(self.synopsis.classrefs[cls.name].slotrefs):
195
+ slot = self.schema.slots[sn]
196
+ if slot.range == cls.name:
197
+ items.append(
196
198
  self.bullet(
197
199
  f" **{self.class_link(slot.domain)}** "
198
200
  f"*{self.slot_link(slot, add_subset=False)}*{self.predicate_cardinality(slot)} "
199
201
  f"**{self.class_type_link(slot.range)}**"
200
202
  )
203
+ )
201
204
 
202
- self.header(2, "Attributes")
203
-
204
- # List all of the slots that directly belong to the class
205
- slot_list = [slot for slot in [self.schema.slots[sn] for sn in cls.slots]]
206
- own_slots = [slot for slot in slot_list if cls.name in slot.domain_of]
207
- if own_slots:
208
- self.header(3, "Own")
209
- for slot in own_slots:
210
- self.slot_field(cls, slot)
211
- slot_list.remove(slot)
212
-
213
- # List all of the inherited slots
214
- ancestors = set(self.ancestors(cls))
215
- inherited_slots = [slot for slot in slot_list if set(slot.domain_of).intersection(ancestors)]
216
- if inherited_slots:
217
- self.header(3, "Inherited from " + cls.is_a + ":")
218
- for inherited_slot in inherited_slots:
219
- self.slot_field(cls, inherited_slot)
220
- slot_list.remove(inherited_slot)
221
-
222
- # List all of the slots acquired through mixing
223
- mixed_in_classes = set()
224
- for mixin in cls.mixins:
225
- mixed_in_classes.add(mixin)
226
- mixed_in_classes.update(set(self.ancestors(self.schema.classes[mixin])))
227
- for slot in slot_list:
228
- mixers = set(slot.domain_of).intersection(mixed_in_classes)
229
- for mixer in mixers:
230
- self.header(3, "Mixed in from " + mixer + ":")
231
- self.slot_field(cls, slot)
232
-
233
- self.element_properties(cls)
234
-
235
- return False
236
-
237
- def visit_type(self, typ: TypeDefinition) -> None:
205
+ items.append(self.header(2, "Attributes"))
206
+
207
+ # List all of the slots that directly belong to the class
208
+ slot_list = [slot for slot in [self.schema.slots[sn] for sn in cls.slots]]
209
+ own_slots = [slot for slot in slot_list if cls.name in slot.domain_of]
210
+ if own_slots:
211
+ items.append(self.header(3, "Own"))
212
+ for slot in own_slots:
213
+ items.append(self.slot_field(cls, slot))
214
+ slot_list.remove(slot)
215
+
216
+ # List all of the inherited slots
217
+ ancestors = set(self.ancestors(cls))
218
+ inherited_slots = [slot for slot in slot_list if set(slot.domain_of).intersection(ancestors)]
219
+ if inherited_slots:
220
+ items.append(self.header(3, "Inherited from " + cls.is_a + ":"))
221
+ for inherited_slot in inherited_slots:
222
+ items.append(self.slot_field(cls, inherited_slot))
223
+ slot_list.remove(inherited_slot)
224
+
225
+ # List all of the slots acquired through mixing
226
+ mixed_in_classes = set()
227
+ for mixin in cls.mixins:
228
+ mixed_in_classes.add(mixin)
229
+ mixed_in_classes.update(set(self.ancestors(self.schema.classes[mixin])))
230
+ for slot in slot_list:
231
+ mixers = set(slot.domain_of).intersection(mixed_in_classes)
232
+ for mixer in mixers:
233
+ items.append(self.header(3, "Mixed in from " + mixer + ":"))
234
+ items.append(self.slot_field(cls, slot))
235
+
236
+ items.append(self.element_properties(cls))
237
+ out = "\n".join(items)
238
+ out = pad_heading(out)
239
+ clsfile.write(out)
240
+ return out
241
+
242
+ def visit_type(self, typ: TypeDefinition) -> str:
238
243
  with open(self.exist_warning(self.dir_path(typ)), "w", encoding="UTF-8") as typefile:
239
- with redirect_stdout(typefile):
240
- type_uri = typ.definition_uri
241
- type_curie = self.namespaces.curie_for(type_uri)
242
- self.element_header(typ, typ.name, type_curie, type_uri)
243
-
244
- print("| | | |")
245
- print("| --- | --- | --- |")
246
- if typ.typeof:
247
- print(f"| Parent type | | {self.class_type_link(typ.typeof)} |")
248
- print(f"| Root (builtin) type | | **{typ.base}** |")
249
- if typ.repr:
250
- print(f"| Representation | | {typ.repr} |")
251
- self.element_properties(typ)
252
-
253
- def visit_slot(self, aliased_slot_name: str, slot: SlotDefinition) -> None:
244
+ type_uri = typ.definition_uri
245
+ type_curie = self.namespaces.curie_for(type_uri)
246
+ out = self.element_header(typ, typ.name, type_curie, type_uri)
247
+
248
+ out = "\n".join([out, "| | | |"])
249
+ out = "\n".join([out, "| --- | --- | --- |"])
250
+ if typ.typeof:
251
+ out = "\n".join([out, f"| Parent type | | {self.class_type_link(typ.typeof)} |"])
252
+ out = "\n".join([out, f"| Root (builtin) type | | **{typ.base}** |"])
253
+ if typ.repr:
254
+ out = "\n".join([out, f"| Representation | | {typ.repr} |"])
255
+ out += self.element_properties(typ)
256
+ out += "\n"
257
+ out = pad_heading(out)
258
+ typefile.write(out)
259
+ return out
260
+
261
+ def visit_slot(self, aliased_slot_name: str, slot: SlotDefinition) -> str:
254
262
  with open(self.exist_warning(self.dir_path(slot)), "w", encoding="UTF-8") as slotfile:
255
- with redirect_stdout(slotfile):
256
- slot_curie = self.namespaces.uri_or_curie_for(str(self.namespaces._base), underscore(slot.name))
257
- slot_uri = self.namespaces.uri_for(slot_curie)
258
- self.element_header(slot, aliased_slot_name, slot_curie, slot_uri)
259
- self.mappings(slot)
260
-
261
- self.header(2, "Domain and Range")
262
- print(
263
+ items = []
264
+ slot_curie = self.namespaces.uri_or_curie_for(str(self.namespaces._base), underscore(slot.name))
265
+ slot_uri = self.namespaces.uri_for(slot_curie)
266
+ items.append(self.element_header(slot, aliased_slot_name, slot_curie, slot_uri))
267
+
268
+ items.append(self.header(2, "Domain and Range"))
269
+ items.append(
270
+ (
263
271
  f"{self.class_link(slot.domain)} →{self.predicate_cardinality(slot)} "
264
272
  f"{self.class_type_link(slot.range)}"
265
273
  )
274
+ )
266
275
 
267
- self.header(2, "Parents")
268
- if slot.is_a:
269
- self.bullet(f" is_a: {self.slot_link(slot.is_a)}")
270
-
271
- self.header(2, "Children")
272
- if slot.name in sorted(self.synopsis.isarefs):
273
- for child in sorted(self.synopsis.isarefs[slot.name].slotrefs):
274
- self.bullet(f" {self.slot_link(child)}")
275
-
276
- self.header(2, "Used by")
277
- if slot.name in sorted(self.synopsis.slotrefs):
278
- for rc in sorted(self.synopsis.slotrefs[slot.name].classrefs):
279
- self.bullet(f"{self.class_link(rc)}")
280
- if aliased_slot_name == "relation":
281
- if slot.subproperty_of:
282
- reifies = (
283
- self.slot_link(slot.subproperty_of)
284
- if slot.subproperty_of in self.schema.slots
285
- else slot.subproperty_of
286
- )
287
- self.bullet(f" reifies: {reifies}")
288
- self.element_properties(slot)
289
-
290
- def visit_enum(self, enum: EnumDefinition) -> None:
276
+ items.append(self.header(2, "Parents"))
277
+ if slot.is_a:
278
+ items.append(self.bullet(f" is_a: {self.slot_link(slot.is_a)}"))
279
+
280
+ items.append(self.header(2, "Children"))
281
+ if slot.name in sorted(self.synopsis.isarefs):
282
+ for child in sorted(self.synopsis.isarefs[slot.name].slotrefs):
283
+ items.append(self.bullet(f" {self.slot_link(child)}"))
284
+
285
+ items.append(self.header(2, "Used by"))
286
+ if slot.name in sorted(self.synopsis.slotrefs):
287
+ for rc in sorted(self.synopsis.slotrefs[slot.name].classrefs):
288
+ items.append(self.bullet(f"{self.class_link(rc)}"))
289
+ if aliased_slot_name == "relation":
290
+ if slot.subproperty_of:
291
+ reifies = (
292
+ self.slot_link(slot.subproperty_of)
293
+ if slot.subproperty_of in self.schema.slots
294
+ else slot.subproperty_of
295
+ )
296
+ items.append(self.bullet(f" reifies: {reifies}"))
297
+ items.append(self.element_properties(slot))
298
+ out = "\n".join(items)
299
+ out = pad_heading(out)
300
+ slotfile.write(out)
301
+ return out
302
+
303
+ def visit_enum(self, enum: EnumDefinition) -> str:
291
304
  with open(self.exist_warning(self.dir_path(enum)), "w", encoding="UTF-8") as enumfile:
292
- with redirect_stdout(enumfile):
293
- enum_curie = self.namespaces.uri_or_curie_for(str(self.namespaces._base), underscore(enum.name))
294
- enum_uri = self.namespaces.uri_for(enum_curie)
295
- self.element_header(obj=enum, name=enum.name, curie=enum_curie, uri=enum_uri)
296
- self.element_properties(enum)
297
-
298
- def visit_subset(self, subset: SubsetDefinition) -> None:
305
+ items = []
306
+ enum_curie = self.namespaces.uri_or_curie_for(str(self.namespaces._base), underscore(enum.name))
307
+ enum_uri = self.namespaces.uri_for(enum_curie)
308
+ items.append(self.element_header(obj=enum, name=enum.name, curie=enum_curie, uri=enum_uri))
309
+ items.append(self.element_properties(enum))
310
+ out = "\n".join(items)
311
+ out = pad_heading(out)
312
+ enumfile.write(out)
313
+ return out
314
+
315
+ def visit_subset(self, subset: SubsetDefinition) -> str:
299
316
  with open(self.exist_warning(self.dir_path(subset)), "w", encoding="UTF-8") as subsetfile:
300
- with redirect_stdout(subsetfile):
301
- curie = self.namespaces.uri_or_curie_for(str(self.namespaces._base), underscore(subset.name))
302
- uri = self.namespaces.uri_for(curie)
303
- self.element_header(obj=subset, name=subset.name, curie=curie, uri=uri)
304
- # TODO: consider showing hierarchy within a subset
305
- self.header(3, "Classes")
306
- for cls in sorted(self.schema.classes.values(), key=lambda c: c.name):
307
- if not cls.mixin:
308
- if cls.in_subset and subset.name in cls.in_subset:
309
- self.bullet(self.class_link(cls, use_desc=True), 0)
310
- self.header(3, "Mixins")
311
- for cls in sorted(self.schema.classes.values(), key=lambda c: c.name):
312
- if cls.mixin:
313
- if cls.in_subset and subset.name in cls.in_subset:
314
- self.bullet(self.class_link(cls, use_desc=True), 0)
315
- self.header(3, "Slots")
316
- for slot in sorted(self.schema.slots.values(), key=lambda s: s.name):
317
- if slot.in_subset and subset.name in slot.in_subset:
318
- self.bullet(self.slot_link(slot, use_desc=True), 0)
319
- self.header(3, "Types")
320
- for type in sorted(self.schema.types.values(), key=lambda s: s.name):
321
- if type.in_subset and subset.name in type.in_subset:
322
- self.bullet(self.type_link(type, use_desc=True), 0)
323
- self.header(3, "Enums")
324
- for enum in sorted(self.schema.enums.values(), key=lambda s: s.name):
325
- if enum.in_subset and subset.name in enum.in_subset:
326
- self.bullet(self.enum_link(type, use_desc=True), 0)
327
- self.element_properties(subset)
328
-
329
- def element_header(self, obj: Element, name: str, curie: str, uri: str) -> None:
317
+ items = []
318
+ curie = self.namespaces.uri_or_curie_for(str(self.namespaces._base), underscore(subset.name))
319
+ uri = self.namespaces.uri_for(curie)
320
+ items.append(self.element_header(obj=subset, name=subset.name, curie=curie, uri=uri))
321
+ # TODO: consider showing hierarchy within a subset
322
+ items.append(self.header(3, "Classes"))
323
+ for cls in sorted(self.schema.classes.values(), key=lambda c: c.name):
324
+ if not cls.mixin:
325
+ if cls.in_subset and subset.name in cls.in_subset:
326
+ items.append(self.bullet(self.class_link(cls, use_desc=True), 0))
327
+ items.append(self.header(3, "Mixins"))
328
+ for cls in sorted(self.schema.classes.values(), key=lambda c: c.name):
329
+ if cls.mixin:
330
+ if cls.in_subset and subset.name in cls.in_subset:
331
+ items.append(self.bullet(self.class_link(cls, use_desc=True), 0))
332
+ items.append(self.header(3, "Slots"))
333
+ for slot in sorted(self.schema.slots.values(), key=lambda s: s.name):
334
+ if slot.in_subset and subset.name in slot.in_subset:
335
+ items.append(self.bullet(self.slot_link(slot, use_desc=True), 0))
336
+ items.append(self.header(3, "Types"))
337
+ for type in sorted(self.schema.types.values(), key=lambda s: s.name):
338
+ if type.in_subset and subset.name in type.in_subset:
339
+ items.append(self.bullet(self.type_link(type, use_desc=True), 0))
340
+ items.append(self.header(3, "Enums"))
341
+ for enum in sorted(self.schema.enums.values(), key=lambda s: s.name):
342
+ if enum.in_subset and subset.name in enum.in_subset:
343
+ items.append(self.bullet(self.enum_link(enum, use_desc=True), 0))
344
+ items.append(self.element_properties(subset))
345
+ out = "\n".join(items)
346
+ out = pad_heading(out)
347
+ subsetfile.write(out)
348
+ return out
349
+
350
+ def element_header(self, obj: Element, name: str, curie: str, uri: str) -> str:
330
351
  if isinstance(obj, TypeDefinition):
331
352
  obj_type = "Type"
332
353
  elif isinstance(obj, ClassDefinition):
@@ -341,13 +362,13 @@ class MarkdownGenerator(Generator):
341
362
  obj_type = "Class"
342
363
 
343
364
  header_label = f"{obj_type}: ~~{name}~~ _(deprecated)_" if obj.deprecated else f"{obj_type}: {name}"
344
- self.header(1, header_label)
365
+ out = self.header(1, header_label)
345
366
 
346
- self.para(be(obj.description))
347
- print(f"URI: [{curie}]({uri})")
348
- print()
367
+ out += self.para(be(obj.description))
368
+ out = "\n".join([out, f"URI: [{curie}]({uri})", ""])
369
+ return out
349
370
 
350
- def element_properties(self, obj: Element) -> None:
371
+ def element_properties(self, obj: Element) -> str:
351
372
  def identity(e: Any) -> Any:
352
373
  return e
353
374
 
@@ -355,21 +376,24 @@ class MarkdownGenerator(Generator):
355
376
  title: str,
356
377
  entries: Union[List, Dict],
357
378
  formatter: Optional[Callable[[Element], str]] = None,
358
- ) -> None:
379
+ ) -> Optional[str]:
359
380
  if formatter is None:
360
381
  formatter = identity
361
382
  if isinstance(entries, (dict, JsonObj)):
362
383
  entries = list(values(entries))
363
384
  if entries:
364
- print(f"| **{title}:** | | {formatter(entries[0])} |")
385
+ items = []
386
+ items.append(f"| **{title}:** | | {formatter(entries[0])} |")
365
387
  for entry in entries[1:]:
366
- print(f"| | | {formatter(entry)} |")
388
+ items.append(f"| | | {formatter(entry)} |")
389
+ return "\n".join(items)
367
390
 
368
- def enum_list(title: str, obj: EnumDefinition) -> None:
391
+ def enum_list(title: str, obj: EnumDefinition) -> str:
369
392
  # This data is from the enum provided in the YAML
370
- self.header(2, title)
371
- print("| Text | Description | Meaning | Other Information |")
372
- print("| :--- | :---: | :---: | ---: |")
393
+ items = []
394
+ items.append(self.header(2, title))
395
+ items.append("| Text | Description | Meaning | Other Information |")
396
+ items.append("| :--- | :---: | :---: | ---: |")
373
397
 
374
398
  for item, item_info in obj.permissible_values.items():
375
399
  text = ""
@@ -388,66 +412,81 @@ class MarkdownGenerator(Generator):
388
412
  other[k] = item_info[k]
389
413
  if not other:
390
414
  other = ""
391
- print(f"| {text} | {desc} | {meaning} | {other} |")
415
+ items.append(f"| {text} | {desc} | {meaning} | {other} |")
416
+ return "\n".join(items)
417
+
418
+ items = []
392
419
 
393
- attributes = StringIO()
394
- with redirect_stdout(attributes):
395
- prop_list("Aliases", obj.aliases)
420
+ items.append(prop_list("Aliases", obj.aliases))
421
+ items.append(
396
422
  prop_list(
397
423
  "Local names",
398
424
  obj.local_names,
399
425
  lambda e: f"{e.local_name_value} ({e.local_name_source})",
400
426
  )
401
- prop_list("Mappings", obj.mappings)
427
+ )
428
+ items.append(prop_list("Mappings", obj.mappings))
429
+ items.append(
402
430
  prop_list(
403
431
  "Alt Descriptions",
404
432
  obj.alt_descriptions,
405
433
  lambda e: f"{e.description} ({e.source})",
406
434
  )
407
- # todos
408
- # notes
409
- prop_list("Comments", obj.comments)
410
- prop_list("Examples", obj.examples)
411
- prop_list("In Subsets", obj.in_subset)
412
- # from_schema
413
- # imported_from
414
- prop_list("See also", [f"[{v}]({v})" for v in obj.see_also])
415
- prop_list("Exact Mappings", obj.exact_mappings)
416
- prop_list("Close Mappings", obj.close_mappings)
417
- prop_list("Narrow Mappings", obj.narrow_mappings)
418
- prop_list("Broad Mappings", obj.broad_mappings)
419
- prop_list("Related Mappings", obj.related_mappings)
420
- # - exact mappings
421
- # - close mappings
422
- # - related mappings
423
- # - deprecated element has exact replacement
424
- # - deprecated element has possible replacement
425
- if type(obj) == EnumDefinition:
426
- enum_list("Permissible Values", obj)
427
-
428
- if attributes.getvalue():
429
- self.header(2, "Other properties")
430
- print("| | | |")
431
- print("| --- | --- | --- |")
432
- print(attributes.getvalue())
433
-
434
- def class_hier(self, cls: ClassDefinition, level=0) -> None:
435
- self.bullet(self.class_link(cls, use_desc=True), level)
435
+ )
436
+ # todos
437
+ # notes
438
+ items.append(prop_list("Comments", obj.comments))
439
+ items.append(prop_list("Examples", obj.examples))
440
+ items.append(prop_list("In Subsets", obj.in_subset))
441
+ # from_schema
442
+ # imported_from
443
+ items.append(prop_list("See also", [f"[{v}]({v})" for v in obj.see_also]))
444
+ items.append(prop_list("Exact Mappings", obj.exact_mappings))
445
+ items.append(prop_list("Close Mappings", obj.close_mappings))
446
+ items.append(prop_list("Narrow Mappings", obj.narrow_mappings))
447
+ items.append(prop_list("Broad Mappings", obj.broad_mappings))
448
+ items.append(prop_list("Related Mappings", obj.related_mappings))
449
+
450
+ items = [i for i in items if i is not None]
451
+ if len(items) > 0:
452
+ header = "\n".join([self.header(2, "Other properties"), "| | | |", "| --- | --- | --- |"])
453
+ items.insert(0, header)
454
+
455
+ # - exact mappings
456
+ # - close mappings
457
+ # - related mappings
458
+ # - deprecated element has exact replacement
459
+ # - deprecated element has possible replacement
460
+ if type(obj) == EnumDefinition:
461
+ items.insert(0, enum_list("Permissible Values", obj))
462
+ items.insert(1, "\n")
463
+
464
+ out = "\n".join(items)
465
+ return out
466
+
467
+ def class_hier(self, cls: ClassDefinition, level=0) -> str:
468
+ items = []
469
+ items.append(self.bullet(self.class_link(cls, use_desc=True), level))
436
470
  if cls.name in sorted(self.synopsis.isarefs):
437
471
  for child in sorted(self.synopsis.isarefs[cls.name].classrefs):
438
- self.class_hier(self.schema.classes[child], level + 1)
472
+ items.append(self.class_hier(self.schema.classes[child], level + 1))
473
+ return "\n".join(items) if items else None
439
474
 
440
- def pred_hier(self, slot: SlotDefinition, level=0) -> None:
441
- self.bullet(self.slot_link(slot, use_desc=True), level)
475
+ def pred_hier(self, slot: SlotDefinition, level=0) -> str:
476
+ items = []
477
+ items.append(self.bullet(self.slot_link(slot, use_desc=True), level))
442
478
  if slot.name in sorted(self.synopsis.isarefs):
443
479
  for child in sorted(self.synopsis.isarefs[slot.name].slotrefs):
444
- self.pred_hier(self.schema.slots[child], level + 1)
480
+ items.append(self.pred_hier(self.schema.slots[child], level + 1))
481
+ return "\n".join(items) if items else None
445
482
 
446
- def enum_hier(self, enum: EnumDefinition, level=0) -> None:
447
- self.bullet(self.enum_link(enum, use_desc=True), level)
483
+ def enum_hier(self, enum: EnumDefinition, level=0) -> str:
484
+ items = []
485
+ items.append(self.bullet(self.enum_link(enum, use_desc=True), level))
448
486
  if enum.name in sorted(self.synopsis.isarefs):
449
487
  for child in sorted(self.synopsis.isarefs[enum.name].classrefs):
450
- self.enum_hier(self.schema.enums[child], level + 1)
488
+ items.append(self.enum_hier(self.schema.enums[child], level + 1))
489
+ return "\n".join(items) if items else None
451
490
 
452
491
  def dir_path(
453
492
  self,
@@ -465,15 +504,6 @@ class MarkdownGenerator(Generator):
465
504
  subdir = "/types" if isinstance(obj, TypeDefinition) and not self.no_types_dir else ""
466
505
  return f"{self.directory}{subdir}/{filename}.md"
467
506
 
468
- def mappings(self, obj: Union[SlotDefinition, ClassDefinition]) -> None:
469
- # TODO: get rid of this?
470
- # self.header(2, 'Mappings')
471
- # for mapping in obj.mappings:
472
- # self.bullet(f"{self.xlink(mapping)} {self.to_uri(mapping)}")
473
- # if obj.subclass_of:
474
- # self.bullet(self.xlink(obj.subclass_of))
475
- pass
476
-
477
507
  def is_secondary_ref(self, en: str) -> bool:
478
508
  """Determine whether 'en' is the name of something in the neighborhood of the requested classes
479
509
 
@@ -492,23 +522,27 @@ class MarkdownGenerator(Generator):
492
522
  else:
493
523
  return True
494
524
 
495
- def slot_field(self, cls: ClassDefinition, slot: SlotDefinition) -> None:
496
- self.bullet(f"{self.slot_link(slot)}{self.predicate_cardinality(slot)}")
525
+ def slot_field(self, cls: ClassDefinition, slot: SlotDefinition) -> str:
526
+ items = []
527
+ items.append(self.bullet(f"{self.slot_link(slot)}{self.predicate_cardinality(slot)}"))
497
528
  if slot.description:
498
- self.bullet(f"Description: {slot.description}", level=1)
499
- self.bullet(f"Range: {self.class_type_link(slot.range)}", level=1)
529
+ items.append(self.bullet(f"Description: {slot.description}", level=1))
530
+ items.append(self.bullet(f"Range: {self.class_type_link(slot.range)}", level=1))
500
531
  # if slot.subproperty_of:
501
532
  # self.bullet(f'edge label: {self.slot_link(slot.subproperty_of)}', level=1)
502
533
  for example in slot.examples:
503
- self.bullet(
504
- f'Example: {getattr(example, "value", " ")} {getattr(example, "description", " ")}',
505
- level=1,
534
+ items.append(
535
+ self.bullet(
536
+ f'Example: {getattr(example, "value", " ")} {getattr(example, "description", " ")}',
537
+ level=1,
538
+ )
506
539
  )
507
540
  # if slot.name not in self.own_slot_names(cls):
508
541
  # self.bullet(f'inherited from: {self.class_link(slot.domain)}', level=1)
509
542
  if slot.in_subset:
510
543
  ssl = ",".join(slot.in_subset)
511
- self.bullet(f"in subsets: ({ssl})", level=1)
544
+ items.append(self.bullet(f"in subsets: ({ssl})", level=1))
545
+ return "\n".join(items)
512
546
 
513
547
  def to_uri(self, uri_or_curie: str) -> str:
514
548
  """Return the URI for the slot if known"""
@@ -541,27 +575,28 @@ class MarkdownGenerator(Generator):
541
575
  return f" <sub><b>{card_str}</b></sub>"
542
576
 
543
577
  @staticmethod
544
- def anchor(id_: str) -> None:
545
- print(f'<a name="{id_}">', end="")
578
+ def anchor(id_: str) -> str:
579
+ return f'<a name="{id_}">'
546
580
 
547
581
  @staticmethod
548
- def anchorend() -> None:
549
- print("</a>")
582
+ def anchorend() -> str:
583
+ return "</a>"
550
584
 
551
- def header(self, level: int, txt: str) -> None:
585
+ def header(self, level: int, txt: str) -> str:
552
586
  txt = self.get_metamodel_slot_name(txt)
553
- print(f'\n{"#" * level} {txt}\n')
587
+ out = f'\n{"#" * level} {txt}\n'
588
+ return out
554
589
 
555
590
  @staticmethod
556
- def para(txt: str) -> None:
557
- print(f"\n{txt}\n")
591
+ def para(txt: str) -> str:
592
+ return f"\n{txt}\n"
558
593
 
559
594
  @staticmethod
560
- def bullet(txt: str, level=0) -> None:
561
- print(f'{" " * level} * {txt}')
595
+ def bullet(txt: str, level=0) -> str:
596
+ return f'{" " * level} * {txt}'
562
597
 
563
- def frontmatter(self, thingtype: str, layout="default") -> None:
564
- self.header(1, thingtype)
598
+ def frontmatter(self, thingtype: str, layout="default") -> str:
599
+ return self.header(1, thingtype)
565
600
  # print(f'---\nlayout: {layout}\n---\n')
566
601
 
567
602
  def bbin(self, obj: Element) -> str:
@@ -748,6 +783,11 @@ class MarkdownGenerator(Generator):
748
783
  return uri
749
784
 
750
785
 
786
+ def pad_heading(text: str) -> str:
787
+ """Add an extra newline to a non-top-level header that doesn't have one preceding it"""
788
+ return re.sub(r"(?<!\n)\n##", "\n\n##", text)
789
+
790
+
751
791
  @shared_arguments(MarkdownGenerator)
752
792
  @click.command()
753
793
  @click.option("--dir", "-d", required=True, help="Output directory")