Skip to content

Commit a4287d7

Browse files
reginabuehleramgebauer
authored andcommitted
Enhance documentation anchors for One_Of options
1 parent 0b6203e commit a4287d7

File tree

4 files changed

+144
-33
lines changed

4 files changed

+144
-33
lines changed

doc/documentation/conf.py.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ html_theme_options = {
104104
# so a file named "default.css" will overwrite the builtin "default.css".
105105
# Since up to now, we don't have these, it's not needed. Uncomment if necessary.
106106
html_static_path = ["@PROJECT_BINARY_DIR@/doc/tmp/_static", "@_sphinx_OUT_DIR@/reference_docs/_static"]
107+
html_js_files = ['open_details_on_link.js']
107108
html_css_files = ['html_extensions.css']
108109
#
109110
# for finding headers as internal link targets, we have to set the variable myst_heading_anchors
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
(function () {
2+
// Scroll to and open section upon clicking a link
3+
function openAncestors(id) {
4+
if (!id) return;
5+
const target = document.getElementById(decodeURIComponent(id));
6+
if (!target) return;
7+
let el = target;
8+
while (el && el !== document.body) {
9+
if (el.tagName && el.tagName.toLowerCase() === 'details') el.open = true;
10+
el = el.parentElement;
11+
}
12+
target.scrollIntoView({ block: 'start', behavior: 'instant' });
13+
}
14+
15+
function run() { openAncestors(location.hash.slice(1)); }
16+
17+
document.addEventListener('DOMContentLoaded', run);
18+
window.addEventListener('hashchange', run);
19+
})();

doc/documentation/src/tutorial_templates/tutorial_fsi_monolithic.md.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ They are defined as follows:
162162
Thereby, `MAT: 1` specifies a Newtonian fluid for the fluid domain, while `MAT 2` defines a St.-Venant-Kirchhoff material for the solid domain. The values correspond to the pressure wave example [^Gerbeau2003a].
163163

164164
```{note}
165-
**Link to documentation:** For details, see the 4C documentation: [Newtonian fluid](https://4c-multiphysics.github.io/4C/documentation/materialreference.html#mat-fluid), [St.-Venant-Kirchhoff](https://4c-multiphysics.github.io/4C/documentation/materialreference.html#mat-struct-stvenantkirchhoff)
165+
**Link to documentation:** For details, see the 4C documentation: {ref}`Newtonian fluid<MATERIALS_MAT_fluid>`, {ref}`St.-Venant-Kirchhoff<MATERIALS_MAT_Struct_StVenantKirchhoff>`.
166166
```
167167

168168
### Geometry and mesh information

utilities/four_c_python/src/four_c_documentation/make_documentation.py

Lines changed: 123 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
DESCRIPTION_MISSING = '<span style="color:grey">*no description yet*</span>'
5959
TOO_MANY_TESTS_TO_SHOW = 200
6060
TESTS_TO_SHOW_IF_TOO_MANY = 20
61+
USED_ANCHORS = set()
6162

6263

6364
# Data class to store which sections belong to which chapter (and thus to which file)
@@ -74,6 +75,48 @@ class ReferenceChapter:
7475
has_content: bool = False # Whether the chapter has any content (filled later)
7576

7677

78+
def sanitize_anchor(text: str):
79+
"""
80+
Sanitize a string so it is safe and consistent to use as an explicit MyST anchor.
81+
82+
- Strip trailing HTML (e.g. ", <span ...>").
83+
- Remove '<' and '>'.
84+
- Remove whitespace.
85+
- Replace '/' with '_'.
86+
"""
87+
# cut off HTML decoration like ", <span style=...>"
88+
text = re.sub(r"\s*, <span.*", "", text).strip()
89+
text = text.replace("<", "").replace(">", "")
90+
text = text.replace(" ", "")
91+
text = text.replace("/", "_")
92+
return text
93+
94+
95+
def make_anchor_base(option_name: str, path_prefix: str | None):
96+
"""
97+
Build the base anchor from an option name and an optional hierarchical prefix.
98+
"""
99+
base = sanitize_anchor(option_name)
100+
if path_prefix:
101+
base = f"{path_prefix}_{base}"
102+
return base
103+
104+
105+
def make_unique_anchor(base: str):
106+
if base not in USED_ANCHORS:
107+
USED_ANCHORS.add(base)
108+
return base
109+
110+
n = 2
111+
candidate = f"{base}-{n}"
112+
while candidate in USED_ANCHORS:
113+
n += 1
114+
candidate = f"{base}-{n}"
115+
116+
USED_ANCHORS.add(candidate)
117+
return candidate
118+
119+
77120
def make_collapsible(content, summary=None, open_content=None):
78121
documentation = []
79122
if open_content is None:
@@ -247,7 +290,9 @@ def primitive_to_md(primitive: Primitive, make_description=make_spec_description
247290
)
248291

249292

250-
def vector_or_map_to_md(entry: Vector | Map, make_description=make_spec_description):
293+
def vector_or_map_to_md(
294+
entry: Vector | Map, make_description=make_spec_description, path_prefix: str = ""
295+
):
251296
type_description = type_or_none(value_type_flatter(entry), entry)
252297

253298
header_properties = {"size": flatten_size_vector(entry), "required": entry.required}
@@ -260,7 +305,9 @@ def vector_or_map_to_md(entry: Vector | Map, make_description=make_spec_descript
260305
collapsible_properties = None
261306
if not isinstance(entry.value_type, Primitive):
262307
collapsible_properties = {
263-
"Value type": all_of_to_md(All_Of([entry.value_type]))
308+
"Value type": all_of_to_md(
309+
All_Of([entry.value_type]), path_prefix=path_prefix
310+
)
264311
}
265312

266313
return make_description(
@@ -272,7 +319,9 @@ def vector_or_map_to_md(entry: Vector | Map, make_description=make_spec_descript
272319
)
273320

274321

275-
def tuple_to_md(tuple_entry: Tuple, make_description=make_spec_description):
322+
def tuple_to_md(
323+
tuple_entry: Tuple, make_description=make_spec_description, path_prefix: str = ""
324+
):
276325
type_description = type_or_none(value_type_flatter(tuple_entry), tuple_entry)
277326

278327
header_properties = {"required": tuple_entry.required}
@@ -282,7 +331,9 @@ def tuple_to_md(tuple_entry: Tuple, make_description=make_spec_description):
282331
header_properties["default"] = none_to_null(tuple_entry.default)
283332

284333
collapsible_properties = {
285-
"Entries": all_of_to_md(All_Of(tuple_entry.value_types), indent=1)
334+
"Entries": all_of_to_md(
335+
All_Of(tuple_entry.value_types), indent=1, path_prefix=path_prefix
336+
)
286337
}
287338

288339
return make_description(
@@ -320,12 +371,16 @@ def enum_to_md(enum: Enum, make_description=make_spec_description):
320371
)
321372

322373

323-
def group_to_md(group: Group, make_description=make_spec_description):
374+
def group_to_md(
375+
group: Group, make_description=make_spec_description, path_prefix: str = ""
376+
):
324377
type_description = type_or_none(group.spec_type, group)
325378

326379
collapsible_properties = {}
327380
if len(group.spec) > 0:
328-
collapsible_properties = {"Contains": all_of_to_md(group.spec, indent=1)}
381+
collapsible_properties = {
382+
"Contains": all_of_to_md(group.spec, indent=1, path_prefix=path_prefix)
383+
}
329384

330385
header_properties = validator_to_header_argument(group.validator)
331386
return make_description(
@@ -337,7 +392,9 @@ def group_to_md(group: Group, make_description=make_spec_description):
337392
)
338393

339394

340-
def list_to_md(list_entry: List, make_description=make_spec_description):
395+
def list_to_md(
396+
list_entry: List, make_description=make_spec_description, path_prefix: str = ""
397+
):
341398
type_description = type_or_none(list_entry.spec_type, list_entry)
342399

343400
header_properties = {}
@@ -351,18 +408,22 @@ def list_to_md(list_entry: List, make_description=make_spec_description):
351408
header_properties,
352409
list_entry.description,
353410
collapsible_properties={
354-
"Each element contains": all_of_to_md(list_entry.spec, indent=1)
411+
"Each element contains": all_of_to_md(
412+
list_entry.spec, indent=1, path_prefix=path_prefix
413+
)
355414
},
356415
)
357416

358417

359-
def selection_to_md(selection: Selection, make_description=make_spec_description):
418+
def selection_to_md(
419+
selection: Selection, make_description=make_spec_description, path_prefix: str = ""
420+
):
360421
type_description = type_or_none(selection.spec_type, selection)
361422

362423
choices = ""
363424
for choice, choice_spec in selection.choices.items():
364425
choices += "\n\n" + " " + f"- *{choice}*:\n\n"
365-
choices += all_of_to_md(choice_spec, indent=3)
426+
choices += all_of_to_md(choice_spec, indent=3, path_prefix=path_prefix)
366427

367428
return make_description(
368429
selection.name,
@@ -391,22 +452,31 @@ def sort_one_of_option_names(one_of: One_Of) -> list:
391452
return [", ".join(l) for l in options]
392453

393454

394-
def one_of_to_md(one_of: One_Of):
455+
def one_of_to_md(one_of: One_Of, path_prefix: str = ""):
395456
header = "*One of*"
396457

397458
description = ""
398459
if check_if_set(one_of.description):
399-
description += "\n" + description + "\n"
460+
description += "\n" + one_of.description + "\n"
400461

401462
open_content = len(one_of.specs) < 11
402463

403464
names = sort_one_of_option_names(one_of)
404465

405466
for i, spec in enumerate(one_of.specs):
467+
anchor_base = make_anchor_base(names[i], path_prefix)
468+
opt_anchor = make_unique_anchor(anchor_base)
469+
406470
key = "Option (" + names[i] + ")"
407-
description += "\n" + make_collapsible(
408-
textwrap.indent(all_of_to_md(spec, 1), " "), key, open_content
471+
472+
# Handle nested One Of
473+
nested_prefix = anchor_base
474+
content = textwrap.indent(
475+
all_of_to_md(spec, indent=1, path_prefix=nested_prefix),
476+
" ",
409477
)
478+
content = f"({opt_anchor})=\n\n" + content
479+
description += "\n" + make_collapsible(content, key, open_content)
410480

411481
return header, description
412482

@@ -450,7 +520,7 @@ def validator_to_header_argument(validator):
450520
raise ValueError(f"Unknown validator {validator}")
451521

452522

453-
def all_of_to_md(all_of: All_Of, indent=0):
523+
def all_of_to_md(all_of: All_Of, indent=0, path_prefix: str = ""):
454524
entries = ""
455525
for entry in all_of:
456526
string_entry = None
@@ -459,19 +529,31 @@ def all_of_to_md(all_of: All_Of, indent=0):
459529
case Primitive():
460530
string_entry, description_entry = primitive_to_md(entry)
461531
case Vector() | Map():
462-
string_entry, description_entry = vector_or_map_to_md(entry)
532+
string_entry, description_entry = vector_or_map_to_md(
533+
entry, path_prefix=path_prefix
534+
)
463535
case Tuple():
464-
string_entry, description_entry = tuple_to_md(entry)
536+
string_entry, description_entry = tuple_to_md(
537+
entry, path_prefix=path_prefix
538+
)
465539
case Enum():
466540
string_entry, description_entry = enum_to_md(entry)
467541
case Group():
468-
string_entry, description_entry = group_to_md(entry)
542+
string_entry, description_entry = group_to_md(
543+
entry, path_prefix=path_prefix
544+
)
469545
case List():
470-
string_entry, description_entry = list_to_md(entry)
546+
string_entry, description_entry = list_to_md(
547+
entry, path_prefix=path_prefix
548+
)
471549
case Selection():
472-
string_entry, description_entry = selection_to_md(entry)
550+
string_entry, description_entry = selection_to_md(
551+
entry, path_prefix=path_prefix
552+
)
473553
case One_Of():
474-
string_entry, description_entry = one_of_to_md(entry)
554+
string_entry, description_entry = one_of_to_md(
555+
entry, path_prefix=path_prefix
556+
)
475557
case _:
476558
raise ValueError(type(entry))
477559

@@ -487,13 +569,11 @@ def all_of_to_md(all_of: All_Of, indent=0):
487569

488570

489571
def create_section_markdown(section, section_in_tests, section_in_tutorials):
490-
# link anchor
491-
replacements = [(" ", ""), ("/", "_"), ("<", ""), (">", "")]
492-
section_link_anchor = "sec" + section.name.lower()
493-
for old, new in replacements:
494-
section_link_anchor = section_link_anchor.replace(old, new)
572+
section_link_anchor = "sec" + sanitize_anchor(section.name.lower())
495573
string_entry = "\n(" + section_link_anchor + ")=\n\n"
496574

575+
section_prefix = sanitize_anchor(section.name.upper())
576+
497577
create_section = partial(
498578
make_section_description,
499579
section_in_tests=section_in_tests,
@@ -504,19 +584,30 @@ def create_section_markdown(section, section_in_tests, section_in_tutorials):
504584
case Primitive():
505585
string_entry += primitive_to_md(section, create_section)
506586
case Vector() | Map():
507-
string_entry += vector_or_map_to_md(section, create_section)
587+
string_entry += vector_or_map_to_md(
588+
section, create_section, path_prefix=section_prefix
589+
)
508590
case Tuple():
509-
string_entry += tuple_to_md(section, create_section)
591+
string_entry += tuple_to_md(
592+
section, create_section, path_prefix=section_prefix
593+
)
510594
case Enum():
511595
string_entry += enum_to_md(section, create_section)
512596
case Group():
513-
string_entry += group_to_md(section, create_section)
597+
string_entry += group_to_md(
598+
section, create_section, path_prefix=section_prefix
599+
)
514600
case List():
515-
string_entry += list_to_md(section, create_section)
601+
string_entry += list_to_md(
602+
section, create_section, path_prefix=section_prefix
603+
)
516604
case Selection():
517-
string_entry += selection_to_md(section, create_section)
605+
string_entry += selection_to_md(
606+
section, create_section, path_prefix=section_prefix
607+
)
518608
case One_Of():
519-
string_entry += one_of_to_md(section, create_section)
609+
header, description = one_of_to_md(section, path_prefix=section_prefix)
610+
string_entry += header + "\n" + description + "\n"
520611
case _:
521612
raise ValueError(type(section))
522613
return string_entry + "\n\n"

0 commit comments

Comments
 (0)