///|
pub(all) struct PathInfo {
raw : String
normalized : String
segments : Array[String]
dirname : String
basename : String
stem : String
extension : String?
depth : Int
hidden : Bool
} derive(Eq)
///|
pub fn path_info(raw : String) -> Result[PathInfo, ReslockError] {
let normalized = match normalize_path(raw) {
Ok(path) => path
Err(err) => return Err(err)
}
let segments = split_to_array(normalized, "/")
let basename = path_basename_from_segments(segments)
let dirname = path_dirname_from_segments(segments)
let stem = path_stem_from_basename(basename)
let extension = path_extension_from_basename(basename)
Ok({
raw,
normalized,
segments,
dirname,
basename,
stem,
extension,
depth: segments.length(),
hidden: basename.has_prefix(".") && basename.length() > 1,
})
}
///|
pub fn path_segments(raw : String) -> Result[Array[String], ReslockError] {
match normalize_path(raw) {
Ok(path) => Ok(split_to_array(path, "/"))
Err(err) => Err(err)
}
}
///|
pub fn path_basename(raw : String) -> Result[String, ReslockError] {
match path_info(raw) {
Ok(info) => Ok(info.basename)
Err(err) => Err(err)
}
}
///|
pub fn path_dirname(raw : String) -> Result[String, ReslockError] {
match path_info(raw) {
Ok(info) => Ok(info.dirname)
Err(err) => Err(err)
}
}
///|
pub fn path_stem(raw : String) -> Result[String, ReslockError] {
match path_info(raw) {
Ok(info) => Ok(info.stem)
Err(err) => Err(err)
}
}
///|
pub fn path_extension(raw : String) -> Result[String?, ReslockError] {
match path_info(raw) {
Ok(info) => Ok(info.extension)
Err(err) => Err(err)
}
}
///|
pub fn path_depth(raw : String) -> Result[Int, ReslockError] {
match path_info(raw) {
Ok(info) => Ok(info.depth)
Err(err) => Err(err)
}
}
///|
pub fn path_is_hidden(raw : String) -> Result[Bool, ReslockError] {
match path_info(raw) {
Ok(info) => Ok(info.hidden)
Err(err) => Err(err)
}
}
///|
pub fn path_parent_chain(raw : String) -> Result[Array[String], ReslockError] {
let info = match path_info(raw) {
Ok(info) => info
Err(err) => return Err(err)
}
let chain : Array[String] = []
let prefix_parts : Array[String] = []
let mut seen = 0
let total = info.segments.length()
for segment in info.segments {
seen = seen + 1
if seen < total {
prefix_parts.push(segment)
chain.push(prefix_parts.join("/"))
}
}
Ok(chain)
}
///|
pub fn path_without_extension(raw : String) -> Result[String, ReslockError] {
match path_info(raw) {
Ok(info) =>
if info.extension is Some(_) {
Ok(path_without_last_extension(info))
} else {
Ok(info.normalized)
}
Err(err) => Err(err)
}
}
///|
pub fn path_with_extension(
raw : String,
extension : String,
) -> Result[String, ReslockError] {
let info = match path_info(raw) {
Ok(info) => info
Err(err) => return Err(err)
}
if extension.trim().is_empty() {
return Ok(info.normalized)
}
let clean = extension.trim().to_owned().to_lower()
let stem = info.stem
if stem == "" {
Ok(info.normalized)
} else if info.dirname == "." {
Ok(stem + "." + clean)
} else {
Ok(info.dirname + "/" + stem + "." + clean)
}
}
///|
pub fn path_is_readme_like(raw : String) -> Result[Bool, ReslockError] {
match path_info(raw) {
Ok(info) =>
Ok(
is_named_document(info.basename, [
"readme", "readme.md", "readme.txt", "readme.mdx",
]),
)
Err(err) => Err(err)
}
}
///|
pub fn path_is_license_like(raw : String) -> Result[Bool, ReslockError] {
match path_info(raw) {
Ok(info) =>
Ok(
is_named_document(info.basename, [
"license", "license.md", "license.txt", "copying", "copyright",
]),
)
Err(err) => Err(err)
}
}
///|
pub fn path_is_changelog_like(raw : String) -> Result[Bool, ReslockError] {
match path_info(raw) {
Ok(info) =>
Ok(
is_named_document(info.basename, [
"changelog", "changes", "news", "history",
]),
)
Err(err) => Err(err)
}
}
///|
pub fn path_is_document_like(raw : String) -> Result[Bool, ReslockError] {
match path_info(raw) {
Ok(info) =>
Ok(
path_is_readme_name(info.basename) ||
path_is_license_name(info.basename) ||
path_is_changelog_name(info.basename) ||
path_is_document_extension(info.extension),
)
Err(err) => Err(err)
}
}
///|
pub fn path_is_config_like(raw : String) -> Result[Bool, ReslockError] {
match path_info(raw) {
Ok(info) =>
Ok(
path_is_config_extension(info.extension) ||
path_is_config_name(info.basename),
)
Err(err) => Err(err)
}
}
///|
pub fn path_is_source_like(raw : String) -> Result[Bool, ReslockError] {
match path_info(raw) {
Ok(info) =>
Ok(
path_is_source_extension(info.extension) ||
path_is_script_extension(info.extension) ||
path_is_shader_extension(info.extension),
)
Err(err) => Err(err)
}
}
///|
pub fn path_is_media_like(raw : String) -> Result[Bool, ReslockError] {
match path_info(raw) {
Ok(info) =>
Ok(
path_is_image_extension(info.extension) ||
path_is_audio_extension(info.extension) ||
path_is_video_extension(info.extension) ||
path_is_font_extension(info.extension),
)
Err(err) => Err(err)
}
}
///|
pub fn path_is_archive_like(raw : String) -> Result[Bool, ReslockError] {
match path_info(raw) {
Ok(info) => Ok(path_is_archive_extension(info.extension))
Err(err) => Err(err)
}
}
///|
pub fn path_is_generated_like(raw : String) -> Result[Bool, ReslockError] {
match path_info(raw) {
Ok(info) =>
Ok(
info.normalized.has_prefix("generated/") ||
info.normalized.has_prefix("gen/") ||
info.normalized.has_prefix("build/") ||
info.normalized.has_prefix("dist/") ||
info.normalized.has_prefix("cache/") ||
info.normalized.contains("/generated/") ||
info.normalized.contains("/gen/") ||
info.normalized.contains("/build/") ||
info.normalized.contains("/dist/") ||
info.normalized.contains("/cache/"),
)
Err(err) => Err(err)
}
}
///|
pub fn render_path_info(info : PathInfo) -> String {
let out = StringBuilder()
out.write_string(info.normalized)
out.write_string(" [")
out.write_string("depth=")
out.write_string(info.depth.to_string())
out.write_string(", basename=")
out.write_string(info.basename)
out.write_string(", dirname=")
out.write_string(info.dirname)
out.write_string(", stem=")
out.write_string(info.stem)
out.write_string(", extension=")
match info.extension {
Some(extension) => out.write_string(extension)
None => out.write_string("-")
}
if info.hidden {
out.write_string(", hidden")
}
out.write_string("]")
out.to_string()
}
///|
fn path_basename_from_segments(segments : Array[String]) -> String {
let mut basename = ""
for segment in segments {
basename = segment
}
basename
}
///|
fn path_dirname_from_segments(segments : Array[String]) -> String {
if segments.length() <= 1 {
return "."
}
let prefix_parts : Array[String] = []
let mut seen = 0
let total = segments.length()
for segment in segments {
seen = seen + 1
if seen < total {
prefix_parts.push(segment)
}
}
prefix_parts.join("/")
}
///|
fn path_stem_from_basename(basename : String) -> String {
if basename.has_prefix(".") {
return basename
}
let pieces = split_to_array(basename, ".")
if pieces.length() <= 1 {
return basename
}
let out = StringBuilder()
let mut seen = 0
let total = pieces.length()
for piece in pieces {
seen = seen + 1
if seen < total {
if seen > 1 {
out.write_string(".")
}
out.write_string(piece)
}
}
out.to_string()
}
///|
fn path_extension_from_basename(basename : String) -> String? {
if basename.has_prefix(".") {
return None
}
let pieces = split_to_array(basename, ".")
if pieces.length() <= 1 {
return None
}
let mut extension = ""
for piece in pieces {
extension = piece
}
if extension == "" {
None
} else {
Some(extension.to_lower())
}
}
///|
fn is_named_document(basename : String, names : Array[String]) -> Bool {
let lower = basename.to_lower()
for name in names {
if lower == name {
return true
}
}
false
}
///|
fn path_is_readme_name(basename : String) -> Bool {
is_named_document(basename, [
"readme", "readme.md", "readme.txt", "readme.mdx",
])
}
///|
fn path_is_license_name(basename : String) -> Bool {
is_named_document(basename, [
"license", "license.md", "license.txt", "copying", "copyright",
])
}
///|
fn path_is_changelog_name(basename : String) -> Bool {
is_named_document(basename, ["changelog", "changes", "news", "history"])
}
///|
fn path_is_document_extension(extension : String?) -> Bool {
match extension {
Some(value) =>
value == "md" ||
value == "markdown" ||
value == "mdx" ||
value == "rst" ||
value == "adoc" ||
value == "txt" ||
value == "pdf" ||
value == "html" ||
value == "htm" ||
value == "epub"
None => false
}
}
///|
fn path_is_config_extension(extension : String?) -> Bool {
match extension {
Some(value) =>
value == "toml" ||
value == "yaml" ||
value == "yml" ||
value == "json" ||
value == "ini" ||
value == "cfg" ||
value == "conf" ||
value == "env" ||
value == "xml"
None => false
}
}
///|
fn path_is_config_name(basename : String) -> Bool {
is_named_document(basename, [
"moon.mod", "moon.pkg", "package.json", "tsconfig.json", "cargo.toml", "go.mod",
])
}
///|
fn path_is_source_extension(extension : String?) -> Bool {
match extension {
Some(value) =>
value == "moon" ||
value == "mbt" ||
value == "rs" ||
value == "go" ||
value == "c" ||
value == "cc" ||
value == "cpp" ||
value == "h" ||
value == "hpp" ||
value == "java" ||
value == "kt" ||
value == "swift" ||
value == "py" ||
value == "rb" ||
value == "js" ||
value == "ts" ||
value == "tsx" ||
value == "jsx" ||
value == "lua" ||
value == "sh" ||
value == "bash" ||
value == "zsh"
None => false
}
}
///|
fn path_is_script_extension(extension : String?) -> Bool {
match extension {
Some(value) =>
value == "sh" ||
value == "bash" ||
value == "zsh" ||
value == "ps1" ||
value == "bat" ||
value == "cmd" ||
value == "py" ||
value == "pl" ||
value == "rb" ||
value == "js" ||
value == "ts"
None => false
}
}
///|
fn path_is_shader_extension(extension : String?) -> Bool {
match extension {
Some(value) =>
value == "glsl" ||
value == "wgsl" ||
value == "vert" ||
value == "frag" ||
value == "comp" ||
value == "spv"
None => false
}
}
///|
fn path_is_image_extension(extension : String?) -> Bool {
match extension {
Some(value) =>
value == "png" ||
value == "jpg" ||
value == "jpeg" ||
value == "gif" ||
value == "webp" ||
value == "bmp" ||
value == "ico" ||
value == "avif" ||
value == "heic" ||
value == "svg" ||
value == "svgz" ||
value == "tif" ||
value == "tiff" ||
value == "psd" ||
value == "xcf"
None => false
}
}
///|
fn path_is_audio_extension(extension : String?) -> Bool {
match extension {
Some(value) =>
value == "mp3" ||
value == "wav" ||
value == "ogg" ||
value == "flac" ||
value == "aac" ||
value == "m4a" ||
value == "opus" ||
value == "mid" ||
value == "midi"
None => false
}
}
///|
fn path_is_video_extension(extension : String?) -> Bool {
match extension {
Some(value) =>
value == "mp4" ||
value == "mov" ||
value == "mkv" ||
value == "webm" ||
value == "avi" ||
value == "m4v" ||
value == "mpg" ||
value == "mpeg" ||
value == "ogv" ||
value == "flv"
None => false
}
}
///|
fn path_is_font_extension(extension : String?) -> Bool {
match extension {
Some(value) =>
value == "ttf" ||
value == "otf" ||
value == "woff" ||
value == "woff2" ||
value == "eot" ||
value == "fon"
None => false
}
}
///|
fn path_is_archive_extension(extension : String?) -> Bool {
match extension {
Some(value) =>
value == "zip" ||
value == "tar" ||
value == "gz" ||
value == "tgz" ||
value == "bz2" ||
value == "xz" ||
value == "7z" ||
value == "rar" ||
value == "zst" ||
value == "cab"
None => false
}
}
///|
fn split_to_array(value : String, delimiter : String) -> Array[String] {
let parts : Array[String] = []
for part in value.split(delimiter) {
parts.push(part.to_owned())
}
parts
}
///|
fn path_without_last_extension(info : PathInfo) -> String {
let out_parts : Array[String] = []
let mut seen = 0
let total = info.segments.length()
for segment in info.segments {
seen = seen + 1
if seen == total {
out_parts.push(info.stem)
} else {
out_parts.push(segment)
}
}
out_parts.join("/")
}