From 9cfcde98c4c359d1c3566882ce70320a6b219bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Yves=20Landur=C3=A9?= Date: Sat, 12 Nov 2022 15:37:59 +0100 Subject: [PATCH] - Allow for indented function description (fix #50). - Add common code for multiline support for: description, example, stdout, stderr, stdin, set, see, and exitcode. --- shdoc | 267 +++++++++--------- tests/testcases/@exitcode.test.sh | 57 ++++ tests/testcases/@see.test.sh | 6 +- tests/testcases/@set.test.sh | 12 +- tests/testcases/@stdin.test.sh | 19 +- tests/testcases/@stdout.test.sh | 18 +- ...e-50-indented-function-description.test.sh | 97 +++++++ 7 files changed, 326 insertions(+), 150 deletions(-) create mode 100644 tests/testcases/@exitcode.test.sh create mode 100644 tests/testcases/issue-50-indented-function-description.test.sh diff --git a/shdoc b/shdoc index 3300061..9e86f96 100755 --- a/shdoc +++ b/shdoc @@ -35,6 +35,9 @@ BEGIN { styles["github", "set", "from"] = "^(\\S+) (\\S+)" styles["github", "set", "to"] = "**\\1** (\\2):" + styles["github", "li-preprocess", "from"] = "\n" + styles["github", "li-preprocess", "to"] = "\n " + styles["github", "li", "from"] = ".*" styles["github", "li", "to"] = "* &" @@ -50,7 +53,7 @@ BEGIN { styles["github", "anchor", "from"] = ".*" styles["github", "anchor", "to"] = "[&](#&)" - styles["github", "exitcode", "from"] = "([>!]?[0-9]{1,3}) (.*)" + styles["github", "exitcode", "from"] = "([>!]?[0-9]{1,3})[[:blank:]](.*)" styles["github", "exitcode", "to"] = "**\\1**: \\2" stderr_section_flag = 0 @@ -182,28 +185,30 @@ function reset() { description = "" } -function handle_description() { +function handle_description(text) { debug("→ handle_description") # Remove empty lines at the start of description. - sub(/^[[:space:]\n]*\n/, "", description) + sub(/^[[:space:]\n]*\n/, "", text) # Remove empty lines at the end of description. - sub(/[[:space:]\n]*$/, "", description) + sub(/[[:space:]\n]*$/, "", text) - if (description == "") { + if (text == "") { debug("→ → description: empty") return; } + description = text + if (section != "" && section_description == "") { debug("→ → section description: added") - section_description = description + section_description = text return; } if (file_description == "") { debug("→ → file description: added") - file_description = description + file_description = text return; } } @@ -315,8 +320,8 @@ function render_docblock_list(docblock, docblock_name, title) { for (i in docblock[docblock_name]) { docblock[docblock_name][i] # Ident additionnal lines to add them to the markdown list item. - gsub(/\n/, "\n ", docblock[docblock_name][i]) - item = render("li", docblock[docblock_name][i]) + item = render("li-preprocess", docblock[docblock_name][i]) + item = render("li", item) push(lines, item) } @@ -428,7 +433,9 @@ function render_docblock(func_name, description, docblock) { if ("example" in docblock) { push(lines, render("h4", "Example")) push(lines, render("code", "bash")) - push(lines, unindent(docblock["example"])) + # Unindent should be done by the new code. + #push(lines, unindent(docblock["example"])) + push(lines, docblock["example"]) push(lines, render("/code")) push(lines, "") } @@ -493,8 +500,8 @@ function render_docblock(func_name, description, docblock) { if ("set" in docblock) { push(lines, render("h4", "Variables set")) for (i in docblock["set"]) { - item = docblock["set"][i] - item = render("set", item) + item = render("set", docblock["set"][i]) + item = render("li-preprocess", item) item = render("li", item) push(lines, item) } @@ -506,7 +513,9 @@ function render_docblock(func_name, description, docblock) { if ("exitcode" in docblock) { push(lines, render("h4", "Exit codes")) for (i in docblock["exitcode"]) { - item = render("li", render("exitcode", docblock["exitcode"][i])) + item = render("exitcode", docblock["exitcode"][i]) + item = render("li-preprocess", item) + item = render("li", item) push(lines, item) } @@ -529,7 +538,8 @@ function render_docblock(func_name, description, docblock) { if ("see" in docblock) { push(lines, render("h4", "See also")) for (i in docblock["see"]) { - item = render("li", render_toc_link(docblock["see"][i])) + item = render("li-preprocess", render_toc_link(docblock["see"][i])) + item = render("li", item) push(lines, item) } @@ -551,6 +561,92 @@ function debug(msg) { debug("line: [" $0 "]") } +# Previous line added a new docblock item. +# Check if current line has the needed indentation +# for it to be a multiple lines docblock item. +# +# This process must be done before any @ tag detection. +multiple_line_tag { + # Determine if the tag allow for next line without additionnal indentation. + # This should be only be true for @description and @example tags, for the moment. + no_indentation_match = "" + if (multiple_line_tag ~ /^(description|example)$/) { + no_indentation_match = sprintf("|%s[^@].*", after_hash_indentation) + } + multiple_line_identation_regex = sprintf( \ + "^%s([[:blank:]]*|%s([[:blank:]]+[^[:blank:]]).*%s)$", \ + hash_indentation, \ + after_hash_indentation, \ + no_indentation_match \ + ) + + # Check if current line indentation does match the previous line docblock item. + if (match($0, multiple_line_identation_regex, contents)) { + debug("→ → @" multiple_line_tag " next line") + additional_line = contents[1] + + # Detect text internal indentation. + if(trim(additional_line) != "" \ + && match(additional_line, /^([[:blank:]]*)([^[:blank:]].*)?$/, detected_indentation)) + { + # Detect the minimal indentation of the text. + if(minimal_indentation == -1 \ + || length(minimal_indentation) > length(detected_indentation[1])) { + minimal_indentation = detected_indentation[1] + } + } + + # Remove trailing spaces. + sub(/[[:space:]]+$/, "") + + # Push matched message to corresponding docblock. + # docblock_append(multiple_line_docblock_name, "\n" $0) + text = concat(text, additional_line) + + # Stop processing current line, and process next line. + next + } else { + # End of the multiple line tag. + debug("→ → END of @" multiple_line_tag) + + # Remove minimal indentation from text. + if(minimal_indentation != -1) { + debug("→ → removing indentation from @ " multiple_line_tag " (length: " length(minimal_indentation) ")") + split(text, text_lines, "\n") + text = "" + for (i = 0; i < length(text_lines); i++) { + current_line = text_lines[i] + sub("^" minimal_indentation,"", current_line) + text = concat(text, current_line) + } + } + + # Remove empty lines at the start of text. + sub(/^[[:space:]\n]*\n/, "", text) + # Remove empty lines at the end of text. + sub(/[[:space:]\n]*$/, "", text) + + ## Print final text on debug output. + debug("→ → Final text for @" multiple_line_tag " : [\n" text "\n]") + + if(multiple_line_tag == "description") { + # If current tag is a description. + # Call handle_description with description set as the multiline text. + handle_description(text) + } else if (multiple_line_tag ~ /^(stdin|stdout|stderr|set|exitcode|see)$/) { + # If current tag is a multiple occurence tag. + # Push multi-line text as new item of the corresponding docblock. + docblock_push(multiple_line_tag, text) + } else { + docblock_set(multiple_line_tag, text) + } + + # End previous line docblock item. + multiple_line_tag = "" + } +} + + /^[[:space:]]*# @internal/ { debug("→ @internal") is_internal = 1 @@ -574,34 +670,33 @@ function debug(msg) { next } -/^[[:space:]]*# @description/ { - debug("→ @description") - in_description = 1 - in_example = 0 - - handle_description() - - reset() -} - -in_description { - if (/^[^[[:space:]]*#]|^[[:space:]]*# @[^d]|^[[:space:]]*[^#]|^[[:space:]]*$/) { - debug("→ → in_description: leave") +# Process @description entries. +# Allow for multiple lines entries. +match($0, /^([[:blank:]]*#)([[:blank:]]+)@(description|example|stdin|stdout|stderr|set|exitcode|see)[[:blank:]]*(.*[^[:blank:]])?[[:blank:]]*$/, contents) { + # Fetch matched values. + hash_indentation = contents[1] + after_hash_indentation = contents[2] + tag_name = contents[3] + if(tag_name == "example") { + # For @example tag, the content of the tag line is ignored. + text = "" + } else { + text = trim(contents[4]) + } + # minimal indentation is used to detect global indentation of the multiple line text. + # Line where the tag (e.g. @description) is is considered to have no indentation. + minimal_indentation = -1 - in_description = 0 + debug("→ @" tag_name) - handle_description() - } else { - debug("→ → in_description: concat") - sub(/^[[:space:]]*# @description[[:space:]]*/, "") - sub(/^[[:space:]]*#[[:space:]]*/, "") - sub(/^[[:space:]]*#$/, "") + # Signal the start of a multiple line tag. + multiple_line_tag = tag_name - description = concat(description, $0) - next - } + # Stop processing current line, and process next line. + next } + /^[[:space:]]*# @section/ { debug("→ @section") sub(/^[[:space:]]*# @section /, "") @@ -610,29 +705,6 @@ in_description { next } -/^[[:space:]]*# @example/ { - debug("→ @example") - - in_example = 1 - - - next -} - -in_example { - if (! /^[[:space:]]*#[ ]{1,}/) { - debug("→ → in_example: leave") - in_example = 0 - } else { - debug("→ → in_example: concat") - sub(/^[[:space:]]*#/, "") - - docblock_concat("example", $0) - next - } - -} - # Select @option lines with content. /^[[:blank:]]*#[[:blank:]]+@option[[:blank:]]+[^[:blank:]]/ { debug("→ @option") @@ -702,79 +774,6 @@ in_example { next } -/^[[:space:]]*# @set/ { - debug("→ @set") - sub(/^[[:space:]]*# @set /, "") - - docblock_push("set", $0) - - next -} - -/^[[:space:]]*# @exitcode/ { - debug("→ @exitcode") - sub(/^[[:space:]]*# @exitcode /, "") - - docblock_push("exitcode", $0) - - next -} - -/^[[:space:]]*# @see/ { - debug("→ @see") - sub(/[[:space:]]*# @see /, "") - - docblock_push("see", $0) - - next -} - -# Previous line added a new docblock item. -# Check if current line has the needed indentation -# for it to be a multiple lines docblock item. -multiple_line_docblock_name { - # Check if current line indentation does match the previous line docblock item. - if ($0 ~ multiple_line_identation_regex ) { - debug("→ @" multiple_line_docblock_name " - new line") - - # Current line has the same indentation as the stderr section. - - # Remove indentation and trailing spaces. - sub(/^[[:space:]]*#[[:space:]]+/, "") - sub(/[[:space:]]+$/, "") - - # Push matched message to corresponding docblock. - docblock_append(multiple_line_docblock_name, "\n" $0) - - # Stop processing current line, and process next line. - next - } else { - # End previous line docblock item. - multiple_line_docblock_name = "" - } -} - -# Process similarly @stdin, @stdout and @stderr entries. -# Allow for multiple lines entries. -match($0, /^([[:blank:]]*#[[:blank:]]+)@(stdin|stdout|stderr)[[:blank:]]+(.*[^[:blank:]])[[:blank:]]*$/, contents) { - # Fetch matched values. - indentation = contents[1] - docblock_name = contents[2] - text = contents[3] - - debug("→ @" docblock_name) - - # Push matched message to corresponding docblock. - docblock_push(docblock_name, text) - - # Signal the start of a multiple line section. - multiple_line_docblock_name = docblock_name - multiple_line_identation_regex = "^" indentation "[[:blank:]]+[^[:blank:]].*$" - - # Stop processing current line, and process next line. - next -} - /^[ \t]*(function([ \t])+)?([a-zA-Z0-9_\-:-\\.]+)([ \t]*)(\(([ \t]*)\))?[ \t]*\{/ \ && (length(docblock) != 0 || description != "") && !in_example { debug("→ function") @@ -801,7 +800,7 @@ match($0, /^([[:blank:]]*#[[:blank:]]+)@(stdin|stdout|stderr)[[:blank:]]+(.*[^[: /^[^#]*$/ { debug("→ break") - handle_description(); + #handle_description(); reset() next } diff --git a/tests/testcases/@exitcode.test.sh b/tests/testcases/@exitcode.test.sh new file mode 100644 index 0000000..49f77a1 --- /dev/null +++ b/tests/testcases/@exitcode.test.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +tests:put input <1 If a issue occured during processing, +# with a long comment here on the issue. +b() { +} + +EOF + +tests:put expected <1**: If a issue occured during processing, + with a long comment here on the issue. + +EOF + +assert diff --git a/tests/testcases/@see.test.sh b/tests/testcases/@see.test.sh index 6708f0d..bb4b989 100644 --- a/tests/testcases/@see.test.sh +++ b/tests/testcases/@see.test.sh @@ -39,7 +39,8 @@ tests:put input <