[OCaml] Mobile-friendly clone of cgit.
feat add .mld and .txt as documentation formats in file view
.mld files are parsed with ocamldoc-style markup ({1 Heading}, {[code]}, {b bold}, {i italic}). .txt files render as plain prose paragraphs. Both now display as formatted documentation rather than syntax- highlighted source when browsed as file/ blobs.
lib/readme.ml
@@ -437,16 +437,195 @@
437
437
inline = org_inline;
438
438
}
439
439
440
Added:
(* {1 Mld (ocamldoc) parsing} *)
441
Added:
442
Added:
let mld_heading line =
443
Added:
let trimmed = String.trim line in
444
Added:
let len = String.length trimmed in
445
Added:
if len < 4 || trimmed.[0] <> '{' then None
446
Added:
else
447
Added:
match trimmed.[1] with
448
Added:
| '0' .. '6' when len > 3 && trimmed.[2] = ' ' ->
449
Added:
let level = Char.code trimmed.[1] - Char.code '0' in
450
Added:
let text_start = 3 in
451
Added:
let text_end = if trimmed.[len - 1] = '}' then len - 1 else len in
452
Added:
let text =
453
Added:
String.sub trimmed text_start (text_end - text_start) |> String.trim
454
Added:
in
455
Added:
Some (max 1 level, text)
456
Added:
| _ -> None
457
Added:
458
Added:
let mld_code_block_open line =
459
Added:
let trimmed = String.trim line in
460
Added:
if String.starts_with ~prefix:"{[" trimmed then
461
Added:
let rest = String.sub trimmed 2 (String.length trimmed - 2) in
462
Added:
if
463
Added:
String.length rest > 0
464
Added:
&& rest.[String.length rest - 1] = ']'
465
Added:
&& String.length rest > 1
466
Added:
&& rest.[String.length rest - 2] = '}'
467
Added:
then
468
Added:
(* Single-line code block: {[code]} on one line *)
469
Added:
None
470
Added:
else Some rest
471
Added:
else None
472
Added:
473
Added:
let mld_code_block_single line =
474
Added:
let trimmed = String.trim line in
475
Added:
let len = String.length trimmed in
476
Added:
if
477
Added:
len >= 4
478
Added:
&& String.starts_with ~prefix:"{[" trimmed
479
Added:
&& trimmed.[len - 2] = ']'
480
Added:
&& trimmed.[len - 1] = '}'
481
Added:
then Some (String.sub trimmed 2 (len - 4))
482
Added:
else None
483
Added:
484
Added:
let is_mld_code_block_close line =
485
Added:
let trimmed = String.trim line in
486
Added:
String.length trimmed >= 2
487
Added:
&& trimmed.[String.length trimmed - 2] = ']'
488
Added:
&& trimmed.[String.length trimmed - 1] = '}'
489
Added:
490
Added:
let parse_mld_blocks lines =
491
Added:
let is_boundary line =
492
Added:
String.trim line = ""
493
Added:
|| Option.is_some (mld_heading line)
494
Added:
|| Option.is_some (mld_code_block_open line)
495
Added:
|| Option.is_some (mld_code_block_single line)
496
Added:
in
497
Added:
let rec take_paragraph collected = function
498
Added:
| line :: _ as rest when is_boundary line -> (List.rev collected, rest)
499
Added:
| line :: rest -> take_paragraph (String.trim line :: collected) rest
500
Added:
| [] -> (List.rev collected, [])
501
Added:
in
502
Added:
let open Doc_format in
503
Added:
let rec loop blocks = function
504
Added:
| [] -> List.rev blocks
505
Added:
| line :: rest when String.trim line = "" -> loop blocks rest
506
Added:
| line :: rest -> (
507
Added:
match mld_heading line with
508
Added:
| Some (level, text) -> loop (Heading (level, text) :: blocks) rest
509
Added:
| None -> (
510
Added:
match mld_code_block_single line with
511
Added:
| Some code -> loop (Code_block (None, code) :: blocks) rest
512
Added:
| None -> (
513
Added:
match mld_code_block_open line with
514
Added:
| Some first_line ->
515
Added:
let code_lines, rest =
516
Added:
Doc_format.take_until is_mld_code_block_close [] rest
517
Added:
in
518
Added:
let all_lines =
519
Added:
if first_line = "" then code_lines
520
Added:
else first_line :: code_lines
521
Added:
in
522
Added:
let code = String.concat "\n" all_lines in
523
Added:
(* Strip trailing ]} if present in last consumed line *)
524
Added:
loop (Code_block (None, code) :: blocks) rest
525
Added:
| None ->
526
Added:
let paragraph, rest = take_paragraph [] (line :: rest) in
527
Added:
loop
528
Added:
(Paragraph (String.concat " " paragraph) :: blocks)
529
Added:
rest)))
530
Added:
in
531
Added:
loop [] lines
532
Added:
533
Added:
let mld_inline text =
534
Added:
let len = String.length text in
535
Added:
let buf = Buffer.create 64 in
536
Added:
let nodes = ref [] in
537
Added:
let flush () =
538
Added:
if Buffer.length buf > 0 then (
539
Added:
nodes := Ui.text (Buffer.contents buf) :: !nodes;
540
Added:
Buffer.clear buf)
541
Added:
in
542
Added:
let rec loop i =
543
Added:
if i >= len then flush ()
544
Added:
else
545
Added:
match text.[i] with
546
Added:
| '{' when i + 1 < len -> (
547
Added:
match text.[i + 1] with
548
Added:
| ('b' | 'i' | 'e') when i + 2 < len && text.[i + 2] = ' ' ->
549
Added:
flush ();
550
Added:
let close = find_brace_close (i + 3) 1 in
551
Added:
let content = String.sub text (i + 3) (close - i - 3) in
552
Added:
nodes :=
553
Added:
Ui.inline ~class_:"readme-emphasis" [ Ui.text content ]
554
Added:
:: !nodes;
555
Added:
loop (close + 1)
556
Added:
| '[' ->
557
Added:
flush ();
558
Added:
let close = find_code_close (i + 2) in
559
Added:
let content = String.sub text (i + 2) (close - i - 2) in
560
Added:
nodes := Ui.code_inline ~class_:"readme-code" content :: !nodes;
561
Added:
loop (close + 2)
562
Added:
| _ ->
563
Added:
Buffer.add_char buf '{';
564
Added:
loop (i + 1))
565
Added:
| c ->
566
Added:
Buffer.add_char buf c;
567
Added:
loop (i + 1)
568
Added:
and find_brace_close start depth =
569
Added:
if start >= len then len
570
Added:
else if text.[start] = '}' then
571
Added:
if depth <= 1 then start else find_brace_close (start + 1) (depth - 1)
572
Added:
else if text.[start] = '{' then find_brace_close (start + 1) (depth + 1)
573
Added:
else find_brace_close (start + 1) depth
574
Added:
and find_code_close start =
575
Added:
if start + 1 >= len then len
576
Added:
else if text.[start] = ']' && text.[start + 1] = '}' then start
577
Added:
else find_code_close (start + 1)
578
Added:
in
579
Added:
loop 0;
580
Added:
List.rev !nodes
581
Added:
582
Added:
let mld : Doc_format.format =
583
Added:
{
584
Added:
name = "mld";
585
Added:
css_class = "readme-document readme-mld";
586
Added:
parse =
587
Added:
(fun content ->
588
Added:
let lines = String.split_on_char '\n' content in
589
Added:
{
590
Added:
Doc_format.title = None;
591
Added:
metadata = [];
592
Added:
blocks = parse_mld_blocks lines;
593
Added:
});
594
Added:
inline = mld_inline;
595
Added:
}
596
Added:
597
Added:
(* {1 Plain text} *)
598
Added:
599
Added:
let plaintext : Doc_format.format =
600
Added:
{
601
Added:
name = "plaintext";
602
Added:
css_class = "readme-document readme-plaintext";
603
Added:
parse =
604
Added:
(fun content ->
605
Added:
let lines = String.split_on_char '\n' content in
606
Added:
let blocks =
607
Added:
List.filter_map
608
Added:
(fun line ->
609
Added:
let trimmed = String.trim line in
610
Added:
if trimmed = "" then None else Some (Doc_format.Paragraph trimmed))
611
Added:
lines
612
Added:
in
613
Added:
{ Doc_format.title = None; metadata = []; blocks });
614
Added:
inline = plain_inline;
615
Added:
}
616
Added:
440
617
(* {1 Public API} *)
441
618
442
619
let format_of_filename filename =
443
620
match Filename.extension filename |> String.lowercase_ascii with
444
621
| ".org" -> org
622
Added:
| ".mld" -> mld
623
Added:
| ".txt" -> plaintext
445
624
| _ -> markdown
446
625
447
626
let is_doc_filename filename =
448
627
match Filename.extension filename |> String.lowercase_ascii with
449
Removed:
| ".md" | ".markdown" | ".org" -> true
628
Added:
| ".md" | ".markdown" | ".org" | ".mld" | ".txt" -> true
450
629
| _ -> false
451
630
452
631
let render ~filename content =