Files
notarius/internal/modules/dnd/shared/source_ref_order.go

108 lines
3.0 KiB
Go

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
}