initial commit
This commit is contained in:
commit
23816d00f7
71 changed files with 14497 additions and 0 deletions
143
backend/docgen/merge.go
Normal file
143
backend/docgen/merge.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
package docgen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// beginPrefix opens the machine-written region of an imported page. Everything
|
||||
// between begin and EndMarker is replaced on every sync; anything outside it is
|
||||
// preserved exactly as written.
|
||||
const beginPrefix = "<!-- go-doc:generated:begin"
|
||||
|
||||
// EndMarker closes the machine-written region.
|
||||
const EndMarker = "<!-- go-doc:generated:end -->"
|
||||
|
||||
// stampPattern matches the generated=<time> field of the opening marker. It is
|
||||
// stripped before comparing an existing page with a freshly rendered one, because
|
||||
// the timestamp changes on every run and would otherwise make every sync look
|
||||
// like a real edit.
|
||||
//
|
||||
// The field has to be matched all the way to the "-->" that closes the comment:
|
||||
// Timestamp writes "2006-01-02 15:04 UTC", so a pattern that stopped at the
|
||||
// first space would leave the time behind and every sync that crossed a minute
|
||||
// boundary would rewrite all four pages for nothing.
|
||||
var stampPattern = regexp.MustCompile(`generated=[^\n]*?-->`)
|
||||
|
||||
// stampReplacement stands in for the stripped timestamp on both sides of the
|
||||
// comparison, so an old page and a fresh render still compare equal.
|
||||
const stampReplacement = "generated=STAMP-->"
|
||||
|
||||
// Status describes what Merge did, so callers can log precisely.
|
||||
type Status string
|
||||
|
||||
// Possible merge outcomes.
|
||||
const (
|
||||
// StatusCreated means no file existed, so the page was written whole.
|
||||
StatusCreated Status = "created"
|
||||
// StatusUpdated means the generated region was replaced in place.
|
||||
StatusUpdated Status = "updated"
|
||||
// StatusUnchanged means the generated region already matched.
|
||||
StatusUnchanged Status = "unchanged"
|
||||
// StatusSkipped means the file had no generated markers and force was off.
|
||||
StatusSkipped Status = "skipped"
|
||||
)
|
||||
|
||||
// Block locates the three boundaries of the generated region inside a page:
|
||||
// the opening marker, the body that follows it, and the byte after the closing
|
||||
// marker. All offsets are absolute and end-exclusive where it matters, so a
|
||||
// caller can rebuild a page as prefix + replacement + suffix.
|
||||
type Block struct {
|
||||
// Marker is the index of the opening marker.
|
||||
Marker int
|
||||
// Body is the index of the first byte after the opening marker's line.
|
||||
Body int
|
||||
// End is the index of the first byte after the closing marker.
|
||||
End int
|
||||
Found bool
|
||||
}
|
||||
|
||||
// FindBlock locates the generated markers. Both must be present and the opening
|
||||
// marker must sit at the start of a line, otherwise Found is false — which is
|
||||
// the signal that a file was authored by hand and must not be clobbered.
|
||||
func FindBlock(content string) Block {
|
||||
start := strings.Index(content, beginPrefix)
|
||||
if start < 0 {
|
||||
return Block{}
|
||||
}
|
||||
// The marker must own its line: either it opens the file or a newline
|
||||
// precedes it.
|
||||
if start > 0 && content[start-1] != '\n' {
|
||||
return Block{}
|
||||
}
|
||||
|
||||
lineEnd := strings.IndexByte(content[start:], '\n')
|
||||
if lineEnd < 0 {
|
||||
return Block{}
|
||||
}
|
||||
|
||||
body := start + lineEnd + 1
|
||||
|
||||
rest := content[body:]
|
||||
end := strings.Index(rest, EndMarker)
|
||||
if end < 0 {
|
||||
return Block{}
|
||||
}
|
||||
return Block{
|
||||
Marker: start,
|
||||
Body: body,
|
||||
End: body + end + len(EndMarker),
|
||||
Found: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Merge folds a freshly generated page into the existing file.
|
||||
//
|
||||
// Hand-written prose outside the markers survives untouched. A file with no
|
||||
// markers at all is left alone unless force is set, so importing can never
|
||||
// destroy a document a human wrote. An identical page is reported unchanged and
|
||||
// returned verbatim, so a no-op sync does not rewrite the file or churn its
|
||||
// modification time.
|
||||
func Merge(existing string, generated string, force bool) (string, Status, error) {
|
||||
if strings.TrimSpace(existing) == "" {
|
||||
return generated, StatusCreated, nil
|
||||
}
|
||||
|
||||
block := FindBlock(existing)
|
||||
if !block.Found {
|
||||
if !force {
|
||||
return existing, StatusSkipped, nil
|
||||
}
|
||||
return generated, StatusUpdated, nil
|
||||
}
|
||||
|
||||
// The replacement carries its own markers, so the preserved prefix stops
|
||||
// before the old opening marker rather than after it.
|
||||
var b strings.Builder
|
||||
b.WriteString(existing[:block.Marker])
|
||||
b.WriteString(generated)
|
||||
b.WriteString(existing[block.End:])
|
||||
|
||||
merged := b.String()
|
||||
if stampPattern.ReplaceAllString(merged, stampReplacement) ==
|
||||
stampPattern.ReplaceAllString(existing, stampReplacement) {
|
||||
return existing, StatusUnchanged, nil
|
||||
}
|
||||
return merged, StatusUpdated, nil
|
||||
}
|
||||
|
||||
// Wrap surrounds body with the generated markers, stamping provenance so a
|
||||
// reader can tell which project and which source revision produced the page.
|
||||
//
|
||||
// The body is normalised to exactly one trailing newline so the closing marker
|
||||
// always starts its own line, and no newline is emitted after that marker: the
|
||||
// marker is the last thing Wrap writes, so replacing a region in place and
|
||||
// keeping the following bytes of the file cannot double the trailing blank line.
|
||||
func Wrap(source, signature, body string) string {
|
||||
return fmt.Sprintf(
|
||||
"%s source=%s signature=%s generated=%s -->\n%s\n%s",
|
||||
beginPrefix, source, signature, Timestamp(),
|
||||
strings.TrimRight(body, "\n"), EndMarker,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue