[OCaml] Mobile-friendly clone of cgit.
feat unified table of contents with nesting support
Add a common Ui.toc building block that both the README renderer and the commit diff view share. Entries support recursive children, rendered as nested <ul> elements for semantic hierarchy. - Ui.toc_entry accepts an optional children list for sub-entries - Ui.toc renders a collapsible disclosure with classes toc/toc-summary/toc-list - README TOC now builds a proper nested tree from heading levels - Commit detail pages show a TOC of changed files before the diff - CSS targets the shared base classes; nesting indentation is automatic - Tests updated to verify nested structure
Changed files
lib/prose/prose_format.ml
@@ -126,23 +126,26 @@
126
126
| Code_block (language, source) -> render_code_block language source
127
127
128
128
let render_toc ~title_text body_headings =
129
Removed:
match body_headings with
130
Removed:
| [] | [ _ ] -> Ui.nothing
131
Removed:
| _ ->
132
Removed:
let toc_anchor = new_anchor () in
133
Removed:
(match title_text with Some t -> ignore (toc_anchor t) | None -> ());
134
Removed:
let toc_entries =
135
Removed:
List.map
136
Removed:
(fun (level, text) ->
137
Removed:
let id = toc_anchor text in
138
Removed:
Ui.item
139
Removed:
~class_:(Printf.sprintf "readme-toc-%d" level)
140
Removed:
[ Ui.text_link ~href:("#" ^ id) text ])
141
Removed:
body_headings
142
Removed:
in
143
Removed:
Ui.disclosure ~class_:"readme-toc" ~summary_class:"readme-toc-summary"
144
Removed:
~summary:[ Ui.text "Table of Contents" ]
145
Removed:
[ Ui.items ~class_:"readme-toc-list" toc_entries ]
129
Added:
let toc_anchor = new_anchor () in
130
Added:
(match title_text with Some t -> ignore (toc_anchor t) | None -> ());
131
Added:
(* Build a nested tree from a flat (level, text) list. Headings at a deeper
132
Added:
level than the current base become children of the preceding entry. *)
133
Added:
let rec build base_level headings =
134
Added:
match headings with
135
Added:
| [] -> ([], [])
136
Added:
| (level, _) :: _ when level < base_level -> ([], headings)
137
Added:
| (level, text) :: rest ->
138
Added:
let id = toc_anchor text in
139
Added:
let children, rest' = build (level + 1) rest in
140
Added:
let entry = Ui.toc_entry ~children ~href:("#" ^ id) text in
141
Added:
let siblings, rest'' = build base_level rest' in
142
Added:
(entry :: siblings, rest'')
143
Added:
in
144
Added:
let min_level =
145
Added:
List.fold_left (fun acc (l, _) -> min acc l) max_int body_headings
146
Added:
in
147
Added:
let entries, _ = build min_level body_headings in
148
Added:
Ui.toc ~class_:"readme-toc" ~title:"Table of Contents" entries
146
149
147
150
(** Render a document using the given format. *)
148
151
let render format content =
lib/static/readme.css
@@ -122,14 +122,14 @@
122
122
}
123
123
124
124
/* Table of contents */
125
Removed:
.readme-toc {
125
Added:
.toc {
126
126
margin: 0.75rem 0 1.25rem;
127
127
border: 1px solid var(--color-border-subtle);
128
128
border-radius: 0.35rem;
129
129
background-color: #1e1e1e;
130
130
}
131
131
132
Removed:
.readme-toc-summary {
132
Added:
.toc-summary {
133
133
cursor: pointer;
134
134
list-style: none;
135
135
padding: 0.6rem 1rem;
@@ -139,47 +139,44 @@
139
139
user-select: none;
140
140
}
141
141
142
Removed:
.readme-toc-summary::-webkit-details-marker {
142
Added:
.toc-summary::-webkit-details-marker {
143
143
display: none;
144
144
}
145
145
146
Removed:
.readme-toc-summary::marker {
146
Added:
.toc-summary::marker {
147
147
content: "";
148
148
}
149
149
150
Removed:
.readme-toc-summary::before {
150
Added:
.toc-summary::before {
151
151
content: "▸ ";
152
152
}
153
153
154
Removed:
.readme-toc[open] > .readme-toc-summary::before {
154
Added:
.toc[open] > .toc-summary::before {
155
155
content: "▾ ";
156
156
}
157
157
158
Removed:
.readme-toc-list {
158
Added:
.toc-list {
159
159
margin: 0;
160
160
padding: 0 1rem 0.6rem;
161
161
list-style: none;
162
162
}
163
163
164
Removed:
.readme-toc-list li {
164
Added:
.toc-list .toc-list {
165
Added:
padding: 0 0 0 1.2em;
166
Added:
}
167
Added:
168
Added:
.toc-list li {
165
169
line-height: 1.7;
166
170
}
167
171
168
Removed:
.readme-toc-list li a {
172
Added:
.toc-list li a {
169
173
color: var(--color-link-hover);
170
174
text-decoration: none;
171
175
}
172
176
173
Removed:
.readme-toc-list li a:hover {
177
Added:
.toc-list li a:hover {
174
178
text-decoration: underline;
175
179
}
176
Removed:
177
Removed:
.readme-toc-1 { padding-left: 0; }
178
Removed:
.readme-toc-2 { padding-left: 1.2em; }
179
Removed:
.readme-toc-3 { padding-left: 2.4em; }
180
Removed:
.readme-toc-4 { padding-left: 3.6em; }
181
Removed:
.readme-toc-5 { padding-left: 4.8em; }
182
Removed:
.readme-toc-6 { padding-left: 6.0em; }
183
180
184
181
.readme-code-block {
185
182
margin: 1rem 0;
lib/views/ui.ml
@@ -150,9 +150,10 @@
150
150
let chevron ?(class_ = "tree-chevron") () =
151
151
inline ~class_ ~decorative:true [ text "\xe2\x80\xba" ]
152
152
153
Removed:
let disclosure ?class_ ?(expanded = false) ?summary_class ~summary children =
153
Added:
let disclosure ?id ?class_ ?(expanded = false) ?summary_class ~summary children
154
Added:
=
154
155
HTML.details
155
Removed:
(opt_class class_ @ flag_open expanded)
156
Added:
(opt_id id @ opt_class class_ @ flag_open expanded)
156
157
(HTML.summary (opt_class summary_class) summary :: children)
157
158
158
159
let css_toggle ~id:toggle_id ~toggle_class ~control_class ~label:control_label
@@ -174,6 +175,41 @@
174
175
[ text glyph ];
175
176
]
176
177
178
Added:
(* Table of contents *)
179
Added:
180
Added:
type toc_entry = {
181
Added:
toc_href : string;
182
Added:
toc_label : string;
183
Added:
toc_children : toc_entry list;
184
Added:
}
185
Added:
186
Added:
let toc_entry ?(children = []) ~href label =
187
Added:
{ toc_href = href; toc_label = label; toc_children = children }
188
Added:
189
Added:
let rec toc_items entries =
190
Added:
items ~class_:"toc-list"
191
Added:
(List.map
192
Added:
(fun { toc_href; toc_label; toc_children } ->
193
Added:
let nested =
194
Added:
match toc_children with [] -> [] | kids -> [ toc_items kids ]
195
Added:
in
196
Added:
item (text_link ~href:toc_href toc_label :: nested))
197
Added:
entries)
198
Added:
199
Added:
let toc ?class_ ~title entries =
200
Added:
let rec count = function
201
Added:
| [] -> 0
202
Added:
| e :: rest -> 1 + count e.toc_children + count rest
203
Added:
in
204
Added:
match entries with
205
Added:
| [] -> nothing
206
Added:
| _ when count entries < 2 -> nothing
207
Added:
| _ ->
208
Added:
let outer_class = classes [ "toc"; Option.value class_ ~default:"" ] in
209
Added:
disclosure ~class_:outer_class ~summary_class:"toc-summary"
210
Added:
~summary:[ text title ]
211
Added:
[ toc_items entries ]
212
Added:
177
213
(* Trees *)
178
214
179
215
let tree_leaf ?(modifier = "") ~href label =
@@ -380,7 +416,9 @@
380
416
[ block ~class_:"diff-lines" (List.map line_node lines) ];
381
417
]
382
418
383
Removed:
let file_node { path; detail; sections; note } =
419
Added:
let file_id index = Printf.sprintf "file-%d" (index + 1)
420
Added:
421
Added:
let file_node index { path; detail; sections; note } =
384
422
let body =
385
423
match note with
386
424
| Some note -> [ paragraph_text ~class_:"binary-diff" note ]
@@ -389,11 +427,18 @@
389
427
disclosure ~class_:"diff-file" ~expanded:true
390
428
~summary_class:"diff-file-header"
391
429
~summary:[ text path ]
430
Added:
~id:(file_id index)
392
431
(block ~class_:"diff-meta" [ text detail ] :: body)
393
432
433
Added:
let file_toc files =
434
Added:
toc ~class_:"diff-toc" ~title:"Changed files"
435
Added:
(List.mapi
436
Added:
(fun index { path; _ } -> toc_entry ~href:("#" ^ file_id index) path)
437
Added:
files)
438
Added:
394
439
let view ~empty_message = function
395
440
| [] -> [ paragraph_text empty_message ]
396
Removed:
| files -> List.map file_node files
441
Added:
| files -> file_toc files :: List.mapi file_node files
397
442
end
398
443
399
444
(* Document scaffolding *)
lib/views/ui.mli
@@ -156,6 +156,7 @@
156
156
technology. *)
157
157
158
158
val disclosure :
159
Added:
?id:string ->
159
160
?class_:string ->
160
161
?expanded:bool ->
161
162
?summary_class:string ->
@@ -181,6 +182,26 @@
181
182
scripting.
182
183
183
184
@param label the accessible name of the control, whose [glyph] has none. *)
185
Added:
186
Added:
(** {1 Table of contents} *)
187
Added:
188
Added:
type toc_entry
189
Added:
(** One entry in a table of contents. Build with {!val-toc_entry}. Entries may
190
Added:
contain nested children to represent subheading hierarchy. *)
191
Added:
192
Added:
val toc_entry : ?children:toc_entry list -> href:string -> string -> toc_entry
193
Added:
(** A TOC entry linking to a fragment.
194
Added:
195
Added:
@param children
196
Added:
nested sub-entries displayed as an indented list beneath this entry. *)
197
Added:
198
Added:
val toc : ?class_:string -> title:string -> toc_entry list -> node
199
Added:
(** A collapsible table of contents with support for nested sub-entries. Renders
200
Added:
as a disclosure widget with classes [toc], [toc-summary], and [toc-list].
201
Added:
Nested children produce nested [toc-list] elements for semantic indentation.
202
Added:
Returns {!nothing} when the list has fewer than two entries.
203
Added:
204
Added:
@param class_ an additional modifier alongside the base [toc] class. *)
184
205
185
206
(** {1 Trees}
186
207
test/test_readme.ml
@@ -10,6 +10,17 @@
10
10
in
11
11
fragment_length <= text_length && loop 0
12
12
13
Added:
let count text fragment =
14
Added:
let text_length = String.length text in
15
Added:
let fragment_length = String.length fragment in
16
Added:
let rec loop index acc =
17
Added:
if index > text_length - fragment_length then acc
18
Added:
else if String.sub text index fragment_length = fragment then
19
Added:
loop (index + 1) (acc + 1)
20
Added:
else loop (index + 1) acc
21
Added:
in
22
Added:
if fragment_length > text_length then 0 else loop 0 0
23
Added:
13
24
let render ~filename content =
14
25
Ogit.Prose.render ~filename content |> Dream_html.to_string
15
26
@@ -84,8 +95,8 @@
84
95
"TOC links to second heading" true
85
96
(contains html "href=\"#api\"");
86
97
Alcotest.(check bool)
87
Removed:
"TOC entry has level class" true
88
Removed:
(contains html "readme-toc-2")
98
Added:
"TOC has nested list for subheadings" true
99
Added:
(count html "toc-list" >= 2)
89
100
90
101
let test_org_toc () =
91
102
let html =
@@ -110,8 +121,8 @@
110
121
"Org TOC links to Usage" true
111
122
(contains html "href=\"#usage\"");
112
123
Alcotest.(check bool)
113
Removed:
"Org TOC level 2 entry" true
114
Removed:
(contains html "readme-toc-2");
124
Added:
"Org TOC has nested list for subheadings" true
125
Added:
(count html "toc-list" >= 2);
115
126
let find s sub =
116
127
let slen = String.length s and sublen = String.length sub in
117
128
let rec f i =