66 "path"
77 "path/filepath"
88 "sort"
9+ "strconv"
910 "strings"
1011 "sync"
1112
@@ -19,6 +20,11 @@ import (
1920 "github.com/jeduden/mdsmith/internal/yamlutil"
2021)
2122
23+ // numericSortPrefix marks a sort spec whose key value should be
24+ // parsed as an integer before comparison. The prefix follows any
25+ // leading "-" descending marker — `-numeric:id`, not `numeric:-id`.
26+ const numericSortPrefix = "numeric:"
27+
2228func init () {
2329 rule .Register (& Rule {})
2430}
@@ -233,6 +239,7 @@ func validateSort(filePath string, line int, sortVal string) []lint.Diagnostic {
233239 `generated section directive has empty "sort" value` )}
234240 }
235241 key := strings .TrimPrefix (sortVal , "-" )
242+ key = strings .TrimPrefix (key , numericSortPrefix )
236243 if key == "" {
237244 return []lint.Diagnostic {makeDiag (filePath , line ,
238245 fmt .Sprintf ("generated section directive has invalid sort value %q" , sortVal ))}
@@ -308,7 +315,7 @@ func buildCatalogEntries(
308315 globFS , prefix := resolveGlobFS (f , params )
309316 files := resolveGlobMatchesFrom (globFS , f , params )
310317
311- sortKey , descending := parseSort (params )
318+ sortKey , descending , numeric := parseSort (params )
312319 _ , hasRow := params ["row" ]
313320 needFM := hasRow || (sortKey != "path" && sortKey != "filename" )
314321
@@ -334,7 +341,7 @@ func buildCatalogEntries(
334341 entries = append (entries , fileEntry {fields : fields })
335342 }
336343
337- sortEntries (entries , sortKey , descending )
344+ sortEntries (entries , sortKey , descending , numeric )
338345 return entries , diags
339346}
340347
@@ -484,26 +491,53 @@ func renderCatalogContent(
484491 return renderTemplate (params , entries , cols )
485492}
486493
487- // parseSort parses the sort value from params, returning the key and direction.
488- func parseSort (params map [string ]string ) (key string , descending bool ) {
494+ // parseSort parses the sort value from params, returning the key,
495+ // direction, and whether the value should be compared numerically.
496+ // The `numeric:` prefix opts into integer comparison and may follow
497+ // the descending `-` marker, e.g. `-numeric:id`.
498+ func parseSort (params map [string ]string ) (key string , descending , numeric bool ) {
489499 sortVal , ok := params ["sort" ]
490500 if ! ok || sortVal == "" {
491- return "path" , false
501+ return "path" , false , false
492502 }
493503
494504 if strings .HasPrefix (sortVal , "-" ) {
495- return sortVal [1 :], true
505+ descending = true
506+ sortVal = sortVal [1 :]
507+ }
508+ if strings .HasPrefix (sortVal , numericSortPrefix ) {
509+ numeric = true
510+ sortVal = sortVal [len (numericSortPrefix ):]
496511 }
497- return sortVal , false
512+ return sortVal , descending , numeric
498513}
499514
500- // sortEntries sorts file entries by the given key.
501- func sortEntries (entries []fileEntry , key string , descending bool ) {
502- sort .SliceStable (entries , func (i , j int ) bool {
503- vi := sortValue (entries [i ], key )
504- vj := sortValue (entries [j ], key )
515+ // sortEntries sorts file entries by the given key. When numeric is
516+ // true and every entry's value parses as an int, entries are ordered
517+ // by the integer value; any parse failure falls back to string
518+ // compare for the whole sort so behavior stays predictable when one
519+ // entry's field is missing or malformed.
520+ func sortEntries (entries []fileEntry , key string , descending , numeric bool ) {
521+ useInts := numeric && allParseAsInt (entries , key )
505522
506- cmp := strings .Compare (strings .ToLower (vi ), strings .ToLower (vj ))
523+ sort .SliceStable (entries , func (i , j int ) bool {
524+ var cmp int
525+ if useInts {
526+ // Re-parse from the current entry rather than a fixed
527+ // index — SliceStable reorders the slice during sort.
528+ ni , _ := parseSortInt (entries [i ], key )
529+ nj , _ := parseSortInt (entries [j ], key )
530+ switch {
531+ case ni < nj :
532+ cmp = - 1
533+ case ni > nj :
534+ cmp = 1
535+ }
536+ } else {
537+ vi := sortValue (entries [i ], key )
538+ vj := sortValue (entries [j ], key )
539+ cmp = strings .Compare (strings .ToLower (vi ), strings .ToLower (vj ))
540+ }
507541 if cmp == 0 {
508542 // Tiebreaker: path ascending, case-insensitive.
509543 pi := strings .ToLower (fieldinterp .Stringify (entries [i ].fields ["filename" ]))
@@ -518,6 +552,24 @@ func sortEntries(entries []fileEntry, key string, descending bool) {
518552 })
519553}
520554
555+ // allParseAsInt reports whether every entry's value for key parses
556+ // as an integer. Used to decide whether numeric mode applies before
557+ // sorting begins.
558+ func allParseAsInt (entries []fileEntry , key string ) bool {
559+ for _ , e := range entries {
560+ if _ , err := parseSortInt (e , key ); err != nil {
561+ return false
562+ }
563+ }
564+ return true
565+ }
566+
567+ // parseSortInt extracts the entry's sort key as a trimmed string
568+ // and parses it via strconv.Atoi.
569+ func parseSortInt (entry fileEntry , key string ) (int , error ) {
570+ return strconv .Atoi (strings .TrimSpace (sortValue (entry , key )))
571+ }
572+
521573// sortValue returns the sort value for a file entry given a key.
522574func sortValue (entry fileEntry , key string ) string {
523575 switch key {
0 commit comments