mirror of https://github.com/harness/drone.git
202 lines
5.2 KiB
Go
202 lines
5.2 KiB
Go
// Copyright 2023 Harness, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package pullreq
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type suggestion struct {
|
|
checkSum string
|
|
code string
|
|
}
|
|
|
|
// parseSuggestions parses the provided string for any markdown code blocks that are suggestions.
|
|
func parseSuggestions(s string) []suggestion {
|
|
const languageSuggestion = "suggestion"
|
|
|
|
out := []suggestion{}
|
|
for len(s) > 0 {
|
|
code, language, remaining, found := findNextMarkdownCodeBlock(s)
|
|
|
|
// always update s to the remainder
|
|
s = remaining
|
|
|
|
if !found {
|
|
break
|
|
}
|
|
|
|
if !strings.EqualFold(language, languageSuggestion) {
|
|
continue
|
|
}
|
|
|
|
out = append(out,
|
|
suggestion{
|
|
checkSum: hashCodeBlock(code),
|
|
code: code,
|
|
},
|
|
)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func hashCodeBlock(s string) string {
|
|
return fmt.Sprintf("%x", sha256.Sum256([]byte(s)))
|
|
}
|
|
|
|
// findNextMarkdownCodeBlock finds a code block in markdown.
|
|
// NOTE: In the future we might want to use a proper markdown parser.
|
|
func findNextMarkdownCodeBlock(s string) (code string, language string, remaining string, found bool) {
|
|
// find fenced code block header
|
|
var startSequence string
|
|
s = foreachLine(s, func(line string) bool {
|
|
line, ok := trimMarkdownWhitespace(line)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
// try to find start sequence of a fenced code block (```+ or ~~~+)
|
|
startSequence, line = cutLongestPrefix(line, '~')
|
|
if len(startSequence) < 3 {
|
|
startSequence, line = cutLongestPrefix(line, '`')
|
|
if len(startSequence) < 3 {
|
|
// no code block prefix found in this line
|
|
return true
|
|
}
|
|
|
|
if strings.Contains(line, "`") {
|
|
// any single tic in the same line breaks a code block ``` opening
|
|
return true
|
|
}
|
|
}
|
|
|
|
language = strings.TrimSpace(line)
|
|
|
|
return false
|
|
})
|
|
|
|
if len(startSequence) == 0 {
|
|
return "", "", "", false
|
|
}
|
|
|
|
// parse codeBuilder block
|
|
codeBuilder := strings.Builder{}
|
|
linesAdded := 0
|
|
addLineToCode := func(line string) {
|
|
// To normalize we:
|
|
// - always use LF line ending
|
|
// - strip any line ending from last line
|
|
//
|
|
// e.g. "```suggestion\n```" is the same as "```suggestion\n" is the same as "```suggestion"
|
|
//
|
|
// This ensures similar result with and without end markers for fenced code blocks,
|
|
// and gives the user control on adding new lines at the end of the file.
|
|
if linesAdded > 0 {
|
|
codeBuilder.WriteByte('\n')
|
|
}
|
|
linesAdded++
|
|
|
|
codeBuilder.WriteString(line)
|
|
}
|
|
|
|
s = foreachLine(s, func(line string) bool {
|
|
// keep original line for appending it to code block if required
|
|
originalLine := line
|
|
|
|
line, ok := trimMarkdownWhitespace(line)
|
|
if !ok {
|
|
addLineToCode(originalLine)
|
|
return true
|
|
}
|
|
|
|
if !strings.HasPrefix(line, startSequence) {
|
|
addLineToCode(originalLine)
|
|
return true
|
|
}
|
|
|
|
_, line = cutLongestPrefix(line, rune(startSequence[0])) // any higher number of chars as starting sequence works
|
|
line = strings.TrimSpace(line) // spaces are fine
|
|
if len(line) > 0 {
|
|
// end of fenced code block can't contain anything else but spaces
|
|
addLineToCode(originalLine)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
})
|
|
|
|
return codeBuilder.String(), language, s, true
|
|
}
|
|
|
|
// trimMarkdownWhitespace returns the provided line by removing any leading whitespaces.
|
|
// If the white space makes it an indented code block line, false is returned.
|
|
func trimMarkdownWhitespace(line string) (string, bool) {
|
|
// remove any leading spaces
|
|
prefix, updatedLine := cutLongestPrefix(line, ' ')
|
|
if len(prefix) >= 4 {
|
|
// line is considered a code line by indentation
|
|
return line, false
|
|
}
|
|
|
|
// check for leading tabs (doesn't matter how many)
|
|
if strings.HasPrefix(updatedLine, "\t") {
|
|
// line is considered a code line by indentation
|
|
return line, false
|
|
}
|
|
|
|
return updatedLine, true
|
|
}
|
|
|
|
// foreachLine iterates over the provided string and calls "process" method for each line.
|
|
// If process returns false, or the scan reaches the end of the lines, the scanning stops.
|
|
// The method returns the remaining text of s.
|
|
func foreachLine(s string, process func(line string) bool) string {
|
|
for len(s) > 0 {
|
|
line, remaining, _ := strings.Cut(s, "\n")
|
|
|
|
// always update s to the remaining string
|
|
s = remaining
|
|
|
|
// handle CLRF
|
|
if lineLen := len(line); lineLen > 0 && line[lineLen-1] == '\r' {
|
|
line = line[:lineLen-1]
|
|
}
|
|
|
|
if !process(line) {
|
|
return s
|
|
}
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// cutLongestPrefix returns the longest prefix of repeating 'c' together with the remainder of the string.
|
|
func cutLongestPrefix(s string, c rune) (string, string) {
|
|
if len(s) == 0 {
|
|
return "", ""
|
|
}
|
|
|
|
i := strings.IndexFunc(s, func(r rune) bool { return r != c })
|
|
if i < 0 {
|
|
// no character found that's different from the provided rune!
|
|
return s, ""
|
|
}
|
|
|
|
return s[:i], s[i:]
|
|
}
|