From 294cc30b6d96327db596365f915e6dd179e52653 Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Mon, 2 Sep 2024 20:10:39 +0200 Subject: [PATCH] Improve doc comment parsing and rendering: - Allow multiple examples/outputs - Fix doc comments that are _just_ an `Example:` block, they wouldn't be highlighted - If a doc comment is entirely indented, trim that indentation before processing instead of rendering everything in a
 tag

---
 main.odin | 142 ++++++++++++++++++++++++++++--------------------------
 1 file changed, 75 insertions(+), 67 deletions(-)

diff --git a/main.odin b/main.odin
index 8f6c934d25..dd3f11be07 100644
--- a/main.odin
+++ b/main.odin
@@ -1641,18 +1641,31 @@ write_markup_text :: proc(w: io.Writer, s_: string) {
 }
 
 write_docs :: proc(w: io.Writer, docs: string, name: string = "") {
+
+	trim_empty_and_subtitle_lines_and_replace_lt :: proc(lines: []string, subtitle: string) -> []string {
+		lines := lines
+		for len(lines) > 0 && (strings.trim_space(lines[0]) == "" || strings.has_prefix(lines[0], subtitle)) {
+			lines = lines[1:]
+		}
+		for len(lines) > 0 && (strings.trim_space(lines[len(lines) - 1]) == "") {
+			lines = lines[:len(lines) - 1]
+		}
+		for &line in lines {
+			line = escape_html_string(line)
+		}
+		return lines
+	}
+
 	if docs == "" {
 		return
 	}
 
-	is_fmt := strings.has_prefix(docs, "package fmt")
-	_ = is_fmt
-
 	Block_Kind :: enum {
 		Paragraph,
 		Code,
 		Example,
 		Output,
+		Possible_Output,
 	}
 	Block :: struct {
 		kind: Block_Kind,
@@ -1664,27 +1677,36 @@ write_docs :: proc(w: io.Writer, docs: string, name: string = "") {
 	start := 0
 	blocks: [dynamic]Block
 
-	has_possible: bool
-	example_block: Block // when set the kind should be Example
-	output_block: Block // when set the kind should be Output
-	// rely on zii that the kinds have not been set
-	assert(example_block.kind != .Example)
-	assert(output_block.kind != .Output)
+	has_any_output: bool
+	has_example: bool
 
-	insert_block :: proc(block: Block, blocks: ^[dynamic]Block, example: ^Block, output: ^Block, name: string) {
-		switch block.kind {
-		case .Paragraph: fallthrough
-		case .Code: append(blocks, block)
-		case .Example:
-			if example.kind == .Example {
-				errorf("The documentation for %q has multiple examples which is not allowed\n", name)
+	// Find the minimum common prefix length of tabs, so an entire doc comment can be indented
+	// without it rendering in a 
 tag.
+	first_line_special := strings.has_prefix(lines_to_process[0], "Example:") || strings.has_prefix(lines_to_process[0], "Output:") || strings.has_prefix(lines_to_process[0], "Possible Output:")
+	if !first_line_special && len(lines_to_process) > 1 {
+		min_tabs: Maybe(int)
+		for line in lines_to_process[1:] {
+			if len(strings.trim_space(line)) == 0 {
+				continue
 			}
-			example^ = block
-		case .Output:
-			if example.kind == .Output {
-				errorf("The documentation for %q has multiple output which is not allowed\n", name)
+
+			tabs: int
+			for ch in line {
+				if ch == '\t' {
+					tabs += 1
+				} else {
+					break
+				}
+			}
+			min_tabs = min(tabs, min_tabs.? or_else max(int))
+		}
+		if min, has_min := min_tabs.?; has_min {
+			for &line in lines_to_process[1:] {
+				if len(strings.trim_space(line)) == 0 {
+					continue
+				}
+				line = line[min:]
 			}
-			output^ = block
 		}
 	}
 
@@ -1698,11 +1720,13 @@ write_docs :: proc(w: io.Writer, docs: string, name: string = "") {
 			switch {
 			case strings.has_prefix(line, "Example:"):
 				next_block_kind = .Example
+				has_example = true
 			case strings.has_prefix(line, "Output:"):
 				next_block_kind = .Output
+				has_any_output = true
 			case strings.has_prefix(line, "Possible Output:"):
-				next_block_kind = .Output
-				has_possible = true
+				next_block_kind = .Possible_Output
+				has_any_output = true
 			case strings.has_prefix(line, "\t"): next_block_kind = .Code
 			case text == "": force_write_block = true
 			}
@@ -1710,11 +1734,13 @@ write_docs :: proc(w: io.Writer, docs: string, name: string = "") {
 			switch {
 			case strings.has_prefix(line, "Example:"):
 				next_block_kind = .Example
+				has_example = true
 			case strings.has_prefix(line, "Output:"):
 				next_block_kind = .Output
+				has_any_output = true
 			case strings.has_prefix(line, "Possible Output:"):
-				next_block_kind = .Output
-				has_possible = true
+				next_block_kind = .Possible_Output
+				has_any_output = true
 			case !strings.has_prefix(line, "\t") && text != "":
 				next_block_kind = .Paragraph
 			}
@@ -1722,33 +1748,35 @@ write_docs :: proc(w: io.Writer, docs: string, name: string = "") {
 			switch {
 			case strings.has_prefix(line, "Output:"):
 				next_block_kind = .Output
+				has_any_output = true
 			case strings.has_prefix(line, "Possible Output:"):
-				next_block_kind = .Output
-				has_possible = true
+				next_block_kind = .Possible_Output
+				has_any_output = true
 			case !strings.has_prefix(line, "\t") && text != "":
 				next_block_kind = .Paragraph
 			}
-		case .Output:
+		case .Output, .Possible_Output:
 			switch {
 			case strings.has_prefix(line, "Example:"):
 				next_block_kind = .Example
+				has_example = true
 			case !strings.has_prefix(line, "\t") && text != "":
 				next_block_kind = .Paragraph
 			}
 		}
 
-		if i-start > 0 && (curr_block_kind != next_block_kind || force_write_block) {
-			insert_block(Block{curr_block_kind, lines_to_process[start:i]}, &blocks, &example_block, &output_block, name)
+		if curr_block_kind != next_block_kind || force_write_block {
+			append(&blocks, Block{curr_block_kind, lines_to_process[start:i]})
 			curr_block_kind = next_block_kind
 			start = i
 		}
 	}
 
 	if start < len(lines_to_process) {
-		insert_block(Block{curr_block_kind, lines_to_process[start:]}, &blocks, &example_block, &output_block, name)
+		append(&blocks, Block{curr_block_kind, lines_to_process[start:]})
 	}
 
-	if output_block.kind == .Output && example_block.kind != .Example {
+	if has_any_output && !has_example {
 		errorf("The documentation for %q has an output block but no example\n", name)
 	}
 
@@ -1824,45 +1852,25 @@ write_docs :: proc(w: io.Writer, docs: string, name: string = "") {
 				io.write_string(w, "\n")
 			}
 			io.write_string(w, "
\n") - case .Example: panic("We should not have example blocks in the block array") - case .Output: panic("We should not have output blocks in the block array") - } - } - - // Write example and output block if required - if example_block.kind == .Example { - trim_empty_and_subtitle_lines_and_replace_lt :: proc(lines: []string, subtitle: string) -> []string { - lines := lines - for len(lines) > 0 && (strings.trim_space(lines[0]) == "" || strings.has_prefix(lines[0], subtitle)) { - lines = lines[1:] - } - for len(lines) > 0 && (strings.trim_space(lines[len(lines) - 1]) == "") { - lines = lines[:len(lines) - 1] - } - for &line in lines { - line = escape_html_string(line) + case .Example: + // Example block starts with `Example:` and a number of white spaces, + example_lines := trim_empty_and_subtitle_lines_and_replace_lt(block.lines, "Example:") + + io.write_string(w, "
\n") + defer io.write_string(w, "
\n") + io.write_string(w, "Example:\n") + io.write_string(w, `
`)
+			for line in example_lines {
+				io.write_string(w, strings.trim_prefix(line, "\t"))
+				io.write_string(w, "\n")
 			}
-			return lines
-		}
-		// Example block starts with `Example:` and a number of white spaces,
-		example_lines := trim_empty_and_subtitle_lines_and_replace_lt(example_block.lines, "Example:")
-
-		io.write_string(w, "
\n") - defer io.write_string(w, "
\n") - io.write_string(w, "Example:\n") - io.write_string(w, `
`)
-		for line in example_lines {
-			io.write_string(w, strings.trim_prefix(line, "\t"))
-			io.write_string(w, "\n")
-		}
-		io.write_string(w, "
\n") + io.write_string(w, "
\n") - // Add the output block if it is present - if output_block.kind == .Output { + case .Output, .Possible_Output: // Output block starts with `Output:` or `Possible Output:` and a number of white spaces, - output_lines := trim_empty_and_subtitle_lines_and_replace_lt(output_block.lines, has_possible ? "Possible Output:" : "Output:") + output_lines := trim_empty_and_subtitle_lines_and_replace_lt(block.lines, block.kind == .Possible_Output ? "Possible Output:" : "Output:") - io.write_string(w, has_possible ? "Possible Output:" : "Output:\n") + io.write_string(w, block.kind == .Possible_Output ? "Possible Output:" : "Output:\n") io.write_string(w, `
`)
 			for line in output_lines {
 				io.write_string(w, strings.trim_prefix(line, "\t"))