package shared import ( "sort" "strings" "gitea.maximumdirect.net/eric/notarius/internal/core/source" ) // SourceRefOrder provides a stable snapshot of a source document's unit // ordering for source-reference comparison and canonicalization. type SourceRefOrder struct { sourceID string positions map[int]int } // NewSourceRefOrder captures the source identity and unit positions from doc. func NewSourceRefOrder(doc *source.SourceDocument) SourceRefOrder { if doc == nil { return SourceRefOrder{} } positions := make(map[int]int, len(doc.Units)) for position, unit := range doc.Units { if _, exists := positions[unit.ID]; !exists { positions[unit.ID] = position } } return SourceRefOrder{sourceID: doc.ID, positions: positions} } // Less orders references by source identity, then document positions when // available, and finally literal endpoint IDs. func (o SourceRefOrder) Less(left, right source.SourceRef) bool { if left.SourceID != right.SourceID { return left.SourceID < right.SourceID } positions := o.positionsFor(left.SourceID) if lessEndpoint(positions, left.StartUnitID, right.StartUnitID) { return true } if lessEndpoint(positions, right.StartUnitID, left.StartUnitID) { return false } return lessEndpoint(positions, left.EndUnitID, right.EndUnitID) } // EarliestValid returns the earliest document position among valid refs. func (o SourceRefOrder) EarliestValid(refs []source.SourceRef) (int, bool) { if len(refs) == 0 || o.sourceID == "" { return 0, false } found := false earliest := 0 for _, ref := range refs { if ref.SourceID != o.sourceID || strings.TrimSpace(ref.SourceID) != ref.SourceID || ref.StartUnitID <= 0 || ref.EndUnitID <= 0 { continue } start, startOK := o.positions[ref.StartUnitID] end, endOK := o.positions[ref.EndUnitID] if !startOK || !endOK || start > end { continue } if !found || start < earliest { earliest = start found = true } } return earliest, found } // Canonicalize returns an owned, stable-sorted, exactly de-duplicated copy of // refs. It deliberately preserves invalid references for diagnostics. func (o SourceRefOrder) Canonicalize(refs []source.SourceRef) []source.SourceRef { if refs == nil { return nil } canonical := append([]source.SourceRef{}, refs...) sort.SliceStable(canonical, func(left, right int) bool { return o.Less(canonical[left], canonical[right]) }) unique := make([]source.SourceRef, 0, len(canonical)) for _, ref := range canonical { if len(unique) == 0 || unique[len(unique)-1] != ref { unique = append(unique, ref) } } return unique } func (o SourceRefOrder) positionsFor(sourceID string) map[int]int { if o.sourceID == "" || sourceID != o.sourceID { return nil } return o.positions } func lessEndpoint(positions map[int]int, left, right int) bool { leftPosition, leftOK := positions[left] rightPosition, rightOK := positions[right] if leftOK != rightOK { return leftOK } if leftOK && leftPosition != rightPosition { return leftPosition < rightPosition } return left < right }