package shared import ( "sort" "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 index source.DocumentIndex } // NewSourceRefOrder captures the source identity and unit positions from doc. func NewSourceRefOrder(doc *source.SourceDocument) SourceRefOrder { return NewSourceRefOrderFromIndex(source.NewDocumentIndex(doc)) } // NewSourceRefOrderFromIndex captures source identity and unit positions from // one document index for source-reference comparison and canonicalization. func NewSourceRefOrderFromIndex(index source.DocumentIndex) SourceRefOrder { sourceID, ok := index.DocumentID() if !ok { return SourceRefOrder{} } return SourceRefOrder{sourceID: sourceID, index: index} } // 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 } if o.lessEndpoint(left.SourceID, left.StartUnitID, right.StartUnitID) { return true } if o.lessEndpoint(left.SourceID, right.StartUnitID, left.StartUnitID) { return false } return o.lessEndpoint(left.SourceID, 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 o.index.ValidateRef(ref) != nil { continue } start, _ := o.index.Position(ref.StartUnitID) 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) lessEndpoint(sourceID string, left, right int) bool { leftPosition, leftOK := o.position(sourceID, left) rightPosition, rightOK := o.position(sourceID, right) if leftOK != rightOK { return leftOK } if leftOK && leftPosition != rightPosition { return leftPosition < rightPosition } return left < right } func (o SourceRefOrder) position(sourceID string, unitID int) (int, bool) { if o.sourceID == "" || sourceID != o.sourceID { return 0, false } return o.index.Position(unitID) }