| // Copyright 2024 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package wrap is used to wrap comments/edits made to GitHub so that |
| // they can later be identified without referencing a database. |
| // This is especially useful for stripping out edits to a user-generated |
| // post made by a bot. |
| // |
| // DO NOT REMOVE/EDIT ANY LITERAL STRINGS OR FORMATTING USED IN THIS PACKAGE, |
| // (unless you have a good reason and a plan to migrate any existing usage). |
| // Go names can be changed. |
| // |
| // Wrapped strings ($body) are of the form: |
| // |
| // <!-- Generated by Oscar. DO NOT EDIT. {"bot":"$bot","kind":"$kind","meta":$meta} -->$body<!-- oscar-end --> |
| // |
| // where $bot and $kind are specified in [New], and $meta is arbitrary |
| // metadata (if nil, metadata is omitted). |
| // |
| // Note that the wrapping strategy is not robust against a user |
| // intentionally removing or editing the tag(s). |
| package wrap |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "regexp" |
| "strings" |
| |
| "golang.org/x/oscar/internal/storage" |
| ) |
| |
| // A Wrapper is used to wrap comments/edits made to GitHub so that |
| // they can later be identified without referencing a database. |
| type Wrapper struct { |
| bot, kind string |
| } |
| |
| // New returns a wrapper for GitHub modifications. |
| // bot is the name of the bot (e.g. "gabyhelp") that is making the edits, |
| // and kind is a context string used to identify the purpose/type |
| // of the edit (e.g "overview" or "related"). |
| func New(bot, kind string) *Wrapper { |
| return &Wrapper{ |
| bot: bot, kind: kind, |
| } |
| } |
| |
| // Wrap wraps body (the text of a GitHub modification). |
| // body must not already be wrapped, or contain the end tag (<!-- oscar-end -->). |
| // metadata is freeform metadata to include in the hidden tags. |
| // metadata is sanitized via [json.Marshal], so it may contain otherwise |
| // forbidden elements (e.g., "-->"). |
| func (w *Wrapper) Wrap(body string, metadata any) (string, error) { |
| // Body cannot contain the string [endTag]. |
| if strings.Contains(body, endTag) { |
| return "", fmt.Errorf("github: wrapped body cannot contain %q", endTag) |
| } |
| // DO NOT REMOVE/EDIT STRUCTURE OR CONTENTS. |
| return w.startTag(metadata) + body + endTag, nil |
| } |
| |
| // The text to inclue in the start tag before the metadata. |
| // DO NOT REMOVE/EDIT. |
| const tagText = "Generated by Oscar. DO NOT EDIT." |
| |
| // regexps for checking if a string is wrapped |
| var ( |
| reString = `<!-- ` + regexp.QuoteMeta(tagText) + ` (\{.*?\}) -->(?s)(.*?)<!-- oscar-end -->` |
| containsRE = regexp.MustCompile(reString) |
| isRE = regexp.MustCompile(`^` + reString + `$`) |
| ) |
| |
| // Is reports whether the string is of the form |
| // returned by [Wrapper.Wrap] (for any [Wrapper]). |
| func Is(s string) bool { |
| return isRE.MatchString(s) |
| } |
| |
| // Contains returns whether the given string |
| // contains one or more strings of the form output by |
| // [Wrapper.Wrap] (for any [Wrapper]). |
| func Contains(s string) bool { |
| return containsRE.MatchString(s) |
| } |
| |
| // Unwrapped contains the parsed contents of a wrapped string. |
| type Unwrapped struct { |
| TagContent |
| Body string |
| } |
| |
| // TagContent contains the structured contents of a start tag. |
| type TagContent struct { |
| Bot string `json:"bot"` |
| Kind string `json:"kind"` |
| Meta json.RawMessage `json:"meta,omitempty"` |
| } |
| |
| // Parse parses the contents of a wrapped string. |
| // It returns nil if the string is not wrapped. |
| func Parse(s string) *Unwrapped { |
| return parseMatch(isRE.FindStringSubmatch(s)) |
| } |
| |
| // parseMatch parses the contents of the match (returned from |
| // [regexp.Regexp.FindStringSubmatch] or similar). |
| func parseMatch(match []string) *Unwrapped { |
| // no match |
| if match == nil { |
| return nil |
| } |
| // bug in regexp ([isRE] or [containsRE]) |
| if len(match) != 3 { |
| panic(fmt.Sprintf("internal: bug in regexp: found %d match parts, want 3", len(match))) |
| } |
| // s is m[0] |
| tc, body := match[1], match[2] |
| var tagContent TagContent |
| if err := json.Unmarshal([]byte(tc), &tagContent); err != nil { |
| return nil |
| } |
| return &Unwrapped{ |
| TagContent: tagContent, |
| Body: body, |
| } |
| } |
| |
| // ParseAll parses the contents of all wrapped substrings in the |
| // string. It returns nil if there are no wrapped substrings, |
| // or any of them are malformed. |
| func ParseAll(s string) []*Unwrapped { |
| var us []*Unwrapped |
| matches := containsRE.FindAllStringSubmatch(s, -1) |
| if len(matches) == 0 { |
| return nil |
| } |
| for _, m := range matches { |
| u := parseMatch(m) |
| if u == nil { |
| return nil |
| } |
| us = append(us, u) |
| } |
| return us |
| } |
| |
| // Strip removes all wrapped substrings of the string |
| // and returns the result. |
| func Strip(s string) string { |
| return containsRE.ReplaceAllString(s, "") |
| } |
| |
| // startTag returns <!-- Generated by Oscar. DO NOT EDIT. {"bot":"$bot","kind":"$kind","meta":$metadata-json} --> |
| // where $metadata-json is the JSON representaion of the given metadata. |
| func (w *Wrapper) startTag(metadata any) string { |
| // DO NOT REMOVE/EDIT this function body. |
| var js json.RawMessage |
| if metadata != nil { |
| js = storage.JSON(metadata) |
| } |
| return fmt.Sprintf("<!-- %s %s -->", |
| tagText, |
| storage.JSON(TagContent{ |
| Bot: w.bot, |
| Kind: w.kind, |
| Meta: js, |
| })) |
| } |
| |
| // DO NOT REMOVE/EDIT. |
| const endTag = "<!-- oscar-end -->" |