github.com/anthonyme00/gomarkdoc@v1.0.0/lang/util.go (about) 1 package lang 2 3 import ( 4 "go/ast" 5 "go/printer" 6 "go/token" 7 "regexp" 8 "strings" 9 "unicode" 10 ) 11 12 func printNode(node ast.Node, fs *token.FileSet) (string, error) { 13 cfg := printer.Config{ 14 Mode: printer.UseSpaces, 15 Tabwidth: 4, 16 } 17 18 var out strings.Builder 19 if err := cfg.Fprint(&out, fs, node); err != nil { 20 return "", err 21 } 22 23 return out.String(), nil 24 } 25 26 func runeIsUpper(r rune) bool { 27 return r >= 'A' && r <= 'Z' 28 } 29 30 const lowerToUpper = 'a' - 'A' 31 32 func runeToUpper(r rune) rune { 33 return r - lowerToUpper 34 } 35 36 func splitCamel(text string) string { 37 var builder strings.Builder 38 var previousRune rune 39 var wordLength int 40 for i, r := range text { 41 if i == 0 { 42 previousRune = runeToUpper(r) 43 continue 44 } 45 46 switch { 47 case runeIsUpper(previousRune) && !runeIsUpper(r) && wordLength > 0: 48 // If we have a capital followed by a lower, that capital should 49 // begin a word. Throw a space before the runes if there is a word 50 // there. 51 builder.WriteRune(' ') 52 builder.WriteRune(previousRune) 53 wordLength = 1 54 case !runeIsUpper(previousRune) && runeIsUpper(r): 55 // If we have a lower followed by a capital, the capital should 56 // begin a word. Throw a space in between the runes. We don't have 57 // to check word length because we're writing the previous rune to 58 // the previous word, automaticall giving it a length of 1. 59 builder.WriteRune(previousRune) 60 builder.WriteRune(' ') 61 wordLength = 0 62 default: 63 // Otherwise, just throw the rune onto the previous word 64 builder.WriteRune(previousRune) 65 wordLength++ 66 } 67 68 previousRune = r 69 } 70 71 // Write the last rune 72 if previousRune != 0 { 73 builder.WriteRune(previousRune) 74 } 75 76 return builder.String() 77 } 78 79 func extractSummary(doc string) string { 80 firstParagraph := normalizeDoc(doc) 81 82 // Trim to first paragraph if there are multiple 83 if idx := strings.Index(firstParagraph, "\n\n"); idx != -1 { 84 firstParagraph = firstParagraph[:idx] 85 } 86 87 var builder strings.Builder 88 var lookback1 rune 89 var lookback2 rune 90 var lookback3 rune 91 for _, r := range formatDocParagraph(firstParagraph) { 92 // We terminate the sequence if we see a space preceded by a '.' which 93 // does not have exactly one word character before it (to avoid 94 // treating initials as the end of a sentence). 95 isPeriod := r == ' ' && lookback1 == '.' 96 isInitial := unicode.IsUpper(lookback2) && !unicode.IsLetter(lookback3) && !unicode.IsDigit(lookback3) 97 if isPeriod && !isInitial { 98 break 99 } 100 101 // Write the rune 102 builder.WriteRune(r) 103 104 // Update tracking variables 105 lookback3 = lookback2 106 lookback2 = lookback1 107 lookback1 = r 108 } 109 110 // Make the summary end with a period if it is nonempty and doesn't already. 111 if lookback1 != '.' && lookback1 != 0 { 112 builder.WriteRune('.') 113 } 114 115 return builder.String() 116 } 117 118 var crlfRegex = regexp.MustCompile("\r\n") 119 120 func normalizeDoc(doc string) string { 121 return strings.TrimSpace(crlfRegex.ReplaceAllString(doc, "\n")) 122 } 123 124 func formatDocParagraph(paragraph string) string { 125 var mergedParagraph strings.Builder 126 for i, line := range strings.Split(paragraph, "\n") { 127 if i > 0 { 128 mergedParagraph.WriteRune(' ') 129 } 130 131 mergedParagraph.WriteString(strings.TrimSpace(line)) 132 } 133 134 return mergedParagraph.String() 135 }