release 0.1.0

This commit is contained in:
rus07tam 2026-05-06 12:21:06 +03:00
commit 30d94536a9
90 changed files with 7722 additions and 0 deletions

222
modules/std/path.owa Normal file
View file

@ -0,0 +1,222 @@
(namespace path
;; Platform-specific path separator.
(def sep (match platform.os-family
(:windows "\\")
(_ "/")
))
;; Internal: converts "/" to "\" on Windows; no-op on other platforms.
(fn norm-seps <Str>[<Str>p]
(match platform.os-family
(:windows (str.map (lambda [c _] (if (eq? c "/") "\\" c)) p))
(_ p)))
;; Returns the root prefix of a path.
;;
;; Unix — "/" for paths starting with "/", "" for relative.
;; Windows — "\" for UNC/rooted paths ("\" or "/" prefix)
;; "X:\" for drive-letter absolute paths ("X:\" or "X:/")
;; "" for all relative paths ("X:path" is relative on Windows)
(fn root <Str>[<Str>p]
(match platform.os-family
(:windows
(if (bool.or (str.starts-with? p "\\") (str.starts-with? p "/"))
"\\"
(if (bool.and (gte? (str.len p) 3)
(bool.and (eq? (str.chars-at p 1) ":")
(bool.or (eq? (str.chars-at p 2) "\\")
(eq? (str.chars-at p 2) "/"))))
(str.concat (str.take p 2) "\\")
"")))
(_ (if (str.starts-with? p "/") "/" ""))
))
;; Returns true if the path is absolute (has a non-empty root prefix).
(fn absolute? <Bool>[<Str>p]
(bool.not (str.empty? (root p))))
;; Returns true if the path is relative (has no root prefix).
(fn relative? <Bool>[<Str>p]
(bool.not (absolute? p)))
;; Splits a path into its string components using the platform separator.
;; On Windows "/" is also accepted as a separator.
;; The root prefix (if any) is preserved as the first component so that
;; split + join is a round-trip operation.
(fn split <Vec>[<Str>p]
(if (str.empty? p)
[]
(str.split (norm-seps p) sep)))
;; Joins path components with the platform separator.
(fn join <Str>[<Vec>p]
(str.join sep p))
;; Removes "." components, resolves ".." components, and returns the
;; canonical form of the path using the platform separator.
;;
;; Bug fixes vs. previous version:
;; • absolute paths that collapse entirely return the root (e.g. "/" not "")
;; • Windows drive-letter paths no longer get a spurious leading "\"
(fn normalize <Str>[<Str>p]
(seq
(def r (root p))
;; Work only on the non-root body so that root components never
;; participate in ".." resolution.
(def body (norm-seps (str.drop p (str.len r))))
(def parts (if (str.empty? body) [] (str.split body sep)))
(def normalized [])
(for part parts
(if (bool.or (eq? part ".") (str.empty? part)) (continue))
(set! normalized (if (eq? part "..")
(vec.drop-last normalized)
(vec.conj normalized part))))
(def result (join normalized))
(if (str.empty? r)
result
(if (str.empty? result)
r
(str.concat r result)))))
;; Returns the parent directory of the path.
;; • Paths directly under the root return the root string.
;; • Top-level relative paths (e.g. "a") return "".
;;
;; Bug fix vs. previous version: returns sep-based root, not hardcoded "/".
(fn parent <Str>[<Str>p] (seq
(def norm (normalize p))
(def r (root norm))
(def body (str.drop norm (str.len r)))
(def parts (if (str.empty? body)
[]
(vec.filter
(lambda [part _] (bool.not (str.empty? part)))
(str.split body sep))))
(def parent-parts (vec.drop-last parts))
(if (vec.empty? parent-parts)
r
(str.concat r (join parent-parts)))))
;; Returns the last component of the path (filename or final directory name).
;; Returns "" for paths that end with a separator.
(fn basename <Str>[<Str>p] (seq
(def parts (split p))
(if (vec.empty? parts) "" (vec.last parts))))
;; Returns the file extension including the leading dot, or "" if none.
;;
;; Rules:
;; • Hidden files whose name starts with "." have no extension.
;; • A trailing dot (e.g. "file.") is not considered an extension.
;; • Only the last dot in the basename is used (e.g. "a.tar.gz" → ".gz").
;;
;; Examples:
;; "file.txt" → ".txt"
;; "archive.tar.gz" → ".gz"
;; "file" → ""
;; ".hidden" → ""
;; "file." → ""
(fn ext <Str>[<Str>p] (seq
(def base (basename p))
(def dot-idx (vec.fold
(lambda [acc elem idx]
(if (eq? elem ".") idx acc))
-1
(vec.from base)))
(if (bool.or (lte? dot-idx 0) (eq? dot-idx (-- (str.len base))))
""
(str.drop base dot-idx))))
;; Returns the filename without its extension.
;;
;; Examples:
;; "file.txt" → "file"
;; "archive.tar.gz" → "archive.tar"
;; "file" → "file"
;; ".hidden" → ".hidden"
(fn stem <Str>[<Str>p] (seq
(def base (basename p))
(def e (ext p))
(if (str.empty? e)
base
(str.take base (- (str.len base) (str.len e))))))
;; Returns the path with its extension replaced by new-ext.
;; Pass "" as new-ext to strip the extension entirely.
;; The directory prefix and stem are preserved unchanged.
;;
;; Examples:
;; (with-ext "file.txt" ".md") → "file.md"
;; (with-ext "file.txt" "") → "file"
;; (with-ext "file" ".txt") → "file.txt"
(fn with-ext <Str>[<Str>p <Str>new-ext] (seq
(def e (ext p))
(def without-ext
(if (str.empty? e)
p
(str.take p (- (str.len p) (str.len e)))))
(str.concat without-ext new-ext)))
;; Resolves path relative to base and returns the normalized result.
;; If path is already absolute it is returned normalized, ignoring base.
;; Otherwise base + sep + path is normalized.
;;
;; Examples (Unix):
;; (resolve "/base" "file.txt") → "/base/file.txt"
;; (resolve "/base/dir" "../file.txt") → "/base/file.txt"
;; (resolve "/other" "/abs") → "/abs"
(fn resolve <Str>[<Str>base <Str>p]
(if (absolute? p)
(normalize p)
(normalize (str.concat base sep p))))
;; Computes the relative path from base to target.
;;
;; • Returns "." when base and target are the same path.
;; • When the roots differ (e.g. different Windows drive letters)
;; the normalized target is returned unchanged.
;;
;; Examples (Unix):
;; (relative-to "/a/b" "/a/b/c/d") → "c/d"
;; (relative-to "/a/b" "/a/c") → "../c"
;; (relative-to "/a/b/c" "/a") → "../.."
;; (relative-to "/a/b" "/a/b") → "."
(fn relative-to <Str>[<Str>base <Str>target] (seq
(def norm-base (normalize base))
(def norm-target (normalize target))
(def base-r (root norm-base))
(def target-r (root norm-target))
(if (nq? base-r target-r)
norm-target
(seq
(def base-body (str.drop norm-base (str.len base-r)))
(def target-body (str.drop norm-target (str.len target-r)))
(fn parts-of [body]
(if (str.empty? body)
[]
(vec.filter
(lambda [part _] (bool.not (str.empty? part)))
(str.split body sep))))
(def base-parts (parts-of base-body))
(def target-parts (parts-of target-body))
(def base-len (vec.len base-parts))
(def target-len (vec.len target-parts))
;; Find length of the common prefix.
(def i 0)
(while (bool.and (lt? i base-len)
(bool.and (lt? i target-len)
(eq? (vec.get base-parts i)
(vec.get target-parts i))))
(set! i (++ i)))
;; One ".." per remaining component in base, then the remaining target components.
(def ups (vec.map (lambda [_e _i] "..") (vec.drop base-parts i)))
(def remaining (vec.drop target-parts i))
(def result-parts
(vec.fold
(lambda [acc elem _] (vec.conj acc elem))
ups
remaining))
(if (vec.empty? result-parts)
"."
(join result-parts))))))
)