package state import ( "encoding/json" "fmt" "strings" "sync" ) // The state comment is wrapped in an HTML comment so the forge's web page hides it from readers. const ( commentPrefix = "" // legacyMarker heads the state comments the YouTrack integration wrote before the hidden format existed. legacyMarker = "" ) // knownFields lists the top-level keys of the embedded schema, so a comment counts as state only if it uses a real state key. var knownFields = sync.OnceValue(func() map[string]struct{} { var doc struct { Properties map[string]json.RawMessage `json:"properties"` } if err := json.Unmarshal(schemaBytes, &doc); err != nil || len(doc.Properties) == 0 { panic(fmt.Sprintf("内嵌 schema 没有可用的 properties: %v", err)) } set := make(map[string]struct{}, len(doc.Properties)) for name := range doc.Properties { set[name] = struct{}{} } return set }) // FormatComment renders `s` as a comment body that the forge's web page hides: ``. // // [json.Marshal] escapes `<`, `>` and `&` as `\u003c`, `\u003e` and `\u0026`, so no value can close the HTML comment early. func FormatComment(s map[string]any) (string, error) { buf, err := json.Marshal(s) if err != nil { return "", fmt.Errorf("序列化 state JSON 失败: %w", err) } return commentPrefix + "\n" + string(buf) + "\n" + commentSuffix, nil } // ParseComment reads the state out of a comment body, or reports `false` when the body is not a state comment. // // It accepts the hidden format [FormatComment] writes, the older YouTrack form (`` then JSON) and an // older bare JSON object. The JSON must be an object carrying at least one schema field, so ordinary chatter that // happens to be JSON is not mistaken for state. func ParseComment(body string) (map[string]any, bool) { text := strings.TrimSpace(body) switch { case strings.HasPrefix(text, legacyMarker): text = text[len(legacyMarker):] case strings.HasPrefix(text, commentPrefix) && strings.HasSuffix(text, commentSuffix) && len(text) >= len(commentPrefix)+len(commentSuffix): text = text[len(commentPrefix) : len(text)-len(commentSuffix)] } var m map[string]any if err := json.Unmarshal([]byte(text), &m); err != nil || m == nil { return nil, false } fields := knownFields() for k := range m { if _, ok := fields[k]; ok { return m, true } } return nil, false } // Latest returns the state from the newest state comment among `bodies` (oldest first), or an empty map when there is none. // // Scanning from the end means comments posted after the state, such as review notes or chatter, never hide it. func Latest(bodies []string) map[string]any { for i := len(bodies) - 1; i >= 0; i-- { if m, ok := ParseComment(bodies[i]); ok { return m } } return map[string]any{} }