diff --git a/docs/src/modules/calendar.md b/docs/src/modules/calendar.md
new file mode 100644
index 0000000..059a955
--- /dev/null
+++ b/docs/src/modules/calendar.md
@@ -0,0 +1,316 @@
+---
+description: "Calendar and date helpers."
+---
+
+# `calendar`
+
+Calendar and date helpers.
+
+## Usage
+
+```lua
+cal = require "mods.calendar"
+
+print(cal.weekday(2026, 3, 26)) --> 4
+```
+
+## Functions
+
+| Function | Description |
+| ------------------------------------------------------ | --------------------------------- |
+| [`getfirstweekday()`](#fn-getfirstweekday) | Return the default first weekday. |
+| [`setfirstweekday(firstweekday)`](#fn-setfirstweekday) | Set the default first weekday. |
+
+**Calendar Calculations**:
+
+| Function | Description |
+| ------------------------------------------- | ----------------------------------------------------------------------- |
+| [`isleap(year)`](#fn-isleap) | Return `true` for leap years. |
+| [`leapdays(y1, y2)`](#fn-leapdays) | Return the number of leap years from `y1` up to but not including `y2`. |
+| [`monthrange(year, month)`](#fn-monthrange) | Return the first weekday and number of days for a month. |
+| [`weekday(year, month, day)`](#fn-weekday) | Return weekday number where Monday is `1` and Sunday is `7`. |
+
+**Formatting**:
+
+| Function | Description |
+| ----------------------------------------------------- | ------------------------------------------- |
+| [`weekheader(width?, firstweekday?)`](#fn-weekheader) | Return the formatted weekday header string. |
+
+**Iterators**:
+
+| Function | Description |
+| -------------------------------------------------------- | ---------------------------------------------------------------------- |
+| [`monthdays(year, month, firstweekday?)`](#fn-monthdays) | Iterate `(year, month, day, weekday)` tuples for a full calendar grid. |
+| [`weekdays(firstweekday?)`](#fn-weekdays) | Iterate weekday numbers for one full week. |
+
+
+
+### `getfirstweekday()`
+
+Return the default first weekday.
+
+**Return**:
+
+- `firstweekday` (`1|2|3|4|5|6|7`)
+
+**Example**:
+
+```lua
+print(cal.getfirstweekday()) --> 1
+```
+
+> [!NOTE]
+>
+> This returns the same value as `cal.firstweekday`.
+
+
+
+### `setfirstweekday(firstweekday)`
+
+Set the default first weekday.
+
+**Parameters**:
+
+- `firstweekday` (`1|2|3|4|5|6|7`)
+
+**Example**:
+
+```lua
+cal.setfirstweekday(cal.SUNDAY)
+```
+
+> [!NOTE]
+>
+> This updates the same value as `cal.firstweekday = ...`.
+
+### Calendar Calculations
+
+
+
+#### `isleap(year)`
+
+Return `true` for leap years.
+
+**Parameters**:
+
+- `year` (`integer`)
+
+**Return**:
+
+- `isLeap` (`boolean`)
+
+**Example**:
+
+```lua
+print(cal.isleap(2024)) --> true
+```
+
+
+
+#### `leapdays(y1, y2)`
+
+Return the number of leap years from `y1` up to but not including `y2`.
+
+**Parameters**:
+
+- `y1` (`integer`)
+- `y2` (`integer`)
+
+**Return**:
+
+- `count` (`integer`)
+
+**Example**:
+
+```lua
+print(cal.leapdays(2000, 2025)) --> 7
+```
+
+
+
+#### `monthrange(year, month)`
+
+Return the first weekday and number of days for a month.
+
+**Parameters**:
+
+- `year` (`integer`)
+- `month` (`integer`)
+
+**Return**:
+
+- `weekday` (`1|2|3|4|5|6|7`)
+- `ndays` (`integer`)
+
+**Example**:
+
+```lua
+wday, ndays = cal.monthrange(2026, 2)
+print(wday, ndays) --> 7 28
+```
+
+
+
+#### `weekday(year, month, day)`
+
+Return weekday number where Monday is `1` and Sunday is `7`.
+
+**Parameters**:
+
+- `year` (`integer`)
+- `month` (`1|2|3|4|5|6|7|8|9|10|11|12`)
+- `day`
+ (`1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31`)
+
+**Return**:
+
+- `weekday` (`1|2|3|4|5|6|7`)
+
+**Example**:
+
+```lua
+print(cal.weekday(2026, 3, 26)) --> 4
+```
+
+### Formatting
+
+
+
+#### `weekheader(width?, firstweekday?)`
+
+Return the formatted weekday header string.
+
+**Parameters**:
+
+- `width?` (`integer`)
+- `firstweekday?` (`1|2|3|4|5|6|7`)
+
+**Return**:
+
+- `header` (`string`)
+
+**Example**:
+
+```lua
+print(cal.weekheader(1, cal.SUNDAY)) --> "S M T W T F S"
+print(cal.weekheader(2, cal.SUNDAY)) --> "Su Mo Tu We Th Fr Sa"
+print(cal.weekheader(3, cal.SUNDAY)) --> "Sun Mon Tue Wed Thu Fri Sat"
+```
+
+### Iterators
+
+
+
+#### `monthdays(year, month, firstweekday?)`
+
+Iterate `(year, month, day, weekday)` tuples for a full calendar grid.
+
+**Parameters**:
+
+- `year` (`integer`)
+- `month` (`1|2|3|4|5|6|7|8|9|10|11|12`)
+- `firstweekday?` (`1|2|3|4|5|6|7`)
+
+**Return**:
+
+- `iter`
+ (`fun():year:integer,month:modsCalendarMonth,day:modsCalendarMonthday,weekday:modsCalendarWeekday`)
+
+**Example**:
+
+```lua
+local mods = require "mods"
+
+local List = mods.List
+local cal = mods.calendar
+local str = mods.str
+
+local header = cal.weekheader(2)
+local lines = List({
+ str.center(("%s %d"):format(cal.months[cal.FEBRUARY], 2026), #header),
+ header,
+})
+
+local cells = List()
+for _, m, d, _ in cal.monthdays(2026, cal.FEBRUARY) do
+ cells:append(m == cal.FEBRUARY and ("%2d"):format(d) or " ")
+ if #cells == 7 then
+ lines:append(cells:join(" "))
+ cells = List()
+ end
+end
+
+print(lines:join("\n"))
+-- February 2026
+-- Mo Tu We Th Fr Sa Su
+-- 1
+-- 2 3 4 5 6 7 8
+-- 9 10 11 12 13 14 15
+-- 16 17 18 19 20 21 22
+-- 23 24 25 26 27 28
+```
+
+
+
+#### `weekdays(firstweekday?)`
+
+Iterate weekday numbers for one full week.
+
+**Parameters**:
+
+- `firstweekday?` (`1|2|3|4|5|6|7`)
+
+**Return**:
+
+- `iter` (`fun():modsCalendarWeekday`)
+
+**Example**:
+
+```lua
+local weekdays = {}
+for day in cal.weekdays() do
+ weekdays[#weekdays + 1] = day
+end
+print(table.concat(weekdays, ", ")) --> "1, 2, 3, 4, 5, 6, 7"
+```
+
+## Fields
+
+
+
+### `days` (`mods.List`)
+
+Weekday names indexed from `1` (Monday) to `7` (Sunday).
+
+```lua
+print(cal.days[1]) --> Monday
+print(cal.days[7]) --> Sunday
+```
+
+
+
+### `firstweekday` (`1|2|3|4|5|6|7`)
+
+The default first weekday field.
+
+```lua
+print(cal.firstweekday) --> 1
+cal.firstweekday = cal.SUNDAY
+print(cal.firstweekday) --> 7
+```
+
+> [!NOTE]
+>
+> Reading or writing this property is equivalent to calling `getfirstweekday()`
+> or `setfirstweekday()`.
+
+
+
+### `months` (`mods.List`)
+
+Month names indexed from `1` to `12`.
+
+```lua
+print(cal.months[1]) --> January
+print(cal.months[12]) --> December
+```
diff --git a/docs/src/modules/fs.md b/docs/src/modules/fs.md
index 0dabb2e..a646a9b 100644
--- a/docs/src/modules/fs.md
+++ b/docs/src/modules/fs.md
@@ -6,6 +6,11 @@ description: "Filesystem I/O, metadata, and filesystem path operations."
Filesystem I/O, metadata, and filesystem path operations.
+> [!NOTE]
+>
+> This module requires
+> [LuaFileSystem (`lfs`)](https://github.com/lunarmodules/luafilesystem).
+
## Usage
```lua
@@ -18,53 +23,57 @@ print(fs.read_text("tmp/cache/app/data.txt")) --> "hello"
## Functions
-**Reading**:
+**Existence Checks**:
-| Function | Description |
-| ------------------------------------- | -------------------------------------- |
-| [`read_bytes(path)`](#fn-read-bytes) | Read full file in binary mode. |
-| [`read_text(path)`](#fn-read-text) | Read full file in text mode. |
-| [`dir(path, opts?)`](#fn-dir) | Iterator over items in `path`. |
-| [`listdir(path, opts?)`](#fn-listdir) | Return direct children of a directory. |
+| Function | Description |
+| ------------------------------ | ------------------------------------------------------------ |
+| [`exists(path)`](#fn-exists) | Return `true` when a path exists. |
+| [`lexists(path)`](#fn-lexists) | Return `true` when a path exists without following symlinks. |
-**Writing**:
+**Filesystem Mutations**:
| Function | Description |
| -------------------------------------------- | ----------------------------------------------------------------------------- |
-| [`write_bytes(path, data)`](#fn-write-bytes) | Write full file in binary mode. |
-| [`write_text(path, data)`](#fn-write-text) | Write full file in text mode. |
-| [`touch(path)`](#fn-touch) | Create file if missing without truncating, or update timestamps if it exists. |
+| [`cd(path)`](#fn-cd) | Change the current working directory. |
+| [`cp(src, dst)`](#fn-cp) | Copy a file or directory tree. |
+| [`cwd()`](#fn-cwd) | Return the current working directory. |
+| [`link(path, linkpath)`](#fn-link) | Create a hard link. |
+| [`mkdir(path, parents?)`](#fn-mkdir) | Create a directory. |
| [`rename(oldname, newname)`](#fn-rename) | Rename or move a filesystem entry. |
| [`rm(path, recursive?)`](#fn-rm) | Remove a filesystem entry, or a directory tree when `recursive` is `true`. |
-| [`mkdir(path, parents?)`](#fn-mkdir) | Create a directory. |
-| [`cp(src, dst)`](#fn-cp) | Copy a file or directory tree. |
+| [`symlink(path, linkpath)`](#fn-symlink) | Create a symbolic link. |
+| [`touch(path)`](#fn-touch) | Create file if missing without truncating, or update timestamps if it exists. |
+| [`write_bytes(path, data)`](#fn-write-bytes) | Write full file in binary mode. |
+| [`write_text(path, data)`](#fn-write-text) | Write full file in text mode. |
**Metadata**:
-| Function | Description |
-| ------------------------------------------ | ---------------------------------------------------------------------------------- |
-| [`getsize(path)`](#fn-getsize) | Return file size in bytes. |
-| [`getatime(path)`](#fn-getatime) | Return last access time. |
-| [`getmtime(path)`](#fn-getmtime) | Return last modification time. |
-| [`getctime(path)`](#fn-getctime) | Return metadata change time. |
-| [`lstat(path)`](#fn-lstat) | Return symlink-aware file attributes. |
-| [`stat(path)`](#fn-stat) | Return file attributes. |
-| [`samefile(path_a, path_b)`](#fn-samefile) | Return whether two paths refer to the same file, or `nil` and an error on failure. |
+| Function | Description |
+| -------------------------------- | ---------------------------------------------------------------------------------- |
+| [`getatime(path)`](#fn-getatime) | Return last access time. |
+| [`getctime(path)`](#fn-getctime) | Return metadata change time. |
+| [`getmtime(path)`](#fn-getmtime) | Return last modification time. |
+| [`getsize(path)`](#fn-getsize) | Return file size in bytes. |
+| [`lstat(path)`](#fn-lstat) | Return symlink-aware file attributes. |
+| [`samefile(a, b)`](#fn-samefile) | Return whether two paths refer to the same file, or `nil` and an error on failure. |
+| [`stat(path)`](#fn-stat) | Return file attributes. |
-**Existence Checks**:
+**Reading**:
-| Function | Description |
-| ------------------------------ | ------------------------------------------------------------ |
-| [`exists(path)`](#fn-exists) | Return `true` when a path exists. |
-| [`lexists(path)`](#fn-lexists) | Return `true` when a path exists without following symlinks. |
+| Function | Description |
+| ------------------------------------- | -------------------------------------- |
+| [`dir(path, opts?)`](#fn-dir) | Iterator over items in `path`. |
+| [`listdir(path, opts?)`](#fn-listdir) | Return direct children of a directory. |
+| [`read_bytes(path)`](#fn-read-bytes) | Read full file in binary mode. |
+| [`read_text(path)`](#fn-read-text) | Read full file in text mode. |
-### Reading
+### Existence Checks
-
+
-#### `read_bytes(path)`
+#### `exists(path)`
-Read full file in binary mode.
+Return `true` when a path exists.
**Parameters**:
@@ -72,21 +81,23 @@ Read full file in binary mode.
**Return**:
-- `body` (`string?`): File contents read in binary mode, or `nil` on failure.
-- `errmsg` (`string?`): Error message when the check fails.
-- `errcode` (`integer?`): OS error code when available.
+- `exists` (`boolean`): True when the path exists.
**Example**:
```lua
-fs.read_bytes("README.md")
+fs.exists("README.md") --> true
```
-
+> [!NOTE]
+>
+> Broken symlinks return `false`.
-#### `read_text(path)`
+
-Read full file in text mode.
+#### `lexists(path)`
+
+Return `true` when a path exists without following symlinks.
**Parameters**:
@@ -94,130 +105,121 @@ Read full file in text mode.
**Return**:
-- `body` (`string?`): File contents read in text mode, or `nil` on failure.
-- `errmsg` (`string?`): Error message when the check fails.
-- `errcode` (`integer?`): OS error code when available.
+- `exists` (`boolean`): True when the path or symlink entry exists.
**Example**:
```lua
-fs.read_text("README.md")
+fs.lexists("README.md") --> true
```
-
+> [!NOTE]
+>
+> Broken symlinks return `true`.
-#### `dir(path, opts?)`
+### Filesystem Mutations
-Iterator over items in `path`.
+
+
+#### `cd(path)`
+
+Change the current working directory.
**Parameters**:
-- `path` (`string`): Input path.
-- `opts?`
- (`{hidden?:boolean, recursive?:boolean, follow_links?:boolean, type?:string}`):
- Optional traversal options.
+- `path` (`string`): Directory path to switch into.
**Return**:
-- `iterator`
- (`(fun(state:table, prev?:string):basename:string?, type:"file"|"directory"|"link"|"fifo"|"socket"|"char"|"block"|"unknown"?)?`):
- Iterator, or `nil` on failure.
-- `state` (`table|string`): Iterator state on success, or error message on
+- `changed` (`true?`): `true` when the directory change succeeds, or `nil` on
failure.
+- `errmsg` (`string?`): Error message when the change fails.
**Example**:
```lua
-for name, type in fs.dir(path.cwd(), { recursive = true }) do
- print(name, type)
-end
+fs.cd("src")
```
-
+
-#### `listdir(path, opts?)`
+#### `cp(src, dst)`
-Return direct children of a directory.
+Copy a file or directory tree.
**Parameters**:
-- `path` (`string`): Input path.
-- `opts?`
- (`{hidden?:boolean, recursive?:boolean, follow_links?:boolean, type?:string}`):
- Optional traversal options.
+- `src` (`string`): Source path.
+- `dst` (`string`): Destination path.
**Return**:
-- `paths` (`mods.List?`): Direct child paths.
-- `err` (`string?`): Error message when traversal setup fails.
+- `copied` (`true?`): `true` when copying succeeds, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
**Example**:
```lua
-fs.listdir("src")
+fs.cp("a.txt", "b.txt")
+fs.cp("src", "backup/src")
```
-### Writing
-
-
-
-#### `write_bytes(path, data)`
+
-Write full file in binary mode.
+#### `cwd()`
-**Parameters**:
-
-- `path` (`string`): Input path.
-- `data` (`string`): Input data.
+Return the current working directory.
**Return**:
-- `written` (`true?`): `true` when writing succeeds, or `nil` on failure.
-- `errmsg` (`string?`): Error message when the check fails.
+- `cwd` (`string?`): Current working directory, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the lookup fails.
- `errcode` (`integer?`): OS error code when available.
**Example**:
```lua
-fs.write_bytes("tmp.bin", "abc") --> true, nil
+fs.cwd()
```
-
+
-#### `write_text(path, data)`
+#### `link(path, linkpath)`
-Write full file in text mode.
+Create a hard link.
**Parameters**:
-- `path` (`string`): Input path.
-- `data` (`string`): Input data.
+- `path` (`string`): Existing path to link to.
+- `linkpath` (`string`): New link path to create.
**Return**:
-- `written` (`true?`): `true` when writing succeeds, or `nil` on failure.
+- `linked` (`true?`): `true` when link creation succeeds, or `nil` on failure.
- `errmsg` (`string?`): Error message when the check fails.
- `errcode` (`integer?`): OS error code when available.
**Example**:
```lua
-fs.write_text("tmp.txt", "abc") --> true, nil
+fs.link("target.txt", "hardlink.txt")
```
-
+
-#### `touch(path)`
+#### `mkdir(path, parents?)`
-Create file if missing without truncating, or update timestamps if it exists.
+Create a directory.
**Parameters**:
- `path` (`string`): Input path.
+- `parents?` (`boolean`): Create missing parent directories when `true`.
**Return**:
-- `touched` (`true?`): `true` when the file exists after touch, or `nil` on
+- `created` (`true?`): `true` when directory creation succeeds, or `nil` on
failure.
- `errmsg` (`string?`): Error message when the check fails.
- `errcode` (`integer?`): OS error code when available.
@@ -225,7 +227,7 @@ Create file if missing without truncating, or update timestamps if it exists.
**Example**:
```lua
-fs.touch("tmp.txt") --> true, nil
+fs.mkdir("tmp/a/b", true)
```
@@ -279,20 +281,42 @@ fs.rm("tmp.txt") --> true, nil
fs.rm("tmp/cache", true) --> true, nil
```
-
+
-#### `mkdir(path, parents?)`
+#### `symlink(path, linkpath)`
-Create a directory.
+Create a symbolic link.
+
+**Parameters**:
+
+- `path` (`string`): Path to reference from the new symlink.
+- `linkpath` (`string`): New symlink path to create.
+
+**Return**:
+
+- `linked` (`true?`): `true` when link creation succeeds, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.symlink("target.txt", "symlink.txt")
+```
+
+
+
+#### `touch(path)`
+
+Create file if missing without truncating, or update timestamps if it exists.
**Parameters**:
- `path` (`string`): Input path.
-- `parents?` (`boolean`): Create missing parent directories when `true`.
**Return**:
-- `created` (`true?`): `true` when directory creation succeeds, or `nil` on
+- `touched` (`true?`): `true` when the file exists after touch, or `nil` on
failure.
- `errmsg` (`string?`): Error message when the check fails.
- `errcode` (`integer?`): OS error code when available.
@@ -300,57 +324,57 @@ Create a directory.
**Example**:
```lua
-fs.mkdir("tmp/a/b", true)
+fs.touch("tmp.txt") --> true, nil
```
-
+
-#### `cp(src, dst)`
+#### `write_bytes(path, data)`
-Copy a file or directory tree.
+Write full file in binary mode.
**Parameters**:
-- `src` (`string`): Source path.
-- `dst` (`string`): Destination path.
+- `path` (`string`): Input path.
+- `data` (`string`): Input data.
**Return**:
-- `copied` (`true?`): `true` when copying succeeds, or `nil` on failure.
+- `written` (`true?`): `true` when writing succeeds, or `nil` on failure.
- `errmsg` (`string?`): Error message when the check fails.
- `errcode` (`integer?`): OS error code when available.
**Example**:
```lua
-fs.cp("a.txt", "b.txt")
-fs.cp("src", "backup/src")
+fs.write_bytes("tmp.bin", "abc") --> true, nil
```
-### Metadata
-
-
+
-#### `getsize(path)`
+#### `write_text(path, data)`
-Return file size in bytes.
+Write full file in text mode.
**Parameters**:
- `path` (`string`): Input path.
+- `data` (`string`): Input data.
**Return**:
-- `size` (`integer?`): File size in bytes.
+- `written` (`true?`): `true` when writing succeeds, or `nil` on failure.
- `errmsg` (`string?`): Error message when the check fails.
- `errcode` (`integer?`): OS error code when available.
**Example**:
```lua
-fs.getsize("README.md") --> 1234
+fs.write_text("tmp.txt", "abc") --> true, nil
```
+### Metadata
+
#### `getatime(path)`
@@ -373,6 +397,28 @@ Return last access time.
fs.getatime("README.md") --> 1712345678
```
+
+
+#### `getctime(path)`
+
+Return metadata change time.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `timestamp` (`number?`): Change time (seconds since epoch).
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.getctime("README.md") --> 1712345678
+```
+
#### `getmtime(path)`
@@ -395,11 +441,11 @@ Return last modification time.
fs.getmtime("README.md") --> 1712345678
```
-
+
-#### `getctime(path)`
+#### `getsize(path)`
-Return metadata change time.
+Return file size in bytes.
**Parameters**:
@@ -407,14 +453,14 @@ Return metadata change time.
**Return**:
-- `timestamp` (`number?`): Change time (seconds since epoch).
+- `size` (`integer?`): File size in bytes.
- `errmsg` (`string?`): Error message when the check fails.
- `errcode` (`integer?`): OS error code when available.
**Example**:
```lua
-fs.getctime("README.md") --> 1712345678
+fs.getsize("README.md") --> 1234
```
@@ -440,6 +486,30 @@ Return symlink-aware file attributes.
fs.lstat("README.md")
```
+
+
+#### `samefile(a, b)`
+
+Return whether two paths refer to the same file, or `nil` and an error on
+failure.
+
+**Parameters**:
+
+- `a` (`string`): Input path.
+- `b` (`string`): Input path.
+
+**Return**:
+
+- `isSameFile` (`boolean?`): True when both paths refer to the same file.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.samefile("README.md", "README.md") --> true
+```
+
#### `stat(path)`
@@ -464,61 +534,85 @@ Return file attributes.
fs.stat("README.md")
```
-
+### Reading
+
+
-#### `samefile(path_a, path_b)`
+#### `dir(path, opts?)`
-Return whether two paths refer to the same file, or `nil` and an error on
-failure.
+Iterator over items in `path`.
+
+**Options**:
+
+- `recursive`: recurse into subdirectories; defaults to `false`.
+- `hidden`: include hidden entries; defaults to `true`.
+- `follow`: recurse into symlinked directories; defaults to `false`.
+- `type`: filter by entry type, such as `"file"` or `"directory"`; defaults to
+ `nil`.
**Parameters**:
-- `path_a` (`string`): Input path.
-- `path_b` (`string`): Input path.
+- `path` (`string`): Input path.
+- `opts?`
+ (`{hidden?:boolean, recursive?:boolean, follow?:boolean, type?:modsFsEntryType}`):
+ Optional traversal options.
**Return**:
-- `isSameFile` (`boolean?`): True when both paths refer to the same file.
-- `errmsg` (`string?`): Error message when the check fails.
-- `errcode` (`integer?`): OS error code when available.
+- `iterator`
+ (`(fun(state:table, prev?:string):basename:string?, type:modsFsEntryType?)?`):
+ Iterator, or `nil` on failure.
+- `state` (`table|string`): Iterator state on success, or error message on
+ failure.
**Example**:
```lua
-fs.samefile("README.md", "README.md") --> true
+for name, type in fs.dir(path.cwd(), { recursive = true }) do
+ print(name, type)
+end
```
-### Existence Checks
+
-
+#### `listdir(path, opts?)`
-#### `exists(path)`
+Return direct children of a directory.
-Return `true` when a path exists.
+**Options**:
+
+- `recursive`: recurse into subdirectories; defaults to `false`.
+- `hidden`: include hidden entries; defaults to `true`.
+- `follow`: recurse into symlinked directories; defaults to `false`.
+- `type`: filter by entry type, such as `"file"` or `"directory"`; defaults to
+ `nil`.
+- `names`: return basenames; defaults to `false`.
**Parameters**:
- `path` (`string`): Input path.
+- `opts?`
+ (`{hidden?:boolean, recursive?:boolean, follow?:boolean, type?:modsFsEntryType, names?:boolean}`):
+ Optional traversal options.
**Return**:
-- `exists` (`boolean`): True when the path exists.
+- `paths` (`mods.List?`): Direct child paths, or basenames when
+ `opts.names` is `true`.
+- `err` (`string?`): Error message when traversal setup fails.
**Example**:
```lua
-fs.exists("README.md") --> true
+fs.listdir("src")
+fs.listdir("src", { names = true })
```
-> [!NOTE]
->
-> Broken symlinks return `false`.
-
-
+
-#### `lexists(path)`
+#### `read_bytes(path)`
-Return `true` when a path exists without following symlinks.
+Read full file in binary mode.
**Parameters**:
@@ -526,14 +620,34 @@ Return `true` when a path exists without following symlinks.
**Return**:
-- `exists` (`boolean`): True when the path or symlink entry exists.
+- `body` (`string?`): File contents read in binary mode, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
**Example**:
```lua
-fs.lexists("README.md") --> true
+fs.read_bytes("README.md")
```
-> [!NOTE]
->
-> Broken symlinks return `true`.
+
+
+#### `read_text(path)`
+
+Read full file in text mode.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `body` (`string?`): File contents read in text mode, or `nil` on failure.
+- `errmsg` (`string?`): Error message when the check fails.
+- `errcode` (`integer?`): OS error code when available.
+
+**Example**:
+
+```lua
+fs.read_text("README.md")
+```
diff --git a/docs/src/modules/glob.md b/docs/src/modules/glob.md
new file mode 100644
index 0000000..045a163
--- /dev/null
+++ b/docs/src/modules/glob.md
@@ -0,0 +1,267 @@
+---
+description: "Glob-style matching and filesystem expansion helpers."
+---
+
+# `glob`
+
+Glob-style matching and filesystem expansion helpers.
+
+## Usage
+
+```lua
+glob = require "mods.glob"
+
+print(glob.match("src/mods/fs.lua", "**/*.lua")) --> true
+print(glob.match("DATA.TXT", "*.txt", true)) --> true
+print(glob.filter({ "a.lua", "b.txt" }, "*.lua")[1]) --> "a.lua"
+print(glob.glob("src", "*.lua")[1])
+```
+
+## Supported wildcards
+
+- `*`: match zero or more characters within one path segment.
+
+ ```lua
+ match("main.lua", "*.lua")
+ ```
+
+- `?`: match exactly one character within one path segment.
+
+ ```lua
+ match("a1.lua", "a?.lua")
+ ```
+
+- `[]`: match one character from a bracket class like `[a-z]`.
+
+ ```lua
+ match("file7.lua", "file[0-9].lua")
+ ```
+
+- `[!]`: negate a bracket class, like `[!0-9]`.
+
+ ```lua
+ match("filex.lua", "file[!0-9].lua")
+ ```
+
+- `{a,b}`: match one of several brace alternatives.
+
+ ```lua
+ match("init.lua", "init.{lua,luac}")
+ ```
+
+- `**`: match across path segments recursively.
+
+ ```lua
+ match("src/mods/fs.lua", "**/*.lua")
+ ```
+
+## Functions
+
+**Glob Operations**:
+
+| Function | Description |
+| --------------------------------------------------- | ----------------------------------------------------------- |
+| [`escape(s)`](#fn-escape) | Escape glob metacharacters in a literal string. |
+| [`filter(names, pattern, ignorecase?)`](#fn-filter) | Return the values from `names` that match the glob pattern. |
+| [`glob(path, pattern?, opts?)`](#fn-glob) | Return glob matches under `path`. |
+| [`has_magic(s)`](#fn-has-magic) | Return whether a pattern contains glob metacharacters. |
+| [`iglob(path, pattern?, opts?)`](#fn-iglob) | Iterator over glob matches under `path`. |
+| [`match(path, pattern, ignorecase?)`](#fn-match) | Match a path against a glob pattern. |
+| [`translate(pattern)`](#fn-translate) | Translate one glob segment into an equivalent Lua pattern. |
+
+### Glob Operations
+
+
+
+#### `escape(s)`
+
+Escape glob metacharacters in a literal string.
+
+**Parameters**:
+
+- `s` (`string`): Input literal string.
+
+**Return**:
+
+- `pattern` (`string`): Escaped glob pattern.
+
+**Example**:
+
+```lua
+glob.escape("a*b") --> "a\\*b"
+```
+
+
+
+#### `filter(names, pattern, ignorecase?)`
+
+Return the values from `names` that match the glob pattern.
+
+**Parameters**:
+
+- `names` (`string[]`): Input names.
+- `pattern` (`string`): Input glob pattern.
+- `ignorecase?` (`boolean`): Override platform-default case matching.
+
+**Return**:
+
+- `matches` (`mods.List`): Matching values from `names`.
+
+**Example**:
+
+```lua
+glob.filter({ "a.lua", "b.txt", "c.lua" }, "*.lua") --> { "a.lua", "c.lua" }
+```
+
+
+
+#### `glob(path, pattern?, opts?)`
+
+Return glob matches under `path`.
+
+**Options**:
+
+- `hidden`: include hidden paths; defaults to `true`.
+- `recursive`: recurse into subdirectories; defaults to `false`.
+- `follow`: recurse into symlinked directories; defaults to `false`.
+- `ignorecase`: use case-insensitive matching; defaults to platform semantics.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `pattern?` (`string`): Optional pattern to match.
+- `opts?`
+ (`{hidden?:boolean, recursive?:boolean, follow?:boolean, ignorecase?:boolean}`):
+ Optional glob options.
+
+**Return**:
+
+- `paths` (`mods.List`): Matching paths under `path`.
+
+**Example**:
+
+```lua
+glob.glob("src", "*.lua")
+glob.glob("src", "*.lua", { recursive = true })
+```
+
+
+
+#### `has_magic(s)`
+
+Return whether a pattern contains glob metacharacters.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `has_magic` (`boolean`): True when the string contains glob syntax.
+
+**Example**:
+
+```lua
+glob.has_magic("foo.txt") --> false
+glob.has_magic("*.txt") --> true
+```
+
+
+
+#### `iglob(path, pattern?, opts?)`
+
+Iterator over glob matches under `path`.
+
+**Options**:
+
+- `hidden`: include hidden paths; defaults to `true`.
+- `recursive`: recurse into subdirectories; defaults to `false`.
+- `follow`: recurse into symlinked directories; defaults to `false`.
+- `ignorecase`: use case-insensitive matching; defaults to platform semantics.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `pattern?` (`string`): Optional pattern to match.
+- `opts?`
+ (`{hidden?:boolean, recursive?:boolean, follow?:boolean, ignorecase?:boolean}`):
+ Optional glob options.
+
+**Return**:
+
+- `iterator` (`(fun(state:table, prev?:string): (path:string?))`): Iterator
+ function.
+- `state` (`table`): Iterator state table.
+- `initial` (`nil`): Initial iterator value.
+
+**Example**:
+
+```lua
+for path in glob.iglob("src", "*.lua") do
+ print(path)
+end
+```
+
+
+
+#### `match(path, pattern, ignorecase?)`
+
+Match a path against a glob pattern.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `pattern` (`string`): Input glob pattern.
+- `ignorecase?` (`boolean`): Override platform-default case matching.
+
+**Return**:
+
+- `matches` (`boolean`): True when the path matches the pattern.
+
+**Example**:
+
+```lua
+glob.match("src/mods/fs.lua", "**/*.lua") --> true
+```
+
+
+
+#### `translate(pattern)`
+
+Translate one glob segment into an equivalent Lua pattern.
+
+**Parameters**:
+
+- `pattern` (`string`): Input glob segment.
+
+**Return**:
+
+- `lua_pattern` (`string`): Lua pattern string.
+
+**Example**:
+
+```lua
+local s = "init.lua"
+local pattern = "*.lua"
+local matches = glob.match(s, pattern)
+local translated_matches = s:match(glob.translate(pattern)) ~= nil
+print(matches == translated_matches) --> true
+```
+
+> [!NOTE]
+>
+> - `*` and `?` stay within a single path segment.
+>
+> ```lua
+> local pattern = "*.txt"
+> print(glob.translate(pattern)) --> "^[^/]*%.txt$"
+> print(glob.match("foo/bar.txt", pattern)) --> false
+> ```
+>
+> - `**` and `{a,b}` need higher-level matching logic.
+>
+> ```lua
+> pattern = "src/{x,y}.lua"
+> print(("src/x.lua"):match(glob.translate(pattern))) --> nil
+> print(glob.match("src/x.lua", pattern)) --> true
+> ```
diff --git a/docs/src/modules/is.md b/docs/src/modules/is.md
index 7c89073..39ab354 100644
--- a/docs/src/modules/is.md
+++ b/docs/src/modules/is.md
@@ -26,6 +26,12 @@ ok = is.table({}) --> true
> is.tAbLe({}) --> true
> ```
+> [!IMPORTANT]
+>
+> Path checks require **LuaFileSystem**
+> ([`lfs`](https://github.com/lunarmodules/luafilesystem)) and raise an error if
+> it is not installed.
+
## `is()`
`is` is also callable as `is(value, type)` to check if a value is of a given
@@ -39,6 +45,20 @@ is("hello", "STRING") --> true
## Functions
+**Path Checks**:
+
+| Function | Description |
+| ------------------------- | ------------------------------------------------------------ |
+| [`block(v)`](#fn-block) | Returns `true` when `v` is a block device path. |
+| [`char(v)`](#fn-char) | Returns `true` when `v` is a character device path. |
+| [`device(v)`](#fn-device) | Returns `true` when `v` is a block or character device path. |
+| [`dir(v)`](#fn-dir) | Returns `true` when `v` is a directory path. |
+| [`fifo(v)`](#fn-fifo) | Returns `true` when `v` is a FIFO path. |
+| [`file(v)`](#fn-file) | Returns `true` when `v` is a file path. |
+| [`link(v)`](#fn-link) | Returns `true` when `v` is a symlink path. |
+| [`path(v)`](#fn-path) | Returns `true` when `v` is a valid filesystem path. |
+| [`socket(v)`](#fn-socket) | Returns `true` when `v` is a socket path. |
+
**Type Checks**:
| Function | Description |
@@ -56,34 +76,20 @@ is("hello", "STRING") --> true
| Function | Description |
| ----------------------------- | ------------------------------------------- |
+| [`callable(v)`](#fn-callable) | Returns `true` when `v` is callable. |
| [`false(v)`](#fn-false) | Returns `true` when `v` is exactly `false`. |
-| [`true(v)`](#fn-true) | Returns `true` when `v` is exactly `true`. |
| [`falsy(v)`](#fn-falsy) | Returns `true` when `v` is falsy. |
-| [`callable(v)`](#fn-callable) | Returns `true` when `v` is callable. |
| [`integer(v)`](#fn-integer) | Returns `true` when `v` is an integer. |
+| [`true(v)`](#fn-true) | Returns `true` when `v` is exactly `true`. |
| [`truthy(v)`](#fn-truthy) | Returns `true` when `v` is truthy. |
-**Path Checks**:
-
-| Function | Description |
-| ------------------------- | ------------------------------------------------------------ |
-| [`path(v)`](#fn-path) | Returns `true` when `v` is a valid filesystem path. |
-| [`block(v)`](#fn-block) | Returns `true` when `v` is a block device path. |
-| [`char(v)`](#fn-char) | Returns `true` when `v` is a character device path. |
-| [`device(v)`](#fn-device) | Returns `true` when `v` is a block or character device path. |
-| [`dir(v)`](#fn-dir) | Returns `true` when `v` is a directory path. |
-| [`fifo(v)`](#fn-fifo) | Returns `true` when `v` is a FIFO path. |
-| [`file(v)`](#fn-file) | Returns `true` when `v` is a file path. |
-| [`link(v)`](#fn-link) | Returns `true` when `v` is a symlink path. |
-| [`socket(v)`](#fn-socket) | Returns `true` when `v` is a socket path. |
-
-### Type Checks
+### Path Checks
-Core Lua type checks (`type(v)` family).
+
-#### `boolean(v)`
+#### `block(v)`
-Returns `true` when `v` is a boolean.
+Returns `true` when `v` is a block device path.
**Parameters**:
@@ -91,19 +97,19 @@ Returns `true` when `v` is a boolean.
**Return**:
-- `isBoolean` (`boolean`): Whether the check succeeds.
+- `isBlock` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.boolean(true)
+is.block("/dev/sda")
```
-
+
-#### `function(v)`
+#### `char(v)`
-Returns `true` when `v` is a function.
+Returns `true` when `v` is a character device path.
**Parameters**:
@@ -111,19 +117,19 @@ Returns `true` when `v` is a function.
**Return**:
-- `isFunction` (`boolean`): Whether the check succeeds.
+- `isChar` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.Function(function() end)
+is.char("/dev/null")
```
-
+
-#### `nil(v)`
+#### `device(v)`
-Returns `true` when `v` is `nil`.
+Returns `true` when `v` is a block or character device path.
**Parameters**:
@@ -131,19 +137,19 @@ Returns `true` when `v` is `nil`.
**Return**:
-- `isNil` (`boolean`): Whether the check succeeds.
+- `isDevice` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.Nil(nil)
+is.device("/dev/null")
```
-
+
-#### `number(v)`
+#### `dir(v)`
-Returns `true` when `v` is a number.
+Returns `true` when `v` is a directory path.
**Parameters**:
@@ -151,19 +157,19 @@ Returns `true` when `v` is a number.
**Return**:
-- `isNumber` (`boolean`): Whether the check succeeds.
+- `isDir` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.number(3.14)
+is.dir("/tmp")
```
-
+
-#### `string(v)`
+#### `fifo(v)`
-Returns `true` when `v` is a string.
+Returns `true` when `v` is a FIFO path.
**Parameters**:
@@ -171,19 +177,19 @@ Returns `true` when `v` is a string.
**Return**:
-- `isString` (`boolean`): Whether the check succeeds.
+- `isFifo` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.string("hello")
+is.fifo("/path/to/fifo")
```
-
+
-#### `table(v)`
+#### `file(v)`
-Returns `true` when `v` is a table.
+Returns `true` when `v` is a file path.
**Parameters**:
@@ -191,19 +197,19 @@ Returns `true` when `v` is a table.
**Return**:
-- `isTable` (`boolean`): Whether the check succeeds.
+- `isFile` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.table({})
+is.file("README.md")
```
-
+
-#### `thread(v)`
+#### `link(v)`
-Returns `true` when `v` is a thread.
+Returns `true` when `v` is a symlink path.
**Parameters**:
@@ -211,19 +217,19 @@ Returns `true` when `v` is a thread.
**Return**:
-- `isThread` (`boolean`): Whether the check succeeds.
+- `isLink` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.thread(coroutine.create(function() end))
+is.link("/path/to/link")
```
-
+
-#### `userdata(v)`
+#### `path(v)`
-Returns `true` when `v` is userdata.
+Returns `true` when `v` is a valid filesystem path.
**Parameters**:
@@ -231,21 +237,23 @@ Returns `true` when `v` is userdata.
**Return**:
-- `isUserdata` (`boolean`): Whether the check succeeds.
+- `isPath` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.userdata(io.stdout)
+is.path("README.md")
```
-### Value Checks
+> [!NOTE]
+>
+> Returns `true` for broken symlinks.
-Truthiness, exact-value, and callable checks.
+
-#### `false(v)`
+#### `socket(v)`
-Returns `true` when `v` is exactly `false`.
+Returns `true` when `v` is a socket path.
**Parameters**:
@@ -253,19 +261,21 @@ Returns `true` when `v` is exactly `false`.
**Return**:
-- `isFalse` (`boolean`): Whether the check succeeds.
+- `isSocket` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.False(false)
+is.socket("/path/to/socket")
```
-
+### Type Checks
-#### `true(v)`
+
-Returns `true` when `v` is exactly `true`.
+#### `boolean(v)`
+
+Returns `true` when `v` is a boolean.
**Parameters**:
@@ -273,19 +283,19 @@ Returns `true` when `v` is exactly `true`.
**Return**:
-- `isTrue` (`boolean`): Whether the check succeeds.
+- `isBoolean` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.True(true)
+is.boolean(true)
```
-
+
-#### `falsy(v)`
+#### `function(v)`
-Returns `true` when `v` is falsy.
+Returns `true` when `v` is a function.
**Parameters**:
@@ -293,19 +303,19 @@ Returns `true` when `v` is falsy.
**Return**:
-- `isFalsy` (`boolean`): Whether the check succeeds.
+- `isFunction` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.falsy(false)
+is.Function(function() end)
```
-
+
-#### `callable(v)`
+#### `nil(v)`
-Returns `true` when `v` is callable.
+Returns `true` when `v` is `nil`.
**Parameters**:
@@ -313,19 +323,19 @@ Returns `true` when `v` is callable.
**Return**:
-- `isCallable` (`boolean`): Whether the check succeeds.
+- `isNil` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.callable(function() end)
+is.Nil(nil)
```
-
+
-#### `integer(v)`
+#### `number(v)`
-Returns `true` when `v` is an integer.
+Returns `true` when `v` is a number.
**Parameters**:
@@ -333,19 +343,19 @@ Returns `true` when `v` is an integer.
**Return**:
-- `isInteger` (`boolean`): Whether the check succeeds.
+- `isNumber` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.integer(42)
+is.number(3.14)
```
-
+
-#### `truthy(v)`
+#### `string(v)`
-Returns `true` when `v` is truthy.
+Returns `true` when `v` is a string.
**Parameters**:
@@ -353,27 +363,19 @@ Returns `true` when `v` is truthy.
**Return**:
-- `isTruthy` (`boolean`): Whether the check succeeds.
+- `isString` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.truthy("non-empty")
+is.string("hello")
```
-### Path Checks
-
-Filesystem path type checks.
-
-> [!IMPORTANT]
->
-> Path checks require **LuaFileSystem**
-> ([`lfs`](https://github.com/lunarmodules/luafilesystem)) and raise an error if
-> it is not installed.
+
-#### `path(v)`
+#### `table(v)`
-Returns `true` when `v` is a valid filesystem path.
+Returns `true` when `v` is a table.
**Parameters**:
@@ -381,21 +383,19 @@ Returns `true` when `v` is a valid filesystem path.
**Return**:
-- `isPath` (`boolean`): Whether the check succeeds.
+- `isTable` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.path("README.md")
+is.table({})
```
-> [!NOTE] Returns `true` for broken symlinks.
-
-
+
-#### `block(v)`
+#### `thread(v)`
-Returns `true` when `v` is a block device path.
+Returns `true` when `v` is a thread.
**Parameters**:
@@ -403,19 +403,19 @@ Returns `true` when `v` is a block device path.
**Return**:
-- `isBlock` (`boolean`): Whether the check succeeds.
+- `isThread` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.block("/dev/sda")
+is.thread(coroutine.create(function() end))
```
-
+
-#### `char(v)`
+#### `userdata(v)`
-Returns `true` when `v` is a character device path.
+Returns `true` when `v` is userdata.
**Parameters**:
@@ -423,19 +423,21 @@ Returns `true` when `v` is a character device path.
**Return**:
-- `isChar` (`boolean`): Whether the check succeeds.
+- `isUserdata` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.char("/dev/null")
+is.userdata(io.stdout)
```
-
+### Value Checks
-#### `device(v)`
+
-Returns `true` when `v` is a block or character device path.
+#### `callable(v)`
+
+Returns `true` when `v` is callable.
**Parameters**:
@@ -443,19 +445,19 @@ Returns `true` when `v` is a block or character device path.
**Return**:
-- `isDevice` (`boolean`): Whether the check succeeds.
+- `isCallable` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.device("/dev/null")
+is.callable(function() end)
```
-
+
-#### `dir(v)`
+#### `false(v)`
-Returns `true` when `v` is a directory path.
+Returns `true` when `v` is exactly `false`.
**Parameters**:
@@ -463,19 +465,19 @@ Returns `true` when `v` is a directory path.
**Return**:
-- `isDir` (`boolean`): Whether the check succeeds.
+- `isFalse` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.dir("/tmp")
+is.False(false)
```
-
+
-#### `fifo(v)`
+#### `falsy(v)`
-Returns `true` when `v` is a FIFO path.
+Returns `true` when `v` is falsy.
**Parameters**:
@@ -483,19 +485,19 @@ Returns `true` when `v` is a FIFO path.
**Return**:
-- `isFifo` (`boolean`): Whether the check succeeds.
+- `isFalsy` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.fifo("/path/to/fifo")
+is.falsy(false)
```
-
+
-#### `file(v)`
+#### `integer(v)`
-Returns `true` when `v` is a file path.
+Returns `true` when `v` is an integer.
**Parameters**:
@@ -503,19 +505,19 @@ Returns `true` when `v` is a file path.
**Return**:
-- `isFile` (`boolean`): Whether the check succeeds.
+- `isInteger` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.file("README.md")
+is.integer(42)
```
-
+
-#### `link(v)`
+#### `true(v)`
-Returns `true` when `v` is a symlink path.
+Returns `true` when `v` is exactly `true`.
**Parameters**:
@@ -523,19 +525,19 @@ Returns `true` when `v` is a symlink path.
**Return**:
-- `isLink` (`boolean`): Whether the check succeeds.
+- `isTrue` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.link("/path/to/link")
+is.True(true)
```
-
+
-#### `socket(v)`
+#### `truthy(v)`
-Returns `true` when `v` is a socket path.
+Returns `true` when `v` is truthy.
**Parameters**:
@@ -543,10 +545,10 @@ Returns `true` when `v` is a socket path.
**Return**:
-- `isSocket` (`boolean`): Whether the check succeeds.
+- `isTruthy` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-is.socket("/path/to/socket")
+is.truthy("non-empty")
```
diff --git a/docs/src/modules/json.md b/docs/src/modules/json.md
new file mode 100644
index 0000000..f123b42
--- /dev/null
+++ b/docs/src/modules/json.md
@@ -0,0 +1,165 @@
+---
+description: "JSON encoding and decoding helpers."
+---
+
+# `json`
+
+JSON encoding and decoding helpers.
+
+> [!NOTE]
+>
+> This module aims to implement strict
+> [RFC 8259](https://www.rfc-editor.org/rfc/rfc8259) JSON behavior.
+
+## Usage
+
+```lua
+local json = require "mods.json"
+
+local encoded = json.encode({ ok = true, value = 42 })
+local decoded = json.decode(encoded)
+
+print(encoded) --> {"ok":true,"value":42}
+print(decoded.ok) --> true
+print(decoded.value) --> 42
+```
+
+## Behavior
+
+- booleans, strings, and finite numbers map directly to JSON values
+
+```lua
+print(json.encode(true)) --> true
+print(json.encode("hi")) --> "hi"
+print(json.encode(12.5)) --> 12.5
+```
+
+- `json.null` encodes to `null`, and decoded `null` becomes `json.null`
+
+```lua
+local value = json.decode('{"a":null}')
+print(value.a == json.null) --> true
+print(json.encode(json.null)) --> null
+```
+
+- tables encode as arrays or objects based on their keys
+
+```lua
+print(json.encode({})) --> []
+print(json.encode({ "a", "b" })) --> ["a","b"]
+print(json.encode({ a = 1 })) --> {"a":1}
+```
+
+- standalone JSON values like booleans, strings, and numbers are valid
+
+```lua
+print(json.decode("true")) --> true
+print(json.decode('"text"')) --> text
+print(json.decode("42")) --> 42
+```
+
+- encoding rejects unsupported Lua types, cyclic tables, mixed tables, sparse
+ arrays, `NaN`, and infinities
+
+```lua
+local t = {}
+t.self = t
+
+assert(json.encode(function() end))
+--> unsupported type: function
+
+assert(json.encode(t))
+--> cannot encode cyclic table
+
+assert(json.encode({ [1] = "a", b = true }))
+--> cannot encode mixed table
+
+assert(json.encode({ [1] = "a", [3] = "c" }))
+--> cannot encode sparse array
+
+assert(json.encode(0 / 0))
+--> cannot encode NaN
+
+assert(json.encode(math.huge))
+--> cannot encode infinity
+```
+
+- decoding rejects comments, trailing commas, single-quoted strings, and invalid
+ escapes
+
+```lua
+assert(json.decode('{"a":1,}'))
+--> expected string key at line 1, column 8
+
+assert(json.decode('{"a":1}// comment'))
+--> unexpected trailing content at line 1, column 8
+
+assert(json.decode("{'a':1}"))
+--> expected string key at line 1, column 2
+
+assert(json.decode('["\\x"]'))
+--> invalid escape sequence at line 1, column 3
+```
+
+## Functions
+
+
+
+### `decode(s)`
+
+Decode a JSON string into Lua values.
+
+**Parameters**:
+
+- `s` (`string`): JSON string.
+
+**Return**:
+
+- `value` (`any`): Decoded Lua value.
+- `err` (`string?`): Error message when decoding fails.
+
+**Example**:
+
+```lua
+local value = json.decode('{"user":"Ada","active":true,"note":null}')
+print(value.user) --> Ada
+print(value.active) --> true
+print(value.note == json.null) --> true
+```
+
+
+
+### `encode(value, opts?)`
+
+Encode a Lua value as JSON.
+
+**Parameters**:
+
+- `value` (`any`): Lua value to encode.
+- `opts?` (`{sort_keys?:boolean, indent?:string}`): Encoding options.
+
+**Return**:
+
+- `json` (`string?`): JSON string.
+- `err` (`string?`): Error message when encoding fails.
+
+**Example**:
+
+```lua
+local s = json.encode({
+ ok = true,
+ items = { 1, 2, 3 },
+}, {
+ indent = " ",
+})
+
+print(s)
+-- {
+-- "ok": true,
+-- "items": [
+-- 1,
+-- 2,
+-- 3
+-- ]
+-- }
+```
diff --git a/docs/src/modules/keyword.md b/docs/src/modules/keyword.md
index 97a0aaf..8e0cb94 100644
--- a/docs/src/modules/keyword.md
+++ b/docs/src/modules/keyword.md
@@ -17,104 +17,124 @@ kw.isidentifier("hello_world") --> true
## Functions
-| Function | Description |
-| ----------------------------------------------------- | ------------------------------------------------------------- |
-| [`iskeyword(v)`](#fn-iskeyword) | Return `true` when `v` is a reserved Lua keyword. |
-| [`isidentifier(v)`](#fn-isidentifier) | Return `true` when `v` is a valid non-keyword Lua identifier. |
-| [`kwlist()`](#fn-kwlist) | Return Lua keywords as a [`mods.List`](/modules/list). |
-| [`kwset()`](#fn-kwset) | Return Lua keywords as a [`mods.Set`](/modules/set). |
-| [`normalize_identifier(s)`](#fn-normalize-identifier) | Normalize an input into a safe Lua identifier. |
+**Collections**:
-
+| Function | Description |
+| ------------------------ | ------------------------------------------------------ |
+| [`kwlist()`](#fn-kwlist) | Return Lua keywords as a [`mods.List`](/modules/list). |
+| [`kwset()`](#fn-kwset) | Return Lua keywords as a [`mods.Set`](/modules/set). |
-### `iskeyword(v)`
+**Normalization**:
-Return `true` when `v` is a reserved Lua keyword.
+| Function | Description |
+| ----------------------------------------------------- | ---------------------------------------------- |
+| [`normalize_identifier(s)`](#fn-normalize-identifier) | Normalize an input into a safe Lua identifier. |
-**Parameters**:
+**Predicates**:
-- `v` (`any`): Value to validate.
+| Function | Description |
+| ------------------------------------- | ------------------------------------------------------------- |
+| [`isidentifier(v)`](#fn-isidentifier) | Return `true` when `v` is a valid non-keyword Lua identifier. |
+| [`iskeyword(v)`](#fn-iskeyword) | Return `true` when `v` is a reserved Lua keyword. |
+
+### Collections
+
+
+
+#### `kwlist()`
+
+Return Lua keywords as a [`mods.List`](/modules/list).
**Return**:
-- `isKeyword` (`boolean`): Whether the check succeeds.
+- `words` (`mods.List`): List of Lua keywords.
**Example**:
```lua
-kw.iskeyword("function") --> true
-kw.iskeyword("hello") --> false
+kw.kwlist():contains("and") --> true
+kw.kwlist():contains("global") --> true -- Lua 5.5+
```
-
-
-### `isidentifier(v)`
-
-Return `true` when `v` is a valid non-keyword Lua identifier.
+
-**Parameters**:
+#### `kwset()`
-- `v` (`any`): Value to validate.
+Return Lua keywords as a [`mods.Set`](/modules/set).
**Return**:
-- `isIdentifier` (`boolean`): Whether the check succeeds.
+- `words` (`mods.Set`): Set of Lua keywords.
**Example**:
```lua
-kw.isidentifier("hello_world") --> true
-kw.isidentifier("local") --> false
+kw.kwlset():contains("and") --> true
+kw.kwlset():contains("global") --> true -- Lua 5.5+
```
-
+### Normalization
-### `kwlist()`
+
-Return Lua keywords as a [`mods.List`](/modules/list).
+#### `normalize_identifier(s)`
+
+Normalize an input into a safe Lua identifier.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
**Return**:
-- `words` (`mods.List`): List of Lua keywords.
+- `identifier` (`string`): Normalized Lua identifier.
**Example**:
```lua
-kw.kwlist():contains("and") --> true
+kw.normalize_identifier(" 2 bad-name ") --> "_2_bad_name"
```
-
+### Predicates
-### `kwset()`
+
-Return Lua keywords as a [`mods.Set`](/modules/set).
+#### `isidentifier(v)`
+
+Return `true` when `v` is a valid non-keyword Lua identifier.
+
+**Parameters**:
+
+- `v` (`any`): Value to validate.
**Return**:
-- `words` (`mods.Set`): Set of Lua keywords.
+- `isIdentifier` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-kw.kwlset():contains("and") --> true
+kw.isidentifier("hello_world") --> true
+kw.isidentifier("local") --> false
```
-
+
-### `normalize_identifier(s)`
+#### `iskeyword(v)`
-Normalize an input into a safe Lua identifier.
+Return `true` when `v` is a reserved Lua keyword.
**Parameters**:
-- `s` (`string`): Input string.
+- `v` (`any`): Value to validate.
**Return**:
-- `identifier` (`string`): Normalized Lua identifier.
+- `isKeyword` (`boolean`): Whether the check succeeds.
**Example**:
```lua
-kw.normalize_identifier(" 2 bad-name ") --> "_2_bad_name"
+kw.iskeyword("function") --> true
+kw.iskeyword("hello") --> false
```
diff --git a/docs/src/modules/list.md b/docs/src/modules/list.md
index 3b30c4a..7a6bba1 100644
--- a/docs/src/modules/list.md
+++ b/docs/src/modules/list.md
@@ -25,15 +25,18 @@ print(ls:index("b")) --> 2
## Functions
-**Predicates**:
+**Access**:
-| Function | Description |
-| -------------------------- | ------------------------------------------------- |
-| [`all(pred)`](#fn-all) | Return `true` if all values match the predicate. |
-| [`any(pred)`](#fn-any) | Return `true` if any value matches the predicate. |
-| [`equals(ls)`](#fn-equals) | Compare two lists using shallow element equality. |
-| [`lt(ls)`](#fn-lt) | Compare two lists lexicographically. |
-| [`le(ls)`](#fn-le) | Compare two lists lexicographically. |
+| Function | Description |
+| ---------------------- | ------------------------------------------- |
+| [`first()`](#fn-first) | Return the first element or `nil` if empty. |
+| [`last()`](#fn-last) | Return the last element or `nil` if empty. |
+
+**Copies**:
+
+| Function | Description |
+| -------------------- | ---------------------------------- |
+| [`copy()`](#fn-copy) | Return a shallow copy of the list. |
**Mutation**:
@@ -49,15 +52,20 @@ print(ls:index("b")) --> 2
| [`pop(pos)`](#fn-pop) | Remove and return the element at the given position. |
| [`prepend(v)`](#fn-prepend) | Insert a value at the start of the list. |
| [`remove(v)`](#fn-remove) | Remove the first matching value. |
+| [`shuffle(rng?)`](#fn-shuffle) | Shuffle the list in place. |
| [`sort(comp?)`](#fn-sort) | Sort the list in place. |
-**Copying**:
+**Predicates**:
-| Function | Description |
-| -------------------- | ---------------------------------- |
-| [`copy()`](#fn-copy) | Return a shallow copy of the list. |
+| Function | Description |
+| -------------------------- | ------------------------------------------------- |
+| [`all(pred)`](#fn-all) | Return `true` if all values match the predicate. |
+| [`any(pred)`](#fn-any) | Return `true` if any value matches the predicate. |
+| [`equals(ls)`](#fn-equals) | Compare two lists using shallow element equality. |
+| [`le(ls)`](#fn-le) | Compare two lists lexicographically. |
+| [`lt(ls)`](#fn-lt) | Compare two lists lexicographically. |
-**Query**:
+**Queries**:
| Function | Description |
| -------------------------------- | ----------------------------------------------------------- |
@@ -65,20 +73,14 @@ print(ls:index("b")) --> 2
| [`count(v)`](#fn-count) | Count how many times a value appears. |
| [`index(v)`](#fn-index) | Return the index of the first matching value. |
| [`index_if(pred)`](#fn-index-if) | Return the index of the first value matching the predicate. |
-| [`len()`](#fn-len) | Return the number of elements in the list. |
| [`isempty()`](#fn-isempty) | Return whether the list has no elements. |
+| [`len()`](#fn-len) | Return the number of elements in the list. |
-**Access**:
-
-| Function | Description |
-| ---------------------- | ------------------------------------------- |
-| [`first()`](#fn-first) | Return the first element or `nil` if empty. |
-| [`last()`](#fn-last) | Return the last element or `nil` if empty. |
-
-**Transform**:
+**Transforms**:
| Function | Description |
| ------------------------------------- | -------------------------------------------------------------------- |
+| [`concat(sep?, i?, j?)`](#fn-concat) | Concatenate list values using Lua's native `table.concat` behavior. |
| [`difference(t)`](#fn-difference) | Return a new list with values not in the given list or set. |
| [`drop(n)`](#fn-drop) | Return a new list without the first n elements. |
| [`filter(pred)`](#fn-filter) | Return a new list with values matching the predicate. |
@@ -87,17 +89,17 @@ print(ls:index("b")) --> 2
| [`group_by(fn)`](#fn-group-by) | Group list values by a computed key. |
| [`intersection(t)`](#fn-intersection) | Return values that are also present in the given list or set. |
| [`invert()`](#fn-invert) | Invert values to indices in a new table. |
-| [`concat(sep?, i?, j?)`](#fn-concat) | Concatenate list values using Lua's native `table.concat` behavior. |
| [`join(sep?, quoted?)`](#fn-join) | Join list values into a string. |
-| [`tostring()`](#fn-tostring) | Render the list to a string via the regular method form. |
| [`keypath()`](#fn-keypath) | Render list items as a table-access key path. |
| [`map(fn)`](#fn-map) | Return a new list by mapping each value. |
+| [`mirror()`](#fn-mirror) | Mirror values into a new table as both keys and values. |
| [`mul(n)`](#fn-mul) | Return a new list repeated `n` times (list multiplication behavior). |
| [`reduce(fn, init?)`](#fn-reduce) | Reduce the list to a single value using an accumulator. |
| [`reverse()`](#fn-reverse) | Reverse the list in place. |
-| [`toset()`](#fn-toset) | Convert the list to a set. |
| [`slice(i?, j?)`](#fn-slice) | Return a new list containing items from i to j (inclusive). |
| [`take(n)`](#fn-take) | Return the first n elements as a new list. |
+| [`toset()`](#fn-toset) | Convert the list to a set. |
+| [`tostring()`](#fn-tostring) | Render the list to a string via the regular method form. |
| [`uniq()`](#fn-uniq) | Return a new list with duplicates removed (first occurrence kept). |
| [`zip(t)`](#fn-zip) | Zip two collections into a list of 2-element tables. |
@@ -105,166 +107,69 @@ print(ls:index("b")) --> 2
| Function | Description |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------- |
+| [`__add(ls)`](#fn-add) | Extend the left-hand list in place with right-hand values, then return the same left-hand list reference (`+`). |
| [`__eq(ls)`](#fn-eq) | Compare two lists using shallow element equality (`==`). |
-| [`__lt(ls)`](#fn-lt) | Compare two lists lexicographically (`<`). |
| [`__le(ls)`](#fn-le) | Compare two lists lexicographically (`<=`). |
+| [`__lt(ls)`](#fn-lt) | Compare two lists lexicographically (`<`). |
| [`__mul(n)`](#fn-mul) | Repeat a list `n` times (`*`). |
-| [`__add(ls)`](#fn-add) | Extend the left-hand list in place with right-hand values, then return the same left-hand list reference (`+`). |
| [`__sub(ls)`](#fn-sub) | Return values from the left list that are not present in the right list (`-`). |
| [`__tostring()`](#fn-tostring) | Render the list to a string like `{ "a", "b", 1 }`. |
-### Predicates
-
-Boolean checks for list-wide conditions.
-
-#### `all(pred)`
-
-Return `true` if all values match the predicate.
-
-**Parameters**:
-
-- `pred` (`fun(v:any):boolean`): Predicate function.
-
-**Return**:
-
-- `allMatch` (`boolean`): Whether the condition is met.
-
-**Example**:
-
-```lua
-is_even = function(v) return v % 2 == 0 end
-ok = List({ 2, 4 }):all(is_even) --> true
-```
-
-> [!NOTE]
->
-> Empty lists return `true`.
-
-
-
-#### `any(pred)`
-
-Return `true` if any value matches the predicate.
-
-**Parameters**:
-
-- `pred` (`fun(v:any):boolean`): Predicate function.
-
-**Return**:
-
-- `anyMatch` (`boolean`): Whether the condition is met.
-
-**Example**:
-
-```lua
-has_len_2 = function(v) return #v == 2 end
-ok = List({ "a", "bb" }):any(has_len_2) --> true
-```
-
-
-
-#### `equals(ls)`
+### Access
-Compare two lists using shallow element equality.
+
-**Parameters**:
+#### `first()`
-- `ls` (`mods.List|any[]`): Other list value.
+Return the first element or `nil` if empty.
**Return**:
-- `isEqual` (`boolean`): Whether the condition is met.
+- `firstValue` (`any`): First value, or `nil` if empty.
**Example**:
```lua
-a = List({ "x", "y" })
-b = List({ "x", "y" })
-ok = a:equals(b) --> true
+v = List({ "a", "b" }):first() --> "a"
```
-> [!NOTE]
->
-> - `equals` is also available through the `==` operator when both operands are
-> `List`.
->
-> ```lua
-> a = List({ "a", 1 })
-> b = List({ "a", 1 })
-> ok = (a == b) --> true
-> ```
->
-> - Unlike `==`, this method also works when `ls` is a plain array table.
->
-> ```lua
-> a = List({ "a", 1 })
-> b = { "a", 1 }
-> ok = a:equals(b) --> true
-> ```
->
-> - `equals` checks only array positions (`1..#list`), so extra non-array keys
-> are ignored:
->
-> ```lua
-> t = {}
-> a = List({ "a", t })
-> b = { "a", t, a = 1 }
-> ok = a:equals(b) --> true
-> ```
-
-
-
-#### `lt(ls)`
-
-Compare two lists lexicographically.
+
-**Parameters**:
+#### `last()`
-- `ls` (`mods.List|any[]`): Other list value.
+Return the last element or `nil` if empty.
**Return**:
-- `isLess` (`boolean`): Whether the condition is met.
+- `lastValue` (`any`): Last value, or `nil` if empty.
**Example**:
```lua
-ok = List({ 1, 2 }):lt({ 1, 3 }) --> true
-ok = List({ 1, 2 }):lt({ 1, 2, 0 }) --> true
+v = List({ "a", "b" }):last() --> "b"
```
-> [!NOTE]
->
-> `lt` is also available through the `<` operator.
-
-
-
-#### `le(ls)`
+### Copies
-Compare two lists lexicographically.
+
-**Parameters**:
+#### `copy()`
-- `ls` (`mods.List|any[]`): Other list value.
+Return a shallow copy of the list.
**Return**:
-- `isLessOrEqual` (`boolean`): Whether the condition is met.
+- `ls` (`mods.List`): New list.
**Example**:
```lua
-ok = List({ 1, 2 }):le({ 1, 2 }) --> true
-ok = List({ 1, 2 }):le({ 1, 1 }) --> false
+c = List({ "a", "b" }):copy() --> { "a", "b" }
```
-> [!NOTE]
->
-> `le` is also available through the `<=` operator.
-
### Mutation
-In-place operations that modify the current list.
+
#### `append()`
@@ -464,6 +369,27 @@ ls = List({ "a", "b", "b" })
ls:remove("b") --> { "a", "b" }
```
+
+
+#### `shuffle(rng?)`
+
+Shuffle the list in place.
+
+**Parameters**:
+
+- `rng?` (`fun(lo:integer, hi:integer):integer`): Optional random index picker;
+ defaults to `math.random`.
+
+**Return**:
+
+- `self` (`T`): Current list.
+
+**Example**:
+
+```lua
+ls = List({ "a", "b", "c" }):shuffle() --> { "b", "c", "a" } -- order varies
+```
+
#### `sort(comp?)`
@@ -491,27 +417,158 @@ words:sort(function(a, b)
end) --> { "a", "bb", "ccc" }
```
-### Copying
+### Predicates
-Operations that return copied list data.
+
-#### `copy()`
+#### `all(pred)`
+
+Return `true` if all values match the predicate.
+
+**Parameters**:
+
+- `pred` (`fun(v:any):boolean`): Predicate function.
+
+**Return**:
+
+- `allMatch` (`boolean`): Whether the condition is met.
+
+**Example**:
+
+```lua
+is_even = function(v) return v % 2 == 0 end
+ok = List({ 2, 4 }):all(is_even) --> true
+```
+
+> [!NOTE]
+>
+> Empty lists return `true`.
+
+
+
+#### `any(pred)`
+
+Return `true` if any value matches the predicate.
+
+**Parameters**:
+
+- `pred` (`fun(v:any):boolean`): Predicate function.
+
+**Return**:
+
+- `anyMatch` (`boolean`): Whether the condition is met.
+
+**Example**:
+
+```lua
+has_len_2 = function(v) return #v == 2 end
+ok = List({ "a", "bb" }):any(has_len_2) --> true
+```
+
+
+
+#### `equals(ls)`
+
+Compare two lists using shallow element equality.
+
+**Parameters**:
+
+- `ls` (`mods.List|any[]`): Other list value.
+
+**Return**:
+
+- `isEqual` (`boolean`): Whether the condition is met.
+
+**Example**:
+
+```lua
+a = List({ "x", "y" })
+b = List({ "x", "y" })
+ok = a:equals(b) --> true
+```
+
+> [!NOTE]
+>
+> - `equals` is also available through the `==` operator when both operands are
+> `List`.
+>
+> ```lua
+> a = List({ "a", 1 })
+> b = List({ "a", 1 })
+> ok = (a == b) --> true
+> ```
+>
+> - Unlike `==`, this method also works when `ls` is a plain array table.
+>
+> ```lua
+> a = List({ "a", 1 })
+> b = { "a", 1 }
+> ok = a:equals(b) --> true
+> ```
+>
+> - `equals` checks only array positions (`1..#list`), so extra non-array keys
+> are ignored:
+>
+> ```lua
+> t = {}
+> a = List({ "a", t })
+> b = { "a", t, a = 1 }
+> ok = a:equals(b) --> true
+> ```
+
+
+
+#### `le(ls)`
+
+Compare two lists lexicographically.
+
+**Parameters**:
+
+- `ls` (`mods.List|any[]`): Other list value.
+
+**Return**:
+
+- `isLessOrEqual` (`boolean`): Whether the condition is met.
+
+**Example**:
+
+```lua
+ok = List({ 1, 2 }):le({ 1, 2 }) --> true
+ok = List({ 1, 2 }):le({ 1, 1 }) --> false
+```
+
+> [!NOTE]
+>
+> `le` is also available through the `<=` operator.
+
+
+
+#### `lt(ls)`
+
+Compare two lists lexicographically.
+
+**Parameters**:
-Return a shallow copy of the list.
+- `ls` (`mods.List|any[]`): Other list value.
**Return**:
-- `ls` (`mods.List`): New list.
+- `isLess` (`boolean`): Whether the condition is met.
**Example**:
```lua
-c = List({ "a", "b" }):copy() --> { "a", "b" }
+ok = List({ 1, 2 }):lt({ 1, 3 }) --> true
+ok = List({ 1, 2 }):lt({ 1, 2, 0 }) --> true
```
-### Query
+> [!NOTE]
+>
+> `lt` is also available through the `<` operator.
+
+### Queries
-Read-only queries for membership, counts, and indices.
+
#### `contains(v)`
@@ -592,26 +649,6 @@ gt_1 = function(x) return x > 1 end
i = List({ 1, 2, 3 }):index_if(gt_1) --> 2
```
-
-
-#### `len()`
-
-Return the number of elements in the list.
-
-**Return**:
-
-- `count` (`integer`): Element count.
-
-**Example**:
-
-```lua
-n = List({ "a", "b", "c" }):len() --> 3
-```
-
-> [!NOTE]
->
-> Uses Lua's `#` operator.
-
#### `isempty()`
@@ -628,43 +665,55 @@ Return whether the list has no elements.
ok = List():isempty() --> true
```
-### Access
-
-Direct element access helpers.
+
-#### `first()`
+#### `len()`
-Return the first element or `nil` if empty.
+Return the number of elements in the list.
**Return**:
-- `firstValue` (`any`): First value, or `nil` if empty.
+- `count` (`integer`): Element count.
**Example**:
```lua
-v = List({ "a", "b" }):first() --> "a"
+n = List({ "a", "b", "c" }):len() --> 3
```
-
+> [!NOTE]
+>
+> Uses Lua's `#` operator.
-#### `last()`
+### Transforms
-Return the last element or `nil` if empty.
+
+
+#### `concat(sep?, i?, j?)`
+
+Concatenate list values using Lua's native `table.concat` behavior.
+
+**Parameters**:
+
+- `sep?` (`string`): Optional separator value (defaults to `""`).
+- `i?` (`integer`): Optional start index (defaults to `1`).
+- `j?` (`integer`): Optional end index (defaults to `#self`).
**Return**:
-- `lastValue` (`any`): Last value, or `nil` if empty.
+- `concatenated` (`string`): Concatenated string.
**Example**:
```lua
-v = List({ "a", "b" }):last() --> "b"
+s = List({ "a", "b", "c" }):concat(",") --> "a,b,c"
```
-### Transform
+> [!NOTE]
+>
+> This method forwards to `table.concat` directly and keeps its strict element
+> rules.
-Non-mutating transformations and derived-list operations.
#### `difference(t)`
@@ -830,33 +879,6 @@ Invert values to indices in a new table.
t = List({ "a", "b", "c" }):invert() --> { a = 1, b = 2, c = 3 }
```
-
-
-#### `concat(sep?, i?, j?)`
-
-Concatenate list values using Lua's native `table.concat` behavior.
-
-**Parameters**:
-
-- `sep?` (`string`): Optional separator value (defaults to `""`).
-- `i?` (`integer`): Optional start index (defaults to `1`).
-- `j?` (`integer`): Optional end index (defaults to `#self`).
-
-**Return**:
-
-- `concatenated` (`string`): Concatenated string.
-
-**Example**:
-
-```lua
-s = List({ "a", "b", "c" }):concat(",") --> "a,b,c"
-```
-
-> [!NOTE]
->
-> This method forwards to `table.concat` directly and keeps its strict element
-> rules.
-
#### `join(sep?, quoted?)`
@@ -884,22 +906,6 @@ s = List({ "a", "b", "c" }):join(", ", true) --> '"a", "b", "c"'
> Values are converted with `tostring` before joining. Set `quoted = true` to
> quote string values.
-
-
-#### `tostring()`
-
-Render the list to a string via the regular method form.
-
-**Return**:
-
-- `renderedList` (`string`): Rendered list string.
-
-**Example**:
-
-```lua
-s = List({ "a", "b", 1 }):tostring() --> '{ "a", "b", 1 }'
-```
-
#### `keypath()`
@@ -937,6 +943,22 @@ to_upper = function(v) return v:upper() end
m = List({ "a", "b" }):map(to_upper) --> { "A", "B" }
```
+
+
+#### `mirror()`
+
+Mirror values into a new table as both keys and values.
+
+**Return**:
+
+- `mirroredValues` (`table`): Table mapping each value to itself.
+
+**Example**:
+
+```lua
+t = List({ "a", "b", "c" }):mirror() --> { a = "a", b = "b", c = "c" }
+```
+
#### `mul(n)`
@@ -1005,26 +1027,6 @@ Reverse the list in place.
r = List({ "a", "b", "c" }):reverse() --> { "c", "b", "a" }
```
-
-
-#### `toset()`
-
-Convert the list to a set.
-
-**Return**:
-
-- `set` (`mods.Set`): New set.
-
-**Example**:
-
-```lua
-s = List({ "a", "b", "a" }):toset() --> { a = true, b = true }
-```
-
-> [!NOTE]
->
-> Order is preserved from the original list.
-
#### `slice(i?, j?)`
@@ -1070,6 +1072,42 @@ Return the first n elements as a new list.
t = List({ "a", "b", "c" }):take(2) --> { "a", "b" }
```
+
+
+#### `toset()`
+
+Convert the list to a set.
+
+**Return**:
+
+- `set` (`mods.Set`): New set.
+
+**Example**:
+
+```lua
+s = List({ "a", "b", "a" }):toset() --> { a = true, b = true }
+```
+
+> [!NOTE]
+>
+> Order is preserved from the original list.
+
+
+
+#### `tostring()`
+
+Render the list to a string via the regular method form.
+
+**Return**:
+
+- `renderedList` (`string`): Rendered list string.
+
+**Example**:
+
+```lua
+s = List({ "a", "b", 1 }):tostring() --> '{ "a", "b", 1 }'
+```
+
#### `uniq()`
@@ -1113,6 +1151,33 @@ z = List({ "a", "b" }):zip(Set({ 1, 2 })) --> { {"a",1}, {"b",2} }
### Metamethods
+
+
+#### `__add(ls)`
+
+Extend the left-hand list in place with right-hand values, then return the same
+left-hand list reference (`+`).
+
+**Parameters**:
+
+- `ls` (`mods.List|any[]`): Other list value.
+
+**Return**:
+
+- `self` (`mods.List|any[]`): Current list.
+
+**Example**:
+
+```lua
+a = List({ "a", "b" })
+b = { "c", "d" }
+c = a + b --> c and a are the same reference: { "a", "b", "c", "d" }
+```
+
+> [!NOTE]
+>
+> `+` operator is equivalent to `:extend(ls)`.
+
#### `__eq(ls)`
@@ -1162,11 +1227,11 @@ ok = a == b --> true (same nested table reference)
> ok = (a == b) --> true
> ```
-
+
-#### `__lt(ls)`
+#### `__le(ls)`
-Compare two lists lexicographically (`<`).
+Compare two lists lexicographically (`<=`).
**Parameters**:
@@ -1174,23 +1239,23 @@ Compare two lists lexicographically (`<`).
**Return**:
-- `isLess` (`boolean`): Whether the condition is met.
+- `isLessOrEqual` (`boolean`): Whether the condition is met.
**Example**:
```lua
-ok = List({ 1, 2 }) < List({ 1, 3 }) --> true
+ok = List({ 1, 2 }) <= List({ 1, 2 }) --> true
```
> [!NOTE]
>
-> `<` is equivalent to `:lt(ls)`.
+> `<=` is equivalent to `:le(ls)`.
-
+
-#### `__le(ls)`
+#### `__lt(ls)`
-Compare two lists lexicographically (`<=`).
+Compare two lists lexicographically (`<`).
**Parameters**:
@@ -1198,17 +1263,17 @@ Compare two lists lexicographically (`<=`).
**Return**:
-- `isLessOrEqual` (`boolean`): Whether the condition is met.
+- `isLess` (`boolean`): Whether the condition is met.
**Example**:
```lua
-ok = List({ 1, 2 }) <= List({ 1, 2 }) --> true
+ok = List({ 1, 2 }) < List({ 1, 3 }) --> true
```
> [!NOTE]
>
-> `<=` is equivalent to `:le(ls)`.
+> `<` is equivalent to `:lt(ls)`.
@@ -1235,33 +1300,6 @@ l2 = 3 * List({ "a", "b" }) --> { "a", "b", "a", "b", "a", "b" }
>
> `*` is equivalent to `:mul(n)`.
-
-
-#### `__add(ls)`
-
-Extend the left-hand list in place with right-hand values, then return the same
-left-hand list reference (`+`).
-
-**Parameters**:
-
-- `ls` (`mods.List|any[]`): Other list value.
-
-**Return**:
-
-- `self` (`mods.List|any[]`): Current list.
-
-**Example**:
-
-```lua
-a = List({ "a", "b" })
-b = { "c", "d" }
-c = a + b --> c and a are the same reference: { "a", "b", "c", "d" }
-```
-
-> [!NOTE]
->
-> `+` operator is equivalent to `:extend(ls)`.
-
#### `__sub(ls)`
diff --git a/docs/src/modules/log.md b/docs/src/modules/log.md
new file mode 100644
index 0000000..749b4f0
--- /dev/null
+++ b/docs/src/modules/log.md
@@ -0,0 +1,94 @@
+---
+description:
+ "Logger factory that emits normalized records through an optional custom
+ handler."
+---
+
+# `log`
+
+Logger factory that emits normalized records through an optional custom handler.
+When `opts.handler` is omitted, records are written to `io.stderr`.
+
+## Usage
+
+```lua
+log = require "mods.log"
+
+local logger = log.new()
+logger:warn("config missing") --> writes: [WARN]: config missing
+```
+
+## Functions
+
+**Factory**:
+
+| Function | Description |
+| ----------------------- | -------------------- |
+| [`new(opts?)`](#fn-new) | Create a new logger. |
+
+**Logger Methods**:
+
+| Function | Description |
+| -------------------------------- | ----------------------------------------------------------- |
+| [`debug(...)`](#fn-debug) | Emit a `debug` record. |
+| [`error(...)`](#fn-error) | Emit an `error` record. |
+| [`info(...)`](#fn-info) | Emit an `info` record. |
+| [`log(levelname, ...)`](#fn-log) | Emit a record for `level` when it passes the logger filter. |
+| [`warn(...)`](#fn-warn) | Emit a `warn` record. |
+
+### Factory
+
+
+
+#### `new(opts?)`
+
+Create a new logger. **Parameters**:
+
+- `opts?` (`mods.log.new.opts`): Logger configuration.
+
+**Return**:
+
+- `logger` (`mods.log.logger`): Logger instance.
+
+### Logger Methods
+
+
+
+#### `debug(...)`
+
+Emit a `debug` record. **Parameters**:
+
+- `...` (`any`): Additional values joined with spaces.
+
+
+
+#### `error(...)`
+
+Emit an `error` record. **Parameters**:
+
+- `...` (`any`): Additional values joined with spaces.
+
+
+
+#### `info(...)`
+
+Emit an `info` record. **Parameters**:
+
+- `...` (`any`): Additional values joined with spaces.
+
+
+
+#### `log(levelname, ...)`
+
+Emit a record for `level` when it passes the logger filter. **Parameters**:
+
+- `levelname` (`string|"debug"|"info"|"warn"|"error"|"off"`): Log level to emit.
+- `...` (`any`): Additional values joined with spaces.
+
+
+
+#### `warn(...)`
+
+Emit a `warn` record. **Parameters**:
+
+- `...` (`any`): Additional values joined with spaces.
diff --git a/docs/src/modules/ntpath.md b/docs/src/modules/ntpath.md
index 119a590..01bdbcb 100644
--- a/docs/src/modules/ntpath.md
+++ b/docs/src/modules/ntpath.md
@@ -24,6 +24,18 @@ print(ntpath.isreserved([[C:\Temp\CON.txt]])) --> true
## Functions
+
+
+### `_expand_percent_vars(p)`
+
+Expand percent-style variables in a string. **Parameters**:
+
+- `p` (`string`)
+
+**Return**:
+
+- `expanded` (`string`)
+
### `ismount(path)`
diff --git a/docs/src/modules/operator.md b/docs/src/modules/operator.md
index 51951c7..31670a5 100644
--- a/docs/src/modules/operator.md
+++ b/docs/src/modules/operator.md
@@ -21,12 +21,12 @@ print(operator.add(1, 2)) --> 3
| Function | Description |
| ------------------------ | --------------------------------------------------------- |
| [`add(a, b)`](#fn-add) | Add two numbers. |
-| [`sub(a, b)`](#fn-sub) | Subtract `b` from `a`. |
-| [`mul(a, b)`](#fn-mul) | Multiply two numbers. |
| [`div(a, b)`](#fn-div) | Divide `a` by `b` using Lua's floating-point division. |
| [`idiv(a, b)`](#fn-idiv) | Divide `a` by `b` and return the floor-division quotient. |
| [`mod(a, b)`](#fn-mod) | Return the modulo remainder of `a` divided by `b`. |
+| [`mul(a, b)`](#fn-mul) | Multiply two numbers. |
| [`pow(a, b)`](#fn-pow) | Raise `a` to the power of `b`. |
+| [`sub(a, b)`](#fn-sub) | Subtract `b` from `a`. |
| [`unm(a)`](#fn-unm) | Negate a number. |
**Comparison**:
@@ -34,19 +34,19 @@ print(operator.add(1, 2)) --> 3
| Function | Description |
| ---------------------- | -------------------------------------------------- |
| [`eq(a, b)`](#fn-eq) | Check whether two values are equal. |
-| [`neq(a, b)`](#fn-neq) | Check whether two values are not equal. |
-| [`lt(a, b)`](#fn-lt) | Check whether `a` is strictly less than `b`. |
-| [`le(a, b)`](#fn-le) | Check whether `a` is less than or equal to `b`. |
-| [`gt(a, b)`](#fn-gt) | Check whether `a` is strictly greater than `b`. |
| [`ge(a, b)`](#fn-ge) | Check whether `a` is greater than or equal to `b`. |
+| [`gt(a, b)`](#fn-gt) | Check whether `a` is strictly greater than `b`. |
+| [`le(a, b)`](#fn-le) | Check whether `a` is less than or equal to `b`. |
+| [`lt(a, b)`](#fn-lt) | Check whether `a` is strictly less than `b`. |
+| [`neq(a, b)`](#fn-neq) | Check whether two values are not equal. |
**Logical**:
| Function | Description |
| ------------------------ | ---------------------------------------------------- |
| [`land(a, b)`](#fn-land) | Evaluate `a and b` with Lua short-circuit semantics. |
-| [`lor(a, b)`](#fn-lor) | Evaluate `a or b` with Lua short-circuit semantics. |
| [`lnot(a)`](#fn-lnot) | Return the boolean negation of `a`. |
+| [`lor(a, b)`](#fn-lor) | Evaluate `a or b` with Lua short-circuit semantics. |
**String & Length**:
@@ -59,13 +59,13 @@ print(operator.add(1, 2)) --> 3
| Function | Description |
| ----------------------------------- | -------------------------------------------------------------- |
+| [`call(f, ...)`](#fn-call) | Call a function with variadic arguments and return its result. |
| [`index(t, k)`](#fn-index) | Return the value at key/index `k` in table `t`. |
| [`setindex(t, k, v)`](#fn-setindex) | Set `t[k] = v` and return the assigned value. |
-| [`call(f, ...)`](#fn-call) | Call a function with variadic arguments and return its result. |
### Arithmetic
-Numeric arithmetic operators as functions.
+
#### `add(a, b)`
@@ -86,53 +86,53 @@ Add two numbers.
add(1, 2) --> 3
```
-
+
-#### `sub(a, b)`
+#### `div(a, b)`
-Subtract `b` from `a`.
+Divide `a` by `b` using Lua's floating-point division.
**Parameters**:
-- `a` (`number`): Left numeric value.
-- `b` (`number`): Right numeric value.
+- `a` (`number`): Dividend value.
+- `b` (`number`): Divisor value.
**Return**:
-- `difference` (`number`): Difference `a - b`.
+- `quotient` (`number`): Quotient `a / b`.
**Example**:
```lua
-sub(5, 3) --> 2
+div(10, 4) --> 2.5
```
-
+
-#### `mul(a, b)`
+#### `idiv(a, b)`
-Multiply two numbers.
+Divide `a` by `b` and return the floor-division quotient.
**Parameters**:
-- `a` (`number`): Left numeric value.
-- `b` (`number`): Right numeric value.
+- `a` (`number`): Dividend value.
+- `b` (`number`): Divisor value.
**Return**:
-- `product` (`number`): Product `a * b`.
+- `quotient` (`integer`): Floor-division result.
**Example**:
```lua
-mul(3, 4) --> 12
+idiv(5, 2) --> 2
```
-
+
-#### `div(a, b)`
+#### `mod(a, b)`
-Divide `a` by `b` using Lua's floating-point division.
+Return the modulo remainder of `a` divided by `b`.
**Parameters**:
@@ -141,75 +141,75 @@ Divide `a` by `b` using Lua's floating-point division.
**Return**:
-- `quotient` (`number`): Quotient `a / b`.
+- `remainder` (`number`): Remainder of `a % b`.
**Example**:
```lua
-div(10, 4) --> 2.5
+mod(5, 2) --> 1
```
-
+
-#### `idiv(a, b)`
+#### `mul(a, b)`
-Divide `a` by `b` and return the floor-division quotient.
+Multiply two numbers.
**Parameters**:
-- `a` (`number`): Dividend value.
-- `b` (`number`): Divisor value.
+- `a` (`number`): Left numeric value.
+- `b` (`number`): Right numeric value.
**Return**:
-- `quotient` (`integer`): Floor-division result.
+- `product` (`number`): Product `a * b`.
**Example**:
```lua
-idiv(5, 2) --> 2
+mul(3, 4) --> 12
```
-
+
-#### `mod(a, b)`
+#### `pow(a, b)`
-Return the modulo remainder of `a` divided by `b`.
+Raise `a` to the power of `b`.
**Parameters**:
-- `a` (`number`): Dividend value.
-- `b` (`number`): Divisor value.
+- `a` (`number`): Base value.
+- `b` (`number`): Exponent value.
**Return**:
-- `remainder` (`number`): Remainder of `a % b`.
+- `power` (`number`): Result of `a ^ b`.
**Example**:
```lua
-mod(5, 2) --> 1
+pow(2, 4) --> 16
```
-
+
-#### `pow(a, b)`
+#### `sub(a, b)`
-Raise `a` to the power of `b`.
+Subtract `b` from `a`.
**Parameters**:
-- `a` (`number`): Base value.
-- `b` (`number`): Exponent value.
+- `a` (`number`): Left numeric value.
+- `b` (`number`): Right numeric value.
**Return**:
-- `power` (`number`): Result of `a ^ b`.
+- `difference` (`number`): Difference `a - b`.
**Example**:
```lua
-pow(2, 4) --> 16
+sub(5, 3) --> 2
```
@@ -234,7 +234,7 @@ unm(3) --> -3
### Comparison
-Equality and ordering comparison operators.
+
#### `eq(a, b)`
@@ -255,32 +255,32 @@ Check whether two values are equal.
eq(1, 1) --> true
```
-
+
-#### `neq(a, b)`
+#### `ge(a, b)`
-Check whether two values are not equal.
+Check whether `a` is greater than or equal to `b`.
**Parameters**:
-- `a` (`any`): Left value.
-- `b` (`any`): Right value.
+- `a` (`number`): Left numeric value.
+- `b` (`number`): Right numeric value.
**Return**:
-- `isNotEqual` (`boolean`): True when `a ~= b`.
+- `isGreaterOrEqual` (`boolean`): True when `a >= b`.
**Example**:
```lua
-neq(1, 2) --> true
+ge(2, 2) --> true
```
-
+
-#### `lt(a, b)`
+#### `gt(a, b)`
-Check whether `a` is strictly less than `b`.
+Check whether `a` is strictly greater than `b`.
**Parameters**:
@@ -289,12 +289,12 @@ Check whether `a` is strictly less than `b`.
**Return**:
-- `isLess` (`boolean`): True when `a < b`.
+- `isGreater` (`boolean`): True when `a > b`.
**Example**:
```lua
-lt(1, 2) --> true
+gt(3, 2) --> true
```
@@ -318,11 +318,11 @@ Check whether `a` is less than or equal to `b`.
le(2, 2) --> true
```
-
+
-#### `gt(a, b)`
+#### `lt(a, b)`
-Check whether `a` is strictly greater than `b`.
+Check whether `a` is strictly less than `b`.
**Parameters**:
@@ -331,38 +331,38 @@ Check whether `a` is strictly greater than `b`.
**Return**:
-- `isGreater` (`boolean`): True when `a > b`.
+- `isLess` (`boolean`): True when `a < b`.
**Example**:
```lua
-gt(3, 2) --> true
+lt(1, 2) --> true
```
-
+
-#### `ge(a, b)`
+#### `neq(a, b)`
-Check whether `a` is greater than or equal to `b`.
+Check whether two values are not equal.
**Parameters**:
-- `a` (`number`): Left numeric value.
-- `b` (`number`): Right numeric value.
+- `a` (`any`): Left value.
+- `b` (`any`): Right value.
**Return**:
-- `isGreaterOrEqual` (`boolean`): True when `a >= b`.
+- `isNotEqual` (`boolean`): True when `a ~= b`.
**Example**:
```lua
-ge(2, 2) --> true
+neq(1, 2) --> true
```
### Logical
-Boolean logic operators with Lua truthiness semantics.
+
#### `land(a, b)`
@@ -383,50 +383,50 @@ Evaluate `a and b` with Lua short-circuit semantics.
land(true, false) --> false
```
-
+
-#### `lor(a, b)`
+#### `lnot(a)`
-Evaluate `a or b` with Lua short-circuit semantics.
+Return the boolean negation of `a`.
**Parameters**:
-- `a` (`T1`): First operand.
-- `b` (`T2`): Second operand.
+- `a` (`any`): Input value.
**Return**:
-- `orValue` (`T1|T2`): Result of `a or b`.
+- `isNot` (`boolean`): Result of `not a`.
**Example**:
```lua
-lor(false, true) --> true
+lnot(true) --> false
```
-
+
-#### `lnot(a)`
+#### `lor(a, b)`
-Return the boolean negation of `a`.
+Evaluate `a or b` with Lua short-circuit semantics.
**Parameters**:
-- `a` (`any`): Input value.
+- `a` (`T1`): First operand.
+- `b` (`T2`): Second operand.
**Return**:
-- `isNot` (`boolean`): Result of `not a`.
+- `orValue` (`T1|T2`): Result of `a or b`.
**Example**:
```lua
-lnot(true) --> false
+lor(false, true) --> true
```
### String & Length
-String concatenation and length operators.
+
#### `concat(a, b)`
@@ -469,66 +469,66 @@ len("abc") --> 3
### Tables & Calls
-Table indexing helpers and function invocation.
+
-#### `index(t, k)`
+#### `call(f, ...)`
-Return the value at key/index `k` in table `t`.
+Call a function with variadic arguments and return its result.
**Parameters**:
-- `t` (`table`): Source table.
-- `k` (`T`): Key/index value.
+- `f` (`fun(...:T1):T2`): Function to call.
+- `...` (`T1`): Additional arguments.
**Return**:
-- `indexedValue` (`T`): Value stored at `t[k]`.
+- `callResult` (`T2`): Return value(s) from `f(...)`.
**Example**:
```lua
-index({ a = 1 }, "a") --> 1
+call(math.max, 1, 2) --> 2
```
-
+
-#### `setindex(t, k, v)`
+#### `index(t, k)`
-Set `t[k] = v` and return the assigned value.
+Return the value at key/index `k` in table `t`.
**Parameters**:
-- `t` (`table`): Target table.
-- `k` (`any`): Key/index value.
-- `v` (`T`): Value to set.
+- `t` (`table`): Source table.
+- `k` (`T`): Key/index value.
**Return**:
-- `assignedValue` (`T`): Assigned value `v`.
+- `indexedValue` (`T`): Value stored at `t[k]`.
**Example**:
```lua
-setindex({}, "a", 1) --> 1
+index({ a = 1 }, "a") --> 1
```
-
+
-#### `call(f, ...)`
+#### `setindex(t, k, v)`
-Call a function with variadic arguments and return its result.
+Set `t[k] = v` and return the assigned value.
**Parameters**:
-- `f` (`fun(...:T1):T2`): Function to call.
-- `...` (`T1`): Additional arguments.
+- `t` (`table`): Target table.
+- `k` (`any`): Key/index value.
+- `v` (`T`): Value to set.
**Return**:
-- `callResult` (`T2`): Return value(s) from `f(...)`.
+- `assignedValue` (`T`): Assigned value `v`.
**Example**:
```lua
-call(math.max, 1, 2) --> 2
+setindex({}, "a", 1) --> 1
```
diff --git a/docs/src/modules/path.md b/docs/src/modules/path.md
index fe5f7a5..92c0272 100644
--- a/docs/src/modules/path.md
+++ b/docs/src/modules/path.md
@@ -18,161 +18,192 @@ print(path.splitext("archive.tar.gz")) --> "archive.tar", ".gz"
## Functions
-**Normalization**:
+| Function | Description |
+| ------------------------------------------------------- | ---------------------------- |
+| [`_splitext(path, sep, altsep?, extsep)`](#fn-splitext) | Split extension from a path. |
-| Function | Description |
-| -------------------------------- | ---------------------------------------------------- |
-| [`normcase(s)`](#fn-normcase) | Normalize path case using the active path semantics. |
-| [`join(path, ...)`](#fn-join) | Join path components. |
-| [`normpath(path)`](#fn-normpath) | Normalize separators and dot segments. |
-| [`isabs(path)`](#fn-isabs) | Return `true` when `path` is absolute. |
+**Anchors**:
+
+| Function | Description |
+| ---------------------------- | ------------------------------------------- |
+| [`anchor(path)`](#fn-anchor) | Return drive and root combined. |
+| [`drive(path)`](#fn-drive) | Return drive prefix when present. |
+| [`root(path)`](#fn-root) | Return root separator segment when present. |
+
+**Components**:
+
+| Function | Description |
+| -------------------------------- | ------------------------------------------------------------- |
+| [`parents(path)`](#fn-parents) | Return logical parent paths from nearest to farthest. |
+| [`parts(path)`](#fn-parts) | Split path into logical parts, including anchor when present. |
+| [`stem(path)`](#fn-stem) | Return filename without its final suffix. |
+| [`suffixes(path)`](#fn-suffixes) | Return all filename suffixes in order. |
+
+**Conversions**:
+
+| Function | Description |
+| -------------------------------- | --------------------------------------------------- |
+| [`as_posix(path)`](#fn-as-posix) | Convert backslashes (`\`) to forward slashes (`/`). |
+| [`as_uri(path)`](#fn-as-uri) | Convert a local path to a `file://` URI. |
+| [`from_uri(uri)`](#fn-from-uri) | Convert a `file://` URI to a local absolute path. |
**Decomposition**:
| Function | Description |
| ------------------------------------ | -------------------------------------------------- |
+| [`basename(path)`](#fn-basename) | Return final path component. |
+| [`dirname(path)`](#fn-dirname) | Return directory portion of a path. |
| [`split(path)`](#fn-split) | Split path into directory head and tail component. |
-| [`splitext(path)`](#fn-splitext) | Split path into a root and extension. |
| [`splitdrive(path)`](#fn-splitdrive) | Split drive prefix from remainder. |
+| [`splitext(path)`](#fn-splitext) | Split path into a root and extension. |
| [`splitroot(path)`](#fn-splitroot) | Split path into drive, root, and tail components. |
-| [`basename(path)`](#fn-basename) | Return final path component. |
-| [`dirname(path)`](#fn-dirname) | Return directory portion of a path. |
-
-**Environment**:
-
-| Function | Description |
-| ------------------------------------ | ----------------------------------------------------------------------- |
-| [`expanduser(path)`](#fn-expanduser) | Expand `~` home segment when available. |
-| [`expandvars(path)`](#fn-expandvars) | Expand vars in a path (`$VAR`/`${VAR}` everywhere, `%VAR%` on Windows). |
-| [`home()`](#fn-home) | Return the current user's home directory path. |
-| [`cwd()`](#fn-cwd) | Return the current working directory path. |
**Derived**:
| Function | Description |
| ----------------------------------------- | ------------------------------------------------ |
| [`abspath(path)`](#fn-abspath) | Return normalized absolute path. |
-| [`relpath(path, start?)`](#fn-relpath) | Return `path` relative to optional `start` path. |
| [`commonpath(paths)`](#fn-commonpath) | Return longest common sub-path from a path list. |
| [`commonprefix(paths)`](#fn-commonprefix) | Return longest common leading string prefix. |
+| [`relpath(path, start?)`](#fn-relpath) | Return `path` relative to optional `start` path. |
-**Anchors**:
+**Environment**:
-| Function | Description |
-| ---------------------------- | ------------------------------------------- |
-| [`drive(path)`](#fn-drive) | Return drive prefix when present. |
-| [`root(path)`](#fn-root) | Return root separator segment when present. |
-| [`anchor(path)`](#fn-anchor) | Return drive and root combined. |
+| Function | Description |
+| ------------------------------------ | ----------------------------------------------------------------------- |
+| `cwd` | Return the current working directory path. |
+| [`expanduser(path)`](#fn-expanduser) | Expand `~` home segment when available. |
+| [`expandvars(path)`](#fn-expandvars) | Expand vars in a path (`$VAR`/`${VAR}` everywhere, `%VAR%` on Windows). |
+| [`home()`](#fn-home) | Return the current user's home directory path. |
-**Components**:
+**Normalization**:
-| Function | Description |
-| -------------------------------- | ------------------------------------------------------------- |
-| [`parts(path)`](#fn-parts) | Split path into logical parts, including anchor when present. |
-| [`stem(path)`](#fn-stem) | Return filename without its final suffix. |
-| [`suffixes(path)`](#fn-suffixes) | Return all filename suffixes in order. |
-| [`parents(path)`](#fn-parents) | Return logical parent paths from nearest to farthest. |
+| Function | Description |
+| -------------------------------- | ---------------------------------------------------- |
+| [`isabs(path)`](#fn-isabs) | Return `true` when `path` is absolute. |
+| [`join(path, ...)`](#fn-join) | Join path components. |
+| [`normcase(s)`](#fn-normcase) | Normalize path case using the active path semantics. |
+| [`normpath(path)`](#fn-normpath) | Normalize separators and dot segments. |
**Relations**:
| Function | Description |
| ------------------------------------------------------- | --------------------------------------------------------------------------------------- |
-| [`relative_to(path, other, walk_up?)`](#fn-relative-to) | Return `path` relative to `other`, or `nil` with an error when it is not under `other`. |
| [`is_relative_to(path, other)`](#fn-is-relative-to) | Return `true` when `path` is under `other`. |
+| [`relative_to(path, other, walk_up?)`](#fn-relative-to) | Return `path` relative to `other`, or `nil` with an error when it is not under `other`. |
| [`with_name(path, name)`](#fn-with-name) | Return a path with the final filename replaced. |
| [`with_stem(path, stem)`](#fn-with-stem) | Return a path with the final filename stem replaced. |
| [`with_suffix(path, suffix)`](#fn-with-suffix) | Return a path with the final filename suffix replaced. |
-**Conversions**:
+
-| Function | Description |
-| ---------------------------------------------------- | --------------------------------------------------------------------------- |
-| [`as_posix(path)`](#fn-as-posix) | Convert backslashes (`\`) to forward slashes (`/`). |
-| [`as_uri(path)`](#fn-as-uri) | Convert a local path to a `file://` URI. |
-| [`match(path, pattern, case_sensitive?)`](#fn-match) | Match a path against a glob-style pattern using only `*` and `?` wildcards. |
-| [`from_uri(uri)`](#fn-from-uri) | Convert a `file://` URI to a local absolute path. |
+### `_splitext(path, sep, altsep?, extsep)`
-### Normalization
+Split extension from a path. **Parameters**:
-
+- `path` (`string`)
+- `sep` (`string`)
+- `altsep?` (`string`)
+- `extsep` (`string`)
-#### `normcase(s)`
+**Return**:
-Normalize path case using the active path semantics.
+- `root` (`string`)
+- `ext` (`string`)
+
+### Anchors
+
+
+
+#### `anchor(path)`
+
+Return drive and root combined.
**Parameters**:
-- `s` (`string`): Input path value.
+- `path` (`string`): Input path.
**Return**:
-- `normalizedPath` (`string`): Path after case normalization.
+- `anchor` (`string`): Drive and root anchor.
**Example**:
```lua
-path.normcase("ABC") --> "abc"
-path.normcase("/A/B") --> "\\a\\b"
+path.anchor("c:\\") --> "c:\\"
```
-> [!NOTE]
->
-> On POSIX semantics this returns the input unchanged. Use
-> [`mods.ntpath`](/modules/ntpath) to force Windows-style case folding and
-> separator normalization.
+
-
+#### `drive(path)`
-#### `join(path, ...)`
+Return drive prefix when present.
-Join path components.
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `drivePrefix` (`string`): Drive prefix.
+
+**Example**:
+
+```lua
+path.drive("c:a/b") --> "c:"
+path.drive("a/b") --> ""
+```
+
+
+
+#### `root(path)`
+
+Return root separator segment when present.
**Parameters**:
-- `path` (`string`): Base path component.
-- `...` (`string`): Additional path components.
+- `path` (`string`): Input path.
**Return**:
-- `joinedPath` (`string`): Joined path.
+- `rootSeparator` (`string`): Root separator segment.
**Example**:
```lua
-path.join("/usr", "bin") --> "/usr/bin"
-path.join([[C:/a]], [[b]]) --> [[C:/a\b]]
+path.root("/tmp/a.txt") --> "/"
+path.root("c:/") --> "\\"
+path.root("a/b") --> ""
```
-> [!NOTE]
->
-> Single input is returned as-is.
+### Components
-
+
-#### `normpath(path)`
+#### `parents(path)`
-Normalize separators and dot segments.
+Return logical parent paths from nearest to farthest.
**Parameters**:
-- `path` (`string`): Path to normalize.
+- `path` (`string`): Input path.
**Return**:
-- `normalizedPath` (`string`): Normalized path.
+- `parents` (`mods.List`): Ancestor paths from nearest to farthest.
**Example**:
```lua
-path.normpath("/a//./b/..") --> "/a"
-path.normpath([[A/foo/../B]]) --> [[A\B]]
+path.parents("a/b/c") --> {"a/b", "a", "."}
+path.parents("c:a/b") --> {"c:a", "c:"}
```
-
+
-#### `isabs(path)`
+#### `parts(path)`
-Return `true` when `path` is absolute.
+Split path into logical parts, including anchor when present.
**Parameters**:
@@ -180,21 +211,21 @@ Return `true` when `path` is absolute.
**Return**:
-- `isAbsolute` (`boolean`): True when `path` is absolute.
+- `paths` (`mods.List`): Path parts including anchor when present.
**Example**:
```lua
-path.isabs("/a/b") --> true
+path.parts("a/b.txt") --> {"a", "b.txt"}
+path.parts("/a/b") --> {"/", "a", "b"}
+path.parts("c:a\\b") --> {"c:", "a", "b"}
```
-### Decomposition
-
-
+
-#### `split(path)`
+#### `stem(path)`
-Split path into directory head and tail component.
+Return filename without its final suffix.
**Parameters**:
@@ -202,20 +233,20 @@ Split path into directory head and tail component.
**Return**:
-- `head` (`string`): Directory portion.
-- `tail` (`string`): Final path component.
+- `stem` (`string`): Filename stem.
**Example**:
```lua
-path.split("/a/b.txt") --> "/a", "b.txt"
+path.stem("archive.tar.gz") --> "archive.tar"
+path.stem("c:a/b") --> "b"
```
-
+
-#### `splitext(path)`
+#### `suffixes(path)`
-Split path into a root and extension.
+Return all filename suffixes in order.
**Parameters**:
@@ -223,20 +254,22 @@ Split path into a root and extension.
**Return**:
-- `root` (`string`): Path without the final extension.
-- `ext` (`string`): Final extension including leading dot.
+- `suffixes` (`mods.List`): Filename suffixes.
**Example**:
```lua
-path.splitext("archive.tar.gz") --> "archive.tar", ".gz"
+path.suffixes("archive.tar.gz") --> {".tar", ".gz"}
+path.suffixes("a/b") --> {}
```
-
+### Conversions
-#### `splitdrive(path)`
+
-Split drive prefix from remainder.
+#### `as_posix(path)`
+
+Convert backslashes (`\`) to forward slashes (`/`).
**Parameters**:
@@ -244,42 +277,60 @@ Split drive prefix from remainder.
**Return**:
-- `drive` (`string`): Drive or share prefix when present.
-- `rest` (`string`): Path remainder.
+- `posixPath` (`string`): POSIX-style path.
**Example**:
```lua
-path.splitdrive("/a/b") --> "", "/a/b"
+path.as_posix("a\\b\\c") --> "a/b/c"
```
-> [!NOTE]
->
-> On POSIX semantics the drive portion is always empty.
+
-
+#### `as_uri(path)`
-#### `splitroot(path)`
+Convert a local path to a `file://` URI.
-Split path into drive, root, and tail components.
+**Parameters**:
+
+- `path` (`string`): Input path.
+
+**Return**:
+
+- `fileUri` (`string?`): File URI.
+- `err` (`string?`): Error message when conversion fails.
+
+**Example**:
+
+```lua
+path.as_uri("/home/user/report.txt") --> "file:///home/user/report.txt"
+path.as_uri("c:/a/b.c") --> "file:///c:/a/b.c"
+path.as_uri("/a/b%#c") --> "file:///a/b%25%23c"
+```
+
+
+
+#### `from_uri(uri)`
+
+Convert a `file://` URI to a local absolute path.
**Parameters**:
-- `path` (`string`): Path to split.
+- `uri` (`string`): URI value.
**Return**:
-- `drive` (`string`): Drive or share prefix (empty on POSIX).
-- `root` (`string`): Root separator segment.
-- `tail` (`string`): Remaining path without leading root separator.
+- `path` (`string?`): Resolved absolute path.
+- `err` (`string?`): Error message when conversion fails.
**Example**:
```lua
-path.splitroot("/a/b") --> "", "/", "a/b"
-path.splitroot([[C:\a\b]]) --> "C:", [[\]], "a\\b"
+path.from_uri("file://localhost/tmp/a.txt") --> "/tmp/a.txt"
```
+### Decomposition
+
#### `basename(path)`
@@ -322,86 +373,94 @@ path.dirname("/a/b.txt") --> "/a"
path.dirname([[C:\a\b.txt]]) --> [[C:\a]]
```
-### Environment
-
-
+
-#### `expanduser(path)`
+#### `split(path)`
-Expand `~` home segment when available.
+Split path into directory head and tail component.
**Parameters**:
-- `path` (`string`): Path that may begin with `~`.
+- `path` (`string`): Input path.
**Return**:
-- `expandedPath` (`string?`): Path with the home segment expanded when
- available.
-- `err` (`string?`): Error message when `~` expansion cannot be resolved.
+- `head` (`string`): Directory portion.
+- `tail` (`string`): Final path component.
**Example**:
```lua
-path.expanduser("~/tmp") --> "/tmp" (when HOME is set)
-path.expanduser([[x\y]]) --> [[x\y]]
+path.split("/a/b.txt") --> "/a", "b.txt"
```
-
+
-#### `expandvars(path)`
+#### `splitdrive(path)`
-Expand vars in a path (`$VAR`/`${VAR}` everywhere, `%VAR%` on Windows).
+Split drive prefix from remainder.
**Parameters**:
-- `path` (`string`): Path containing variable placeholders.
+- `path` (`string`): Input path.
**Return**:
-- `expandedPath` (`string`): Path with variable values substituted.
+- `drive` (`string`): Drive or share prefix when present.
+- `rest` (`string`): Path remainder.
**Example**:
```lua
-path.expandvars("$HOME/bin") --> "/home/me/bin"
-path.expandvars("${XDG_CONFIG_HOME}/nvim") --> "/home/me/.config/nvim"
-path.expandvars("%USERPROFILE%\\bin") --> "C:\\Users\\me\\bin"
-path.expandvars("$UNKNOWN/bin") --> "$UNKNOWN/bin"
+path.splitdrive("/a/b") --> "", "/a/b"
```
-
+> [!NOTE]
+>
+> On POSIX semantics the drive portion is always empty.
-#### `home()`
+
-Return the current user's home directory path.
+#### `splitext(path)`
+
+Split path into a root and extension.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
**Return**:
-- `homePath` (`string?`): Home directory path when available.
-- `err` (`string?`): Error message when the home directory cannot be resolved.
+- `root` (`string`): Path without the final extension.
+- `ext` (`string`): Final extension including leading dot.
**Example**:
```lua
-path.home()
+path.splitext("archive.tar.gz") --> "archive.tar", ".gz"
```
-
+
-#### `cwd()`
+#### `splitroot(path)`
-Return the current working directory path.
+Split path into drive, root, and tail components.
+
+**Parameters**:
+
+- `path` (`string`): Path to split.
**Return**:
-- `cwd` (`string?`): Current working directory path.
-- `err` (`string?`): Error message when the cwd cannot be resolved.
+- `drive` (`string`): Drive or share prefix (empty on POSIX).
+- `root` (`string`): Root separator segment.
+- `tail` (`string`): Remaining path without leading root separator.
**Example**:
```lua
-path.cwd()
+path.splitroot("/a/b") --> "", "/", "a/b"
+path.splitroot([[C:\a\b]]) --> "C:", [[\]], "a\\b"
```
### Derived
@@ -424,30 +483,7 @@ Return normalized absolute path.
```lua
path.abspath("/a/./b") --> "/a/b"
-path.abspath([[C:\a\..\b]]) --> [[C:\b]]
-```
-
-
-
-#### `relpath(path, start?)`
-
-Return `path` relative to optional `start` path.
-
-**Parameters**:
-
-- `path` (`string`): Input path.
-- `start?` (`string`): Optional base path.
-
-**Return**:
-
-- `relativePath` (`string?`): Relative path from `start` to `path`.
-- `err` (`string?`): Error message when the path cannot be made relative.
-
-**Example**:
-
-```lua
-path.relpath("/a/b/c", "/a") --> "b/c"
-path.relpath([[C:\a\b\c]], [[C:\a]]) --> [[b\c]]
+path.abspath([[C:\a\..\b]]) --> [[C:\b]]
```
@@ -494,100 +530,105 @@ path.commonprefix({"/home/swen/spam", "/home/swen/eggs"}) --> "/home/swen/"
path.commonprefix({"abc", "xyz"}) --> ""
```
-### Anchors
-
-
+
-#### `drive(path)`
+#### `relpath(path, start?)`
-Return drive prefix when present.
+Return `path` relative to optional `start` path.
**Parameters**:
- `path` (`string`): Input path.
+- `start?` (`string`): Optional base path.
**Return**:
-- `drivePrefix` (`string`): Drive prefix.
+- `relativePath` (`string?`): Relative path from `start` to `path`.
+- `err` (`string?`): Error message when the path cannot be made relative.
**Example**:
```lua
-path.drive("c:a/b") --> "c:"
-path.drive("a/b") --> ""
+path.relpath("/a/b/c", "/a") --> "b/c"
+path.relpath([[C:\a\b\c]], [[C:\a]]) --> [[b\c]]
```
-
+### Environment
-#### `root(path)`
+
-Return root separator segment when present.
+#### `cwd`
+
+Return the current working directory path.
+
+#### `expanduser(path)`
+
+Expand `~` home segment when available.
**Parameters**:
-- `path` (`string`): Input path.
+- `path` (`string`): Path that may begin with `~`.
**Return**:
-- `rootSeparator` (`string`): Root separator segment.
+- `expandedPath` (`string?`): Path with the home segment expanded when
+ available.
+- `err` (`string?`): Error message when `~` expansion cannot be resolved.
**Example**:
```lua
-path.root("/tmp/a.txt") --> "/"
-path.root("c:/") --> "\\"
-path.root("a/b") --> ""
+path.expanduser("~/tmp") --> "/tmp" (when HOME is set)
+path.expanduser([[x\y]]) --> [[x\y]]
```
-
+
-#### `anchor(path)`
+#### `expandvars(path)`
-Return drive and root combined.
+Expand vars in a path (`$VAR`/`${VAR}` everywhere, `%VAR%` on Windows).
**Parameters**:
-- `path` (`string`): Input path.
+- `path` (`string`): Path containing variable placeholders.
**Return**:
-- `anchor` (`string`): Drive and root anchor.
+- `expandedPath` (`string`): Path with variable values substituted.
**Example**:
```lua
-path.anchor("c:\\") --> "c:\\"
+path.expandvars("$HOME/bin") --> "/home/me/bin"
+path.expandvars("${XDG_CONFIG_HOME}/nvim") --> "/home/me/.config/nvim"
+path.expandvars("%USERPROFILE%\\bin") --> "C:\\Users\\me\\bin"
+path.expandvars("$UNKNOWN/bin") --> "$UNKNOWN/bin"
```
-### Components
-
-
-
-#### `parts(path)`
-
-Split path into logical parts, including anchor when present.
+
-**Parameters**:
+#### `home()`
-- `path` (`string`): Input path.
+Return the current user's home directory path.
**Return**:
-- `paths` (`mods.List`): Path parts including anchor when present.
+- `homePath` (`string?`): Home directory path when available.
+- `err` (`string?`): Error message when the home directory cannot be resolved.
**Example**:
```lua
-path.parts("a/b.txt") --> {"a", "b.txt"}
-path.parts("/a/b") --> {"/", "a", "b"}
-path.parts("c:a\\b") --> {"c:", "a", "b"}
+path.home()
```
-
+### Normalization
-#### `stem(path)`
+
-Return filename without its final suffix.
+#### `isabs(path)`
+
+Return `true` when `path` is absolute.
**Parameters**:
@@ -595,87 +636,90 @@ Return filename without its final suffix.
**Return**:
-- `stem` (`string`): Filename stem.
+- `isAbsolute` (`boolean`): True when `path` is absolute.
**Example**:
```lua
-path.stem("archive.tar.gz") --> "archive.tar"
-path.stem("c:a/b") --> "b"
+path.isabs("/a/b") --> true
```
-
+
-#### `suffixes(path)`
+#### `join(path, ...)`
-Return all filename suffixes in order.
+Join path components.
**Parameters**:
-- `path` (`string`): Input path.
+- `path` (`string`): Base path component.
+- `...` (`string`): Additional path components.
**Return**:
-- `suffixes` (`mods.List`): Filename suffixes.
+- `joinedPath` (`string`): Joined path.
**Example**:
```lua
-path.suffixes("archive.tar.gz") --> {".tar", ".gz"}
-path.suffixes("a/b") --> {}
+path.join("/usr", "bin") --> "/usr/bin"
+path.join([[C:/a]], [[b]]) --> [[C:/a\b]]
```
-
+> [!NOTE]
+>
+> Single input is returned as-is.
-#### `parents(path)`
+
-Return logical parent paths from nearest to farthest.
+#### `normcase(s)`
+
+Normalize path case using the active path semantics.
**Parameters**:
-- `path` (`string`): Input path.
+- `s` (`string`): Input path value.
**Return**:
-- `parents` (`mods.List`): Ancestor paths from nearest to farthest.
+- `normalizedPath` (`string`): Path after case normalization.
**Example**:
```lua
-path.parents("a/b/c") --> {"a/b", "a", "."}
-path.parents("c:a/b") --> {"c:a", "c:"}
+path.normcase("ABC") --> "abc"
+path.normcase("/A/B") --> "\\a\\b"
```
-### Relations
-
-
+> [!NOTE]
+>
+> On POSIX semantics this returns the input unchanged. Use
+> [`mods.ntpath`](/modules/ntpath) to force Windows-style case folding and
+> separator normalization.
-#### `relative_to(path, other, walk_up?)`
+
-Return `path` relative to `other`, or `nil` with an error when it is not under
-`other`.
+#### `normpath(path)`
-When `walk_up` is `true`, allow `..` segments to walk up to a shared prefix.
+Normalize separators and dot segments.
**Parameters**:
-- `path` (`string`): Input path.
-- `other` (`string`): Reference path.
-- `walk_up?` (`boolean`): Allow walking up to a shared prefix.
+- `path` (`string`): Path to normalize.
**Return**:
-- `relativePath` (`string?`): Path relative to `other`, or `nil` on error.
-- `err` (`string?`): Error message when the path cannot be made relative.
+- `normalizedPath` (`string`): Normalized path.
**Example**:
```lua
-path.relative_to("/a/b/c.txt", "/a") --> "b/c.txt"
-path.relative_to("/a/b", "/a/c", true) --> "../b"
-path.relative_to("/a/b", "/a/x") --> nil, "'/a/b' is not in the subpath of '/a/x'"
+path.normpath("/a//./b/..") --> "/a"
+path.normpath([[A/foo/../B]]) --> [[A\B]]
```
+### Relations
+
#### `is_relative_to(path, other)`
@@ -699,6 +743,34 @@ path.is_relative_to("C:A/B", "c:a") --> true
path.is_relative_to("a/b", "a/b/c") --> false
```
+
+
+#### `relative_to(path, other, walk_up?)`
+
+Return `path` relative to `other`, or `nil` with an error when it is not under
+`other`.
+
+When `walk_up` is `true`, allow `..` segments to walk up to a shared prefix.
+
+**Parameters**:
+
+- `path` (`string`): Input path.
+- `other` (`string`): Reference path.
+- `walk_up?` (`boolean`): Allow walking up to a shared prefix.
+
+**Return**:
+
+- `relativePath` (`string?`): Path relative to `other`, or `nil` on error.
+- `err` (`string?`): Error message when the path cannot be made relative.
+
+**Example**:
+
+```lua
+path.relative_to("/a/b/c.txt", "/a") --> "b/c.txt"
+path.relative_to("/a/b", "/a/c", true) --> "../b"
+path.relative_to("/a/b", "/a/x") --> nil, "'/a/b' is not in the subpath of '/a/x'"
+```
+
#### `with_name(path, name)`
@@ -774,94 +846,3 @@ path.with_suffix("a/b.gz", ".lua") --> "a/b/.lua"
path.with_suffix("a/b", "gz") --> nil, "invalid suffix 'gz'"
path.with_suffix("//a/b", "gz") --> nil, "'//a/b' has an empty name"
```
-
-### Conversions
-
-
-
-#### `as_posix(path)`
-
-Convert backslashes (`\`) to forward slashes (`/`).
-
-**Parameters**:
-
-- `path` (`string`): Input path.
-
-**Return**:
-
-- `posixPath` (`string`): POSIX-style path.
-
-**Example**:
-
-```lua
-path.as_posix("a\\b\\c") --> "a/b/c"
-```
-
-
-
-#### `as_uri(path)`
-
-Convert a local path to a `file://` URI.
-
-**Parameters**:
-
-- `path` (`string`): Input path.
-
-**Return**:
-
-- `fileUri` (`string?`): File URI.
-- `err` (`string?`): Error message when conversion fails.
-
-**Example**:
-
-```lua
-path.as_uri("/home/user/report.txt") --> "file:///home/user/report.txt"
-path.as_uri("c:/a/b.c") --> "file:///c:/a/b.c"
-path.as_uri("/a/b%#c") --> "file:///a/b%25%23c"
-```
-
-
-
-#### `match(path, pattern, case_sensitive?)`
-
-Match a path against a glob-style pattern using only `*` and `?` wildcards.
-
-**Parameters**:
-
-- `path` (`string`): Input path.
-- `pattern` (`string`): Pattern to match.
-- `case_sensitive?` (`boolean`): Override platform-default case matching.
-
-**Return**:
-
-- `matchesPattern` (`boolean`): True when the path matches.
-
-**Example**:
-
-```lua
-path.match("a/b.lua", "*.lua") --> true
-path.match("A.lua", "a.LUA", false) --> true
-path.match("notes.txt", "n?tes.*") --> true
-path.match("a/b/c.lua", "a/*/c.lua") --> true
-```
-
-
-
-#### `from_uri(uri)`
-
-Convert a `file://` URI to a local absolute path.
-
-**Parameters**:
-
-- `uri` (`string`): URI value.
-
-**Return**:
-
-- `path` (`string?`): Resolved absolute path.
-- `err` (`string?`): Error message when conversion fails.
-
-**Example**:
-
-```lua
-path.from_uri("file://localhost/tmp/a.txt") --> "/tmp/a.txt"
-```
diff --git a/docs/src/modules/runtime.md b/docs/src/modules/runtime.md
index 6c3e236..805d576 100644
--- a/docs/src/modules/runtime.md
+++ b/docs/src/modules/runtime.md
@@ -11,102 +11,121 @@ Lua runtime metadata and version compatibility flags.
```lua
runtime = require "mods.runtime"
-print(runtime.version) --> "Lua 5.x"
-print(runtime.version_num) --> 501 | 502 | 503 | 504
-print(runtime.is_lua54) --> true | false
+print(runtime.version) --> 501 | 502 | 503 | 504 | 505
+print(runtime.is_lua55) --> true | false
```
## Fields
-| Field | Description |
-| ----------------------------- | ------------------------------------------------- |
-| [`version`](#version) | Version string reported by the runtime. |
-| [`major`](#major) | Major version number parsed from `version`. |
-| [`minor`](#minor) | Minor version number parsed from `version`. |
-| [`version_num`](#version-num) | Numeric version encoded as `major * 100 + minor`. |
-| [`is_luajit`](#is-luajit) | True when running under LuaJIT. |
-| [`is_windows`](#is-windows) | True when running on a Windows host. |
-| [`is_lua51`](#is-lua51) | True only on Lua 5.1 runtimes. |
-| [`is_lua52`](#is-lua52) | True only on Lua 5.2 runtimes. |
-| [`is_lua53`](#is-lua53) | True only on Lua 5.3 runtimes. |
-| [`is_lua54`](#is-lua54) | True only on Lua 5.4 runtimes. |
+| Field | Description |
+| --------------------------- | ------------------------------------------------- |
+| [`is_lua51`](#is-lua51) | True only on Lua 5.1 runtimes. |
+| [`is_lua52`](#is-lua52) | True only on Lua 5.2 runtimes. |
+| [`is_lua53`](#is-lua53) | True only on Lua 5.3 runtimes. |
+| [`is_lua54`](#is-lua54) | True only on Lua 5.4 runtimes. |
+| [`is_lua55`](#is-lua55) | True only on Lua 5.5 runtimes. |
+| [`is_luajit`](#is-luajit) | True when running under LuaJIT. |
+| [`is_windows`](#is-windows) | True when running on a Windows host. |
+| [`major`](#major) | Major version number parsed from `version`. |
+| [`minor`](#minor) | Minor version number parsed from `version`. |
+| [`version`](#version) | Numeric version encoded as `major * 100 + minor`. |
-### `version`
+
-Version string reported by the runtime.
+### `is_lua51` (`boolean`)
+
+True only on Lua 5.1 runtimes.
```lua
-print(runtime.version) --> "Lua 5.x"
+print(runtime.is_lua51) --> true | false
```
-### `major`
+
-Major version number parsed from `version`.
+### `is_lua52` (`boolean`)
+
+True only on Lua 5.2 runtimes.
```lua
-print(runtime.major) --> 5
+print(runtime.is_lua52) --> true | false
```
-### `minor`
+
-Minor version number parsed from `version`.
+### `is_lua53` (`boolean`)
+
+True only on Lua 5.3 runtimes.
```lua
-print(runtime.minor) --> 1 | 2 | 3 | 4
+print(runtime.is_lua53) --> true | false
```
-### `version_num`
+
-Numeric version encoded as `major * 100 + minor`.
+### `is_lua54` (`boolean`)
+
+True only on Lua 5.4 runtimes.
```lua
-print(runtime.version_num) --> 501 | 502 | 503 | 504
+print(runtime.is_lua54) --> true | false
```
-### `is_luajit`
+
-True when running under LuaJIT.
+### `is_lua55` (`boolean`)
+
+True only on Lua 5.5 runtimes.
```lua
-print(runtime.is_luajit) --> true | false
+print(runtime.is_lua55) --> true | false
```
-### `is_windows`
+
-True when running on a Windows host.
+### `is_luajit` (`boolean`)
+
+True when running under LuaJIT.
```lua
-print(runtime.is_windows) --> true | false
+print(runtime.is_luajit) --> true | false
```
-### `is_lua51`
+
-True only on Lua 5.1 runtimes.
+### `is_windows` (`boolean`)
+
+True when running on a Windows host.
```lua
-print(runtime.is_lua51) --> true | false
+print(runtime.is_windows) --> true | false
```
-### `is_lua52`
+
-True only on Lua 5.2 runtimes.
+### `major` (`5`)
+
+Major version number parsed from `version`.
```lua
-print(runtime.is_lua52) --> true | false
+print(runtime.major) --> 5
```
-### `is_lua53`
+
-True only on Lua 5.3 runtimes.
+### `minor` (`1|2|3|4|5`)
+
+Minor version number parsed from `version`.
```lua
-print(runtime.is_lua53) --> true | false
+print(runtime.minor) --> 1 | 2 | 3 | 4 | 5
```
-### `is_lua54`
+
-True only on Lua 5.4 runtimes.
+### `version` (`501|502|503|504|505`)
+
+Numeric version encoded as `major * 100 + minor`.
```lua
-print(runtime.is_lua54) --> true | false
+print(runtime.version) --> 501 | 502 | 503 | 504 | 505
```
diff --git a/docs/src/modules/set.md b/docs/src/modules/set.md
index cad87fb..6265d8b 100644
--- a/docs/src/modules/set.md
+++ b/docs/src/modules/set.md
@@ -29,50 +29,51 @@ print(s:contains("a")) --> true
| [`symmetric_difference_update(set)`](#fn-symmetric-difference-update) | Update the set with elements not shared by both (in place). |
| [`update(set)`](#fn-update) | Add all elements from another set (in place). |
-**Copying**:
-
-| Function | Description |
-| ----------------------------------------------------- | --------------------------------------------------- |
-| [`copy()`](#fn-copy) | Return a shallow copy of the set. |
-| [`difference(t)`](#fn-difference) | Return elements in this set but not in another. |
-| [`intersection(t)`](#fn-intersection) | Return elements common to both sets. |
-| [`remove(v)`](#fn-remove) | Remove an element if present, do nothing otherwise. |
-| [`symmetric_difference(t)`](#fn-symmetric-difference) | Return elements not shared by both sets. |
-| [`union(t)`](#fn-union) | Return a new set with all elements from both. |
-
**Predicates**:
| Function | Description |
| ----------------------------------- | ---------------------------------------------------------------- |
-| [`isdisjoint(set)`](#fn-isdisjoint) | Return true if sets have no elements in common. |
| [`equals(t)`](#fn-equals) | Return true when both sets contain exactly the same members. |
+| [`isdisjoint(set)`](#fn-isdisjoint) | Return true if sets have no elements in common. |
| [`isempty()`](#fn-isempty) | Return true if the set has no elements. |
| [`issubset(t)`](#fn-issubset) | Return true if all elements of this set are also in another set. |
| [`issuperset(t)`](#fn-issuperset) | Return true if this set contains all elements of another set. |
-**Query**:
+**Queries**:
| Function | Description |
| ----------------------------- | ----------------------------------------- |
| [`contains(v)`](#fn-contains) | Return true if the set contains `v`. |
| [`len()`](#fn-len) | Return the number of elements in the set. |
-**Transform**:
+**Set Operations**:
+
+| Function | Description |
+| ----------------------------------------------------- | --------------------------------------------------- |
+| [`copy()`](#fn-copy) | Return a shallow copy of the set. |
+| [`difference(t)`](#fn-difference) | Return elements in this set but not in another. |
+| [`intersection(t)`](#fn-intersection) | Return elements common to both sets. |
+| [`remove(v)`](#fn-remove) | Remove an element if present, do nothing otherwise. |
+| [`symmetric_difference(t)`](#fn-symmetric-difference) | Return elements not shared by both sets. |
+| [`union(t)`](#fn-union) | Return a new set with all elements from both. |
+
+**Transforms**:
-| Function | Description |
-| --------------------------------- | --------------------------------------- |
-| [`map(fn)`](#fn-map) | Return a new set by mapping each value. |
-| [`values()`](#fn-values) | Return a list of all values in the set. |
-| [`tostring()`](#fn-tostring) | Render the set as a string. |
-| [`join(sep?, quoted?)`](#fn-join) | Join set values into a string. |
+| Function | Description |
+| --------------------------------- | ------------------------------------------------------- |
+| [`join(sep?, quoted?)`](#fn-join) | Join set values into a string. |
+| [`map(fn)`](#fn-map) | Return a new set by mapping each value. |
+| [`mirror()`](#fn-mirror) | Mirror values into a new table as both keys and values. |
+| [`tostring()`](#fn-tostring) | Render the set as a string. |
+| [`values()`](#fn-values) | Return a list of all values in the set. |
**Metamethods**:
| Function | Description |
| ------------------------------ | -------------------------------------------------------------------------- |
| [`__add(t)`](#fn-add) | Return the union of two sets using `+`. |
-| [`__bor(t)`](#fn-bor) | Return the union of two sets using `\|`. |
| [`__band(t)`](#fn-band) | Return the intersection of two sets using `&`. |
+| [`__bor(t)`](#fn-bor) | Return the union of two sets using `\|`. |
| [`__bxor(t)`](#fn-bxor) | Return elements present in exactly one set using `^`. |
| [`__eq(t)`](#fn-eq) | Return true if both sets contain exactly the same members using `==`. |
| [`__le(t)`](#fn-le) | Return true if the left set is a subset of the right set using `<=`. |
@@ -82,7 +83,7 @@ print(s:contains("a")) --> true
### Mutation
-In-place operations that mutate the current set.
+
#### `add(v)`
@@ -216,98 +217,101 @@ Add all elements from another set (in place).
s = Set({ "a" }):update(Set({ "b" })) --> s contains "a", "b"
```
-### Copying
+### Predicates
-Non-mutating set operations that return new set instances.
+
-#### `copy()`
+#### `equals(t)`
-Return a shallow copy of the set.
+Return true when both sets contain exactly the same members.
+
+**Parameters**:
+
+- `t` (`mods.Set|mods.List|table`): Other set/list.
**Return**:
-- `set` (`mods.Set`): New set.
+- `isEqual` (`boolean`): True when both sets contain the same members.
**Example**:
```lua
-c = Set({ "a" }):copy() --> c is a new set with "a"
+a = Set({ "a", "b" })
+b = Set({ "b", "a" })
+ok = a:equals(b) --> true
```
-
+> [!NOTE]
+>
+> `equals` is also available as the `__eq` (`==`) operator. `a:equals(b)` is
+> equivalent to `a == b`.
-#### `difference(t)`
+
-Return elements in this set but not in another.
+#### `isdisjoint(set)`
+
+Return true if sets have no elements in common.
**Parameters**:
-- `t` (`mods.Set|mods.List|table`): Other set/list.
+- `set` (`T|mods.List`): Other set/list.
**Return**:
-- `set` (`mods.Set`): New set.
+- `isDisjoint` (`boolean`): True when sets have no elements in common.
**Example**:
```lua
-d = Set({ "a", "b" }):difference(Set({ "b" })) --> d contains "a"
+ok = Set({ "a" }):isdisjoint(Set({ "b" })) --> true
```
-> [!NOTE]
->
-> `difference` is also available as the `__sub` (`-`) operator.
-> `a:difference(b)` is equivalent to `a - b`.
-
-
-
-#### `intersection(t)`
-
-Return elements common to both sets.
+
-**Parameters**:
+#### `isempty()`
-- `t` (`mods.Set|mods.List|table`): Other set/list.
+Return true if the set has no elements.
**Return**:
-- `set` (`mods.Set`): New set.
+- `isEmpty` (`boolean`): True when the set has no elements.
**Example**:
```lua
-i = Set({ "a", "b" }):intersection(Set({ "b", "c" })) --> i contains "b"
+empty = Set({}):isempty() --> true
```
-> [!NOTE]
->
-> `intersection` is also available as `__band` (`&`) on Lua 5.3+.
-
-
+
-#### `remove(v)`
+#### `issubset(t)`
-Remove an element if present, do nothing otherwise.
+Return true if all elements of this set are also in another set.
**Parameters**:
-- `v` (`any`): Value to remove.
+- `t` (`mods.Set|mods.List|table`): Other set/list.
**Return**:
-- `self` (`T`): Current set.
+- `isSubset` (`boolean`): True when every element of `self` exists in `set`.
**Example**:
```lua
-s = Set({ "a", "b" }):remove("b") --> s contains "a"
+ok = Set({ "a" }):issubset(Set({ "a", "b" })) --> true
```
-
+> [!NOTE]
+>
+> `issubset` is also available as the `__le` (`<=`) operator. `a:issubset(b)` is
+> equivalent to `a <= b`.
-#### `symmetric_difference(t)`
+
-Return elements not shared by both sets.
+#### `issuperset(t)`
+
+Return true if this set contains all elements of another set.
**Parameters**:
@@ -315,71 +319,76 @@ Return elements not shared by both sets.
**Return**:
-- `set` (`mods.Set`): New set.
+- `isSuperset` (`boolean`): True when `self` contains every element of `set`.
**Example**:
```lua
-d = Set({ "a", "b" }):symmetric_difference(Set({ "b", "c" }))
---> d contains "a", "c"
+ok = Set({ "a", "b" }):issuperset(Set({ "a" })) --> true
```
-> [!NOTE]
->
-> `symmetric_difference` is also available as `__bxor` (`^`) on Lua 5.3+.
+### Queries
-
+
-#### `union(t)`
+#### `contains(v)`
-Return a new set with all elements from both.
+Return true if the set contains `v`.
**Parameters**:
-- `t` (`mods.Set|mods.List|table`): Other set/list.
+- `v` (`any`): Value to check.
**Return**:
-- `set` (`mods.Set`): New set.
+- `isPresent` (`boolean`): True when `v` is present in the set.
**Example**:
```lua
-s = Set({ "a" }):union(Set({ "b" })) --> s contains "a", "b"
+ok = Set({ "a", "b" }):contains("a") --> true
+ok = Set({ "a", "b" }):contains("z") --> false
```
-> [!NOTE]
->
-> `union` is available as `__add` (`+`) and `__bor` (`|`) on Lua 5.3+.
-> `a:union(b)` is equivalent to `a + b` and `a | b`.
+
-### Predicates
+#### `len()`
-Boolean checks about set relationships and emptiness.
+Return the number of elements in the set.
-#### `isdisjoint(set)`
+**Return**:
-Return true if sets have no elements in common.
+- `count` (`integer`): Element count.
-**Parameters**:
+**Example**:
-- `set` (`T|mods.List`): Other set/list.
+```lua
+n = Set({ "a", "b" }):len() --> 2
+```
+
+### Set Operations
+
+
+
+#### `copy()`
+
+Return a shallow copy of the set.
**Return**:
-- `isDisjoint` (`boolean`): True when sets have no elements in common.
+- `set` (`mods.Set`): New set.
**Example**:
```lua
-ok = Set({ "a" }):isdisjoint(Set({ "b" })) --> true
+c = Set({ "a" }):copy() --> c is a new set with "a"
```
-
+
-#### `equals(t)`
+#### `difference(t)`
-Return true when both sets contain exactly the same members.
+Return elements in this set but not in another.
**Parameters**:
@@ -387,67 +396,68 @@ Return true when both sets contain exactly the same members.
**Return**:
-- `isEqual` (`boolean`): True when both sets contain the same members.
+- `set` (`mods.Set`): New set.
**Example**:
```lua
-a = Set({ "a", "b" })
-b = Set({ "b", "a" })
-ok = a:equals(b) --> true
+d = Set({ "a", "b" }):difference(Set({ "b" })) --> d contains "a"
```
> [!NOTE]
>
-> `equals` is also available as the `__eq` (`==`) operator. `a:equals(b)` is
-> equivalent to `a == b`.
+> `difference` is also available as the `__sub` (`-`) operator.
+> `a:difference(b)` is equivalent to `a - b`.
-
+
-#### `isempty()`
+#### `intersection(t)`
-Return true if the set has no elements.
+Return elements common to both sets.
+
+**Parameters**:
+
+- `t` (`mods.Set|mods.List|table`): Other set/list.
**Return**:
-- `isEmpty` (`boolean`): True when the set has no elements.
+- `set` (`mods.Set`): New set.
**Example**:
```lua
-empty = Set({}):isempty() --> true
+i = Set({ "a", "b" }):intersection(Set({ "b", "c" })) --> i contains "b"
```
-
+> [!NOTE]
+>
+> `intersection` is also available as `__band` (`&`) on Lua 5.3+.
-#### `issubset(t)`
+
-Return true if all elements of this set are also in another set.
+#### `remove(v)`
+
+Remove an element if present, do nothing otherwise.
**Parameters**:
-- `t` (`mods.Set|mods.List|table`): Other set/list.
+- `v` (`any`): Value to remove.
**Return**:
-- `isSubset` (`boolean`): True when every element of `self` exists in `set`.
+- `self` (`T`): Current set.
**Example**:
```lua
-ok = Set({ "a" }):issubset(Set({ "a", "b" })) --> true
+s = Set({ "a", "b" }):remove("b") --> s contains "a"
```
-> [!NOTE]
->
-> `issubset` is also available as the `__le` (`<=`) operator. `a:issubset(b)` is
-> equivalent to `a <= b`.
-
-
+
-#### `issuperset(t)`
+#### `symmetric_difference(t)`
-Return true if this set contains all elements of another set.
+Return elements not shared by both sets.
**Parameters**:
@@ -455,56 +465,73 @@ Return true if this set contains all elements of another set.
**Return**:
-- `isSuperset` (`boolean`): True when `self` contains every element of `set`.
+- `set` (`mods.Set`): New set.
**Example**:
```lua
-ok = Set({ "a", "b" }):issuperset(Set({ "a" })) --> true
+d = Set({ "a", "b" }):symmetric_difference(Set({ "b", "c" }))
+--> d contains "a", "c"
```
-### Query
+> [!NOTE]
+>
+> `symmetric_difference` is also available as `__bxor` (`^`) on Lua 5.3+.
-Read-only queries for membership and size.
+
-#### `contains(v)`
+#### `union(t)`
-Return true if the set contains `v`.
+Return a new set with all elements from both.
**Parameters**:
-- `v` (`any`): Value to check.
+- `t` (`mods.Set|mods.List|table`): Other set/list.
**Return**:
-- `isPresent` (`boolean`): True when `v` is present in the set.
+- `set` (`mods.Set`): New set.
**Example**:
```lua
-ok = Set({ "a", "b" }):contains("a") --> true
-ok = Set({ "a", "b" }):contains("z") --> false
+s = Set({ "a" }):union(Set({ "b" })) --> s contains "a", "b"
```
-
+> [!NOTE]
+>
+> `union` is available as `__add` (`+`) and `__bor` (`|`) on Lua 5.3+.
+> `a:union(b)` is equivalent to `a + b` and `a | b`.
-#### `len()`
+### Transforms
-Return the number of elements in the set.
+
+
+#### `join(sep?, quoted?)`
+
+Join set values into a string.
+
+**Parameters**:
+
+- `sep?` (`string`): Optional separator value (defaults to `""`).
+- `quoted?` (`boolean`): Optional boolean flag (defaults to `false`).
**Return**:
-- `count` (`integer`): Element count.
+- `joined` (`string`): Joined string.
**Example**:
```lua
-n = Set({ "a", "b" }):len() --> 2
+s = Set({ "b", "a" }):join(", ") --> "a, b"
+s = Set({ "b", "a" }):join(", ", true) --> '"a", "b"'
```
-### Transform
+> [!NOTE]
+>
+> Join order is not guaranteed.
-Value-to-value transformations and projection helpers.
+
#### `map(fn)`
@@ -524,20 +551,20 @@ Return a new set by mapping each value.
s = Set({ 1, 2 }):map(function(v) return v * 10 end) --> s contains 10, 20
```
-
+
-#### `values()`
+#### `mirror()`
-Return a list of all values in the set.
+Mirror values into a new table as both keys and values.
**Return**:
-- `values` (`mods.List`): List of set values.
+- `mirroredValues` (`table`): Table mapping each value to itself.
**Example**:
```lua
-values = Set({ "a", "b" }):values() --> { "a", "b" }
+mirrored = Set({ "a", "b" }):mirror() --> { a = "a", b = "b" }
```
@@ -556,32 +583,22 @@ Render the set as a string.
s = Set({ "b", "a", 1 }):tostring() --> '{ 1, "a", "b" }'
```
-
-
-#### `join(sep?, quoted?)`
-
-Join set values into a string.
+
-**Parameters**:
+#### `values()`
-- `sep?` (`string`): Optional separator value (defaults to `""`).
-- `quoted?` (`boolean`): Optional boolean flag (defaults to `false`).
+Return a list of all values in the set.
**Return**:
-- `joined` (`string`): Joined string.
+- `values` (`mods.List`): List of set values.
**Example**:
```lua
-s = Set({ "b", "a" }):join(", ") --> "a, b"
-s = Set({ "b", "a" }):join(", ", true) --> '"a", "b"'
+values = Set({ "a", "b" }):values() --> { "a", "b" }
```
-> [!NOTE]
->
-> Join order is not guaranteed.
-
### Metamethods
@@ -610,11 +627,11 @@ u = a + b --> { a = true, b = true, c = true }
>
> `__add` is the operator form of `:union(set)`.
-
+
-#### `__bor(t)`
+#### `__band(t)`
-Return the union of two sets using `|`.
+Return the intersection of two sets using `&`.
**Parameters**:
@@ -629,18 +646,18 @@ Return the union of two sets using `|`.
```lua
a = Set({ "a", "b" })
b = Set({ "b", "c" })
-u = a | b --> { a = true, b = true, c = true }
+i = a & b --> { b = true }
```
> [!NOTE]
>
-> `__bor` is the operator form of `:union(set)` on Lua 5.3+.
+> `__band` is the operator form of `:intersection(set)` on Lua 5.3+.
-
+
-#### `__band(t)`
+#### `__bor(t)`
-Return the intersection of two sets using `&`.
+Return the union of two sets using `|`.
**Parameters**:
@@ -655,12 +672,12 @@ Return the intersection of two sets using `&`.
```lua
a = Set({ "a", "b" })
b = Set({ "b", "c" })
-i = a & b --> { b = true }
+u = a | b --> { a = true, b = true, c = true }
```
> [!NOTE]
>
-> `__band` is the operator form of `:intersection(set)` on Lua 5.3+.
+> `__bor` is the operator form of `:union(set)` on Lua 5.3+.
diff --git a/docs/src/modules/str.md b/docs/src/modules/str.md
index fa3074c..4f1a88d 100644
--- a/docs/src/modules/str.md
+++ b/docs/src/modules/str.md
@@ -17,6 +17,17 @@ print(str.capitalize("hello world")) --> "Hello world"
## Functions
+**Casing & Transform**:
+
+| Function | Description |
+| -------------------------------------------------------- | --------------------------------------------------------- |
+| [`startswith(s, prefix, start?, stop?)`](#fn-startswith) | Return true if string starts with prefix. |
+| [`swapcase(s)`](#fn-swapcase) | Return a copy with case of alphabetic characters swapped. |
+| [`title(s)`](#fn-title) | Return titlecased copy. |
+| [`translate(s, table_map)`](#fn-translate) | Translate characters using a mapping table. |
+| [`upper(s)`](#fn-upper) | Return uppercased copy. |
+| [`zfill(s, width)`](#fn-zfill) | Pad numeric string on the left with zeros. |
+
**Formatting**:
| Function | Description |
@@ -29,6 +40,17 @@ print(str.capitalize("hello world")) --> "Hello world"
| [`find(s, sub, start?, stop?)`](#fn-find) | Return lowest index of substring or nil if not found. |
| [`format_map(s, mapping)`](#fn-format-map) | Format string with mapping (key-based) replacement. |
+**Layout**:
+
+| Function | Description |
+| ----------------------------------------- | ------------------------------------------------------------------- |
+| [`join(sep, ls)`](#fn-join) | Join an array-like table of strings using this string as separator. |
+| [`ljust(s, width, fillchar?)`](#fn-ljust) | Left-justify string in a field of given width. |
+| [`lower(s)`](#fn-lower) | Return lowercased copy. |
+| [`lstrip(s, chars?)`](#fn-lstrip) | Remove leading characters (default: whitespace). |
+| [`rstrip(s, chars?)`](#fn-rstrip) | Remove trailing characters (default: whitespace). |
+| [`strip(s, chars?)`](#fn-strip) | Remove leading and trailing characters (default: whitespace). |
+
**Predicates**:
| Function | Description |
@@ -37,26 +59,13 @@ print(str.capitalize("hello world")) --> "Hello world"
| [`isalpha(s)`](#fn-isalpha) | Return true if all characters are alphabetic and string is non-empty. |
| [`isascii(s)`](#fn-isascii) | Return true if all characters are ASCII. |
| [`isdecimal(s)`](#fn-isdecimal) | Return true if all characters are decimal characters and string is non-empty. |
-| [`isdigit(s)`](#fn-isdigit) | Return true if all characters are digits and string is non-empty. |
| [`isidentifier(s)`](#fn-isidentifier) | Return true if string is a valid identifier and not a reserved keyword. |
| [`islower(s)`](#fn-islower) | Return true if all cased characters are lowercase and there is at least one cased character. |
-| [`isnumeric(s)`](#fn-isnumeric) | Return true if all characters are numeric and string is non-empty. |
| [`isprintable(s)`](#fn-isprintable) | Return true if all characters are printable. |
| [`isspace(s)`](#fn-isspace) | Return true if all characters are whitespace and string is non-empty. |
| [`istitle(s)`](#fn-istitle) | Return true if string is titlecased. |
| [`isupper(s)`](#fn-isupper) | Return true if all cased characters are uppercase and there is at least one cased character. |
-**Layout**:
-
-| Function | Description |
-| ----------------------------------------- | ------------------------------------------------------------------- |
-| [`join(sep, ls)`](#fn-join) | Join an array-like table of strings using this string as separator. |
-| [`ljust(s, width, fillchar?)`](#fn-ljust) | Left-justify string in a field of given width. |
-| [`lower(s)`](#fn-lower) | Return lowercased copy. |
-| [`lstrip(s, chars?)`](#fn-lstrip) | Remove leading characters (default: whitespace). |
-| [`rstrip(s, chars?)`](#fn-rstrip) | Remove trailing characters (default: whitespace). |
-| [`strip(s, chars?)`](#fn-strip) | Remove leading and trailing characters (default: whitespace). |
-
**Split & Replace**:
| Function | Description |
@@ -73,16 +82,137 @@ print(str.capitalize("hello world")) --> "Hello world"
| [`split(s, sep?, maxsplit?)`](#fn-split) | Split by separator (or whitespace) up to maxsplit. |
| [`splitlines(s, keepends?)`](#fn-splitlines) | Split on line boundaries. |
-**Casing & Transform**:
+### Casing & Transform
-| Function | Description |
-| -------------------------------------------------------- | --------------------------------------------------------- |
-| [`swapcase(s)`](#fn-swapcase) | Return a copy with case of alphabetic characters swapped. |
-| [`startswith(s, prefix, start?, stop?)`](#fn-startswith) | Return true if string starts with prefix. |
-| [`title(s)`](#fn-title) | Return titlecased copy. |
-| [`translate(s, table_map)`](#fn-translate) | Translate characters using a mapping table. |
-| [`upper(s)`](#fn-upper) | Return uppercased copy. |
-| [`zfill(s, width)`](#fn-zfill) | Pad numeric string on the left with zeros. |
+
+
+#### `startswith(s, prefix, start?, stop?)`
+
+Return true if string starts with prefix.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `prefix` (`string|string[]`): Prefix string.
+- `start?` (`integer`): Optional start index (defaults to `1`).
+- `stop?` (`integer`): Optional exclusive end index (defaults to `#s + 1`).
+
+**Return**:
+
+- `hasPrefix` (`boolean`): True when `s` starts with `prefix`.
+
+**Example**:
+
+```lua
+ok = startswith("hello.lua", "he") --> true
+```
+
+> [!NOTE]
+>
+> If prefix is a list, returns `true` when any prefix matches.
+
+
+
+#### `swapcase(s)`
+
+Return a copy with case of alphabetic characters swapped.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `swappedCase` (`string`): String with alphabetic case swapped.
+
+**Example**:
+
+```lua
+s = swapcase("AbC") --> "aBc"
+```
+
+
+
+#### `title(s)`
+
+Return titlecased copy.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `titlecased` (`string`): Titlecased string.
+
+**Example**:
+
+```lua
+s = title("hello world") --> "Hello World"
+```
+
+
+
+#### `translate(s, table_map)`
+
+Translate characters using a mapping table.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `table_map` (`table`): Character translation map.
+
+**Return**:
+
+- `translated` (`string`): Translated string.
+
+**Example**:
+
+```lua
+map = { [string.byte("a")] = "b", ["c"] = false }
+s = translate("abc", map) --> "bb"
+```
+
+
+
+#### `upper(s)`
+
+Return uppercased copy.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+
+**Return**:
+
+- `uppercased` (`string`): Uppercased string.
+
+**Example**:
+
+```lua
+s = upper("Hello") --> "HELLO"
+```
+
+
+
+#### `zfill(s, width)`
+
+Pad numeric string on the left with zeros.
+
+**Parameters**:
+
+- `s` (`string`): Input string.
+- `width` (`integer`): Target width.
+
+**Return**:
+
+- `zeroFilled` (`string`): Zero-padded string.
+
+**Example**:
+
+```lua
+s = zfill("42", 5) --> "00042"
+```
### Formatting
@@ -250,95 +380,56 @@ s = format_map("hi {name}", { name = "bob" }) --> "hi bob"
> `format_map` is a lightweight `{key}` replacement helper. For richer
> templating, use [`mods.template`](/modules/template).
-### Predicates
-
-
-
-#### `isalnum(s)`
-
-Return true if all characters are alphanumeric and string is non-empty.
-
-**Parameters**:
-
-- `s` (`string`): Input string.
-
-**Return**:
-
-- `isAlnum` (`boolean`): True when `s` is non-empty and all characters are
- alphanumeric.
-
-**Example**:
-
-```lua
-ok = isalnum("abc123") --> true
-```
-
-> [!NOTE]
->
-> Lua letters are ASCII by default, so non-ASCII letters are not alphanumeric.
->
-> ```lua
-> isalnum("á1") --> false
-> ```
+### Layout
-
+
-#### `isalpha(s)`
+#### `join(sep, ls)`
-Return true if all characters are alphabetic and string is non-empty.
+Join an array-like table of strings using this string as separator.
**Parameters**:
-- `s` (`string`): Input string.
+- `sep` (`string`): Separator value.
+- `ls` (`string[]`): Table value.
**Return**:
-- `isAlpha` (`boolean`): True when `s` is non-empty and all characters are
- alphabetic.
+- `joined` (`string`): Joined string.
**Example**:
```lua
-ok = isalpha("abc") --> true
+s = join(",", { "a", "b", "c" }) --> "a,b,c"
```
-> [!NOTE]
->
-> Lua letters are ASCII by default, so non-ASCII letters are not alphabetic.
->
-> ```lua
-> isalpha("á") --> false
-> ```
-
-
+
-#### `isascii(s)`
+#### `ljust(s, width, fillchar?)`
-Return true if all characters are ASCII.
+Left-justify string in a field of given width.
**Parameters**:
- `s` (`string`): Input string.
+- `width` (`integer`): Target width.
+- `fillchar?` (`string`): Optional fill character.
**Return**:
-- `isAscii` (`boolean`): True when all bytes in `s` are ASCII.
+- `leftJustified` (`string`): Left-justified string.
**Example**:
```lua
-ok = isascii("hello") --> true
+s = ljust("hi", 5, ".") --> "hi..."
```
-> [!NOTE]
->
-> The empty string returns `true`.
-
-
+
-#### `isdecimal(s)`
+#### `lower(s)`
-Return true if all characters are decimal characters and string is non-empty.
+Return lowercased copy.
**Parameters**:
@@ -346,107 +437,84 @@ Return true if all characters are decimal characters and string is non-empty.
**Return**:
-- `isDecimal` (`boolean`): True when `s` is non-empty and all characters are
- decimal digits.
+- `lowercased` (`string`): Lowercased string.
**Example**:
```lua
-ok = isdecimal("123") --> true
+s = lower("HeLLo") --> "hello"
```
-
+
-#### `isdigit(s)`
+#### `lstrip(s, chars?)`
-Return true if all characters are digits and string is non-empty.
+Remove leading characters (default: whitespace).
**Parameters**:
- `s` (`string`): Input string.
+- `chars?` (`string`): Optional character set.
**Return**:
-- `isDigit` (`boolean`): True when `s` is non-empty and all characters are
- digits.
+- `leadingStripped` (`string`): String with leading characters removed.
**Example**:
```lua
-ok = isdigit("123") --> true
+s = lstrip(" hello") --> "hello"
```
-
+
-#### `isidentifier(s)`
+#### `rstrip(s, chars?)`
-Return true if string is a valid identifier and not a reserved keyword.
+Remove trailing characters (default: whitespace).
**Parameters**:
- `s` (`string`): Input string.
+- `chars?` (`string`): Optional character set.
**Return**:
-- `isIdentifier` (`boolean`): True when `s` is a valid identifier and not a
- keyword.
+- `trailingStripped` (`string`): String with trailing characters removed.
**Example**:
```lua
-ok = isidentifier("foo_bar") --> true
-ok = isidentifier("2var") --> false
-ok = isidentifier("end") --> false (keyword)
+s = rstrip("hello ") --> "hello"
```
-
+
-#### `islower(s)`
+#### `strip(s, chars?)`
-Return true if all cased characters are lowercase and there is at least one
-cased character.
+Remove leading and trailing characters (default: whitespace).
**Parameters**:
- `s` (`string`): Input string.
+- `chars?` (`string`): Optional character set.
**Return**:
-- `isLower` (`boolean`): True when `s` has at least one cased character and all
- are lowercase.
+- `stripped` (`string`): String with leading and trailing characters removed.
**Example**:
```lua
-ok = islower("hello") --> true
+s = strip(" hello ") --> "hello"
```
-
-
-#### `isnumeric(s)`
-
-Return true if all characters are numeric and string is non-empty.
-
-**Parameters**:
-
-- `s` (`string`): Input string.
-
-**Return**:
-
-- `isNumeric` (`boolean`): True when `s` is non-empty and all characters are
- numeric.
-
-**Example**:
-
-```lua
-ok = isnumeric("123") --> true
-```
+### Predicates
-
+
-#### `isprintable(s)`
+#### `isalnum(s)`
-Return true if all characters are printable.
+Return true if all characters are alphanumeric and string is non-empty.
**Parameters**:
@@ -454,23 +522,28 @@ Return true if all characters are printable.
**Return**:
-- `isPrintable` (`boolean`): True when all bytes in `s` are printable ASCII.
+- `isAlnum` (`boolean`): True when `s` is non-empty and all characters are
+ alphanumeric.
**Example**:
```lua
-ok = isprintable("abc!") --> true
+ok = isalnum("abc123") --> true
```
> [!NOTE]
>
-> The empty string returns `true`.
+> Lua letters are ASCII by default, so non-ASCII letters are not alphanumeric.
+>
+> ```lua
+> isalnum("á1") --> false
+> ```
-
+
-#### `isspace(s)`
+#### `isalpha(s)`
-Return true if all characters are whitespace and string is non-empty.
+Return true if all characters are alphabetic and string is non-empty.
**Parameters**:
@@ -478,20 +551,28 @@ Return true if all characters are whitespace and string is non-empty.
**Return**:
-- `isSpace` (`boolean`): True when `s` is non-empty and all characters are
- whitespace.
+- `isAlpha` (`boolean`): True when `s` is non-empty and all characters are
+ alphabetic.
**Example**:
```lua
-ok = isspace(" \t") --> true
+ok = isalpha("abc") --> true
```
-
+> [!NOTE]
+>
+> Lua letters are ASCII by default, so non-ASCII letters are not alphabetic.
+>
+> ```lua
+> isalpha("á") --> false
+> ```
-#### `istitle(s)`
+
-Return true if string is titlecased.
+#### `isascii(s)`
+
+Return true if all characters are ASCII.
**Parameters**:
@@ -499,20 +580,23 @@ Return true if string is titlecased.
**Return**:
-- `isTitle` (`boolean`): True when `s` is titlecased.
+- `isAscii` (`boolean`): True when all bytes in `s` are ASCII.
**Example**:
```lua
-ok = istitle("Hello World") --> true
+ok = isascii("hello") --> true
```
-
+> [!NOTE]
+>
+> The empty string returns `true`.
-#### `isupper(s)`
+
-Return true if all cased characters are uppercase and there is at least one
-cased character.
+#### `isdecimal(s)`
+
+Return true if all characters are decimal characters and string is non-empty.
**Parameters**:
@@ -520,65 +604,65 @@ cased character.
**Return**:
-- `isUpper` (`boolean`): True when `s` has at least one cased character and all
- are uppercase.
+- `isDecimal` (`boolean`): True when `s` is non-empty and all characters are
+ decimal digits.
**Example**:
```lua
-ok = isupper("HELLO") --> true
+ok = isdecimal("123") --> true
```
-### Layout
-
-
+
-#### `join(sep, ls)`
+#### `isidentifier(s)`
-Join an array-like table of strings using this string as separator.
+Return true if string is a valid identifier and not a reserved keyword.
**Parameters**:
-- `sep` (`string`): Separator value.
-- `ls` (`string[]`): Table value.
+- `s` (`string`): Input string.
**Return**:
-- `joined` (`string`): Joined string.
+- `isIdentifier` (`boolean`): True when `s` is a valid identifier and not a
+ keyword.
**Example**:
```lua
-s = join(",", { "a", "b", "c" }) --> "a,b,c"
+ok = isidentifier("foo_bar") --> true
+ok = isidentifier("2var") --> false
+ok = isidentifier("end") --> false (keyword)
```
-
+
-#### `ljust(s, width, fillchar?)`
+#### `islower(s)`
-Left-justify string in a field of given width.
+Return true if all cased characters are lowercase and there is at least one
+cased character.
**Parameters**:
- `s` (`string`): Input string.
-- `width` (`integer`): Target width.
-- `fillchar?` (`string`): Optional fill character.
**Return**:
-- `leftJustified` (`string`): Left-justified string.
+- `isLower` (`boolean`): True when `s` has at least one cased character and all
+ are lowercase.
**Example**:
```lua
-s = ljust("hi", 5, ".") --> "hi..."
+ok = islower("hello") --> true
```
-
+
-#### `lower(s)`
+#### `isprintable(s)`
-Return lowercased copy.
+Return true if all characters are printable.
**Parameters**:
@@ -586,75 +670,79 @@ Return lowercased copy.
**Return**:
-- `lowercased` (`string`): Lowercased string.
+- `isPrintable` (`boolean`): True when all bytes in `s` are printable ASCII.
**Example**:
```lua
-s = lower("HeLLo") --> "hello"
+ok = isprintable("abc!") --> true
```
-
+> [!NOTE]
+>
+> The empty string returns `true`.
-#### `lstrip(s, chars?)`
+
-Remove leading characters (default: whitespace).
+#### `isspace(s)`
+
+Return true if all characters are whitespace and string is non-empty.
**Parameters**:
- `s` (`string`): Input string.
-- `chars?` (`string`): Optional character set.
**Return**:
-- `leadingStripped` (`string`): String with leading characters removed.
+- `isSpace` (`boolean`): True when `s` is non-empty and all characters are
+ whitespace.
**Example**:
```lua
-s = lstrip(" hello") --> "hello"
+ok = isspace(" \t") --> true
```
-
+
-#### `rstrip(s, chars?)`
+#### `istitle(s)`
-Remove trailing characters (default: whitespace).
+Return true if string is titlecased.
**Parameters**:
- `s` (`string`): Input string.
-- `chars?` (`string`): Optional character set.
**Return**:
-- `trailingStripped` (`string`): String with trailing characters removed.
+- `isTitle` (`boolean`): True when `s` is titlecased.
**Example**:
```lua
-s = rstrip("hello ") --> "hello"
+ok = istitle("Hello World") --> true
```
-
+
-#### `strip(s, chars?)`
+#### `isupper(s)`
-Remove leading and trailing characters (default: whitespace).
+Return true if all cased characters are uppercase and there is at least one
+cased character.
**Parameters**:
- `s` (`string`): Input string.
-- `chars?` (`string`): Optional character set.
**Return**:
-- `stripped` (`string`): String with leading and trailing characters removed.
+- `isUpper` (`boolean`): True when `s` has at least one cased character and all
+ are uppercase.
**Example**:
```lua
-s = strip(" hello ") --> "hello"
+ok = isupper("HELLO") --> true
```
### Split & Replace
@@ -902,135 +990,3 @@ Split on line boundaries.
```lua
lines = splitlines("a\nb\r\nc") --> { "a", "b", "c" }
```
-
-### Casing & Transform
-
-
-
-#### `swapcase(s)`
-
-Return a copy with case of alphabetic characters swapped.
-
-**Parameters**:
-
-- `s` (`string`): Input string.
-
-**Return**:
-
-- `swappedCase` (`string`): String with alphabetic case swapped.
-
-**Example**:
-
-```lua
-s = swapcase("AbC") --> "aBc"
-```
-
-
-
-#### `startswith(s, prefix, start?, stop?)`
-
-Return true if string starts with prefix.
-
-**Parameters**:
-
-- `s` (`string`): Input string.
-- `prefix` (`string|string[]`): Prefix string.
-- `start?` (`integer`): Optional start index (defaults to `1`).
-- `stop?` (`integer`): Optional exclusive end index (defaults to `#s + 1`).
-
-**Return**:
-
-- `hasPrefix` (`boolean`): True when `s` starts with `prefix`.
-
-**Example**:
-
-```lua
-ok = startswith("hello.lua", "he") --> true
-```
-
-> [!NOTE]
->
-> If prefix is a list, returns `true` when any prefix matches.
-
-
-
-#### `title(s)`
-
-Return titlecased copy.
-
-**Parameters**:
-
-- `s` (`string`): Input string.
-
-**Return**:
-
-- `titlecased` (`string`): Titlecased string.
-
-**Example**:
-
-```lua
-s = title("hello world") --> "Hello World"
-```
-
-
-
-#### `translate(s, table_map)`
-
-Translate characters using a mapping table.
-
-**Parameters**:
-
-- `s` (`string`): Input string.
-- `table_map` (`table`): Character translation map.
-
-**Return**:
-
-- `translated` (`string`): Translated string.
-
-**Example**:
-
-```lua
-map = { [string.byte("a")] = "b", ["c"] = false }
-s = translate("abc", map) --> "bb"
-```
-
-
-
-#### `upper(s)`
-
-Return uppercased copy.
-
-**Parameters**:
-
-- `s` (`string`): Input string.
-
-**Return**:
-
-- `uppercased` (`string`): Uppercased string.
-
-**Example**:
-
-```lua
-s = upper("Hello") --> "HELLO"
-```
-
-
-
-#### `zfill(s, width)`
-
-Pad numeric string on the left with zeros.
-
-**Parameters**:
-
-- `s` (`string`): Input string.
-- `width` (`integer`): Target width.
-
-**Return**:
-
-- `zeroFilled` (`string`): Zero-padded string.
-
-**Example**:
-
-```lua
-s = zfill("42", 5) --> "00042"
-```
diff --git a/docs/src/modules/stringcase.md b/docs/src/modules/stringcase.md
index 68aa650..e0b7790 100644
--- a/docs/src/modules/stringcase.md
+++ b/docs/src/modules/stringcase.md
@@ -23,29 +23,29 @@ print(stringcase.snake("FooBar")) --> "foo_bar"
| [`lower(s)`](#fn-lower) | Convert string to all lowercase. |
| [`upper(s)`](#fn-upper) | Convert string to all uppercase. |
+**Letter Case**:
+
+| Function | Description |
+| ----------------------------- | ------------------------------------------------------------------------- |
+| [`capital(s)`](#fn-capital) | Capitalize the first letter and lowercase the rest. |
+| [`sentence(s)`](#fn-sentence) | Convert string to sentence case (first letter uppercase, rest unchanged). |
+| [`swap(s)`](#fn-swap) | Swap case of each letter. |
+
**Word Case**:
| Function | Description |
| --------------------------------- | --------------------------------------------------------------------- |
-| [`snake(s)`](#fn-snake) | Convert string to snake_case. |
-| [`camel(s)`](#fn-camel) | Convert string to camelCase. |
-| [`replace(s, sep?)`](#fn-replace) | Normalize to snake_case, then replace underscores with a separator. |
| [`acronym(s)`](#fn-acronym) | Get acronym of words in string (first letters only). |
-| [`title(s)`](#fn-title) | Convert string to Title Case (first letter of each word capitalized). |
+| [`camel(s)`](#fn-camel) | Convert string to camelCase. |
| [`constant(s)`](#fn-constant) | Convert string to CONSTANT_CASE (uppercase snake_case). |
-| [`pascal(s)`](#fn-pascal) | Convert string to PascalCase. |
-| [`kebab(s)`](#fn-kebab) | Convert string to kebab-case. |
| [`dot(s)`](#fn-dot) | Convert string to dot.case. |
-| [`space(s)`](#fn-space) | Convert string to space case (spaces between words). |
+| [`kebab(s)`](#fn-kebab) | Convert string to kebab-case. |
+| [`pascal(s)`](#fn-pascal) | Convert string to PascalCase. |
| [`path(s)`](#fn-path) | Convert string to path/case (slashes between words). |
-
-**Letter Case**:
-
-| Function | Description |
-| ----------------------------- | ------------------------------------------------------------------------- |
-| [`swap(s)`](#fn-swap) | Swap case of each letter. |
-| [`capital(s)`](#fn-capital) | Capitalize the first letter and lowercase the rest. |
-| [`sentence(s)`](#fn-sentence) | Convert string to sentence case (first letter uppercase, rest unchanged). |
+| [`replace(s, sep?)`](#fn-replace) | Normalize to snake_case, then replace underscores with a separator. |
+| [`snake(s)`](#fn-snake) | Convert string to snake_case. |
+| [`space(s)`](#fn-space) | Convert string to space case (spaces between words). |
+| [`title(s)`](#fn-title) | Convert string to Title Case (first letter of each word capitalized). |
### Basic
@@ -91,13 +91,13 @@ upper("foo_bar-baz") --> "FOO_BAR-BAZ"
upper("FooBar baz") --> "FOOBAR BAZ"
```
-### Word Case
+### Letter Case
-
+
-#### `snake(s)`
+#### `capital(s)`
-Convert string to snake_case.
+Capitalize the first letter and lowercase the rest.
**Parameters**:
@@ -105,20 +105,20 @@ Convert string to snake_case.
**Return**:
-- `snakeCased` (`string`): Snake-cased string.
+- `capitalized` (`string`): Capitalized string.
**Example**:
```lua
-snake("foo_bar-baz") --> "foo_bar_baz"
-snake("FooBar baz") --> "foo_bar_baz"
+capital("foo_bar-baz") --> "Foo_bar-baz"
+capital("FooBar baz") --> "Foobar baz"
```
-
+
-#### `camel(s)`
+#### `sentence(s)`
-Convert string to camelCase.
+Convert string to sentence case (first letter uppercase, rest unchanged).
**Parameters**:
@@ -126,37 +126,38 @@ Convert string to camelCase.
**Return**:
-- `camelCased` (`string`): Camel-cased string.
+- `sentenceCased` (`string`): Sentence-cased string.
**Example**:
```lua
-camel("foo_bar-baz") --> "fooBarBaz"
-camel("FooBar baz") --> "fooBarBaz"
+sentence("foo_bar-baz") --> "Foo_bar-baz"
+sentence("FooBar baz") --> "FooBar baz"
```
-
+
-#### `replace(s, sep?)`
+#### `swap(s)`
-Normalize to snake_case, then replace underscores with a separator.
+Swap case of each letter.
**Parameters**:
- `s` (`string`): Input string.
-- `sep?` (`string`): Optional separator value (defaults to `""`).
**Return**:
-- `replaced` (`string`): String with underscores replaced by `sep`.
+- `swapCased` (`string`): Swap-cased string.
**Example**:
```lua
-replace("foo_bar-baz", "-") --> "foo-bar-baz"
-replace("FooBar baz", "-") --> "foo-bar-baz"
+swap("foo_bar-baz") --> "FOO_BAR-BAZ"
+swap("FooBar baz") --> "fOObAR BAZ"
```
+### Word Case
+
#### `acronym(s)`
@@ -178,11 +179,11 @@ acronym("foo_bar-baz") --> "FBB"
acronym("FooBar baz") --> "FBB"
```
-
+
-#### `title(s)`
+#### `camel(s)`
-Convert string to Title Case (first letter of each word capitalized).
+Convert string to camelCase.
**Parameters**:
@@ -190,13 +191,13 @@ Convert string to Title Case (first letter of each word capitalized).
**Return**:
-- `titleCased` (`string`): Title-cased string.
+- `camelCased` (`string`): Camel-cased string.
**Example**:
```lua
-title("foo_bar-baz") --> "Foo Bar Baz"
-title("FooBar baz") --> "Foo Bar Baz"
+camel("foo_bar-baz") --> "fooBarBaz"
+camel("FooBar baz") --> "fooBarBaz"
```
@@ -220,11 +221,11 @@ constant("foo_bar-baz") --> "FOO_BAR_BAZ"
constant("FooBar baz") --> "FOO_BAR_BAZ"
```
-
+
-#### `pascal(s)`
+#### `dot(s)`
-Convert string to PascalCase.
+Convert string to dot.case.
**Parameters**:
@@ -232,13 +233,13 @@ Convert string to PascalCase.
**Return**:
-- `pascalCased` (`string`): Pascal-cased string.
+- `dotCased` (`string`): Dot-cased string.
**Example**:
```lua
-pascal("foo_bar-baz") --> "FooBarBaz"
-pascal("FooBar baz") --> "FooBarBaz"
+dot("foo_bar-baz") --> "foo.bar.baz"
+dot("FooBar baz") --> "foo.bar.baz"
```
@@ -262,11 +263,11 @@ kebab("foo_bar-baz") --> "foo-bar-baz"
kebab("FooBar baz") --> "foo-bar-baz"
```
-
+
-#### `dot(s)`
+#### `pascal(s)`
-Convert string to dot.case.
+Convert string to PascalCase.
**Parameters**:
@@ -274,20 +275,20 @@ Convert string to dot.case.
**Return**:
-- `dotCased` (`string`): Dot-cased string.
+- `pascalCased` (`string`): Pascal-cased string.
**Example**:
```lua
-dot("foo_bar-baz") --> "foo.bar.baz"
-dot("FooBar baz") --> "foo.bar.baz"
+pascal("foo_bar-baz") --> "FooBarBaz"
+pascal("FooBar baz") --> "FooBarBaz"
```
-
+
-#### `space(s)`
+#### `path(s)`
-Convert string to space case (spaces between words).
+Convert string to path/case (slashes between words).
**Parameters**:
@@ -295,43 +296,42 @@ Convert string to space case (spaces between words).
**Return**:
-- `spaceCased` (`string`): Space-cased string.
+- `pathCased` (`string`): Path-cased string.
**Example**:
```lua
-space("foo_bar-baz") --> "foo bar baz"
-space("FooBar baz") --> "foo bar baz"
+path("foo_bar-baz") --> "foo/bar/baz"
+path("FooBar baz") --> "foo/bar/baz"
```
-
+
-#### `path(s)`
+#### `replace(s, sep?)`
-Convert string to path/case (slashes between words).
+Normalize to snake_case, then replace underscores with a separator.
**Parameters**:
- `s` (`string`): Input string.
+- `sep?` (`string`): Optional separator value (defaults to `""`).
**Return**:
-- `pathCased` (`string`): Path-cased string.
+- `replaced` (`string`): String with underscores replaced by `sep`.
**Example**:
```lua
-path("foo_bar-baz") --> "foo/bar/baz"
-path("FooBar baz") --> "foo/bar/baz"
+replace("foo_bar-baz", "-") --> "foo-bar-baz"
+replace("FooBar baz", "-") --> "foo-bar-baz"
```
-### Letter Case
-
-
+
-#### `swap(s)`
+#### `snake(s)`
-Swap case of each letter.
+Convert string to snake_case.
**Parameters**:
@@ -339,20 +339,20 @@ Swap case of each letter.
**Return**:
-- `swapCased` (`string`): Swap-cased string.
+- `snakeCased` (`string`): Snake-cased string.
**Example**:
```lua
-swap("foo_bar-baz") --> "FOO_BAR-BAZ"
-swap("FooBar baz") --> "fOObAR BAZ"
+snake("foo_bar-baz") --> "foo_bar_baz"
+snake("FooBar baz") --> "foo_bar_baz"
```
-
+
-#### `capital(s)`
+#### `space(s)`
-Capitalize the first letter and lowercase the rest.
+Convert string to space case (spaces between words).
**Parameters**:
@@ -360,20 +360,20 @@ Capitalize the first letter and lowercase the rest.
**Return**:
-- `capitalized` (`string`): Capitalized string.
+- `spaceCased` (`string`): Space-cased string.
**Example**:
```lua
-capital("foo_bar-baz") --> "Foo_bar-baz"
-capital("FooBar baz") --> "Foobar baz"
+space("foo_bar-baz") --> "foo bar baz"
+space("FooBar baz") --> "foo bar baz"
```
-
+
-#### `sentence(s)`
+#### `title(s)`
-Convert string to sentence case (first letter uppercase, rest unchanged).
+Convert string to Title Case (first letter of each word capitalized).
**Parameters**:
@@ -381,11 +381,11 @@ Convert string to sentence case (first letter uppercase, rest unchanged).
**Return**:
-- `sentenceCased` (`string`): Sentence-cased string.
+- `titleCased` (`string`): Title-cased string.
**Example**:
```lua
-sentence("foo_bar-baz") --> "Foo_bar-baz"
-sentence("FooBar baz") --> "FooBar baz"
+title("foo_bar-baz") --> "Foo Bar Baz"
+title("FooBar baz") --> "Foo Bar Baz"
```
diff --git a/docs/src/modules/tbl.md b/docs/src/modules/tbl.md
index 4db7bbe..b70dcdd 100644
--- a/docs/src/modules/tbl.md
+++ b/docs/src/modules/tbl.md
@@ -17,29 +17,37 @@ print(tbl.count({ a = 1, b = 2 })) --> 2
## Functions
-**Basics**:
+**Copies**:
+
+| Function | Description |
+| ----------------------------- | ----------------------------------- |
+| [`copy(t)`](#fn-copy) | Create a shallow copy of the table. |
+| [`deepcopy(v)`](#fn-deepcopy) | Create a deep copy of a value. |
+
+**Core Utilities**:
| Function | Description |
| ----------------------- | --------------------------------------- |
| [`clear(t)`](#fn-clear) | Remove all entries from the table. |
| [`count(t)`](#fn-count) | Return the number of keys in the table. |
-**Copying**:
+**Iterators**:
-| Function | Description |
-| ----------------------------- | ----------------------------------- |
-| [`copy(t)`](#fn-copy) | Create a shallow copy of the table. |
-| [`deepcopy(v)`](#fn-deepcopy) | Create a deep copy of a value. |
+| Function | Description |
+| ------------------------------- | -------------------------------------------- |
+| [`foreach(t, fn)`](#fn-foreach) | Call a function for each value in the table. |
+| [`spairs(t)`](#fn-spairs) | Iterate key-value pairs in sorted key order. |
-**Query**:
+**Queries**:
-| Function | Description |
-| --------------------------------- | ---------------------------------------------------------------- |
-| [`filter(t, pred)`](#fn-filter) | Filter entries by a value predicate. |
-| [`find(t, v)`](#fn-find) | Find the first key whose value equals the given value. |
-| [`same(a, b)`](#fn-same) | Return `true` if two tables have the same keys and equal values. |
-| [`find_if(t, pred)`](#fn-find-if) | Find first value and key matching predicate. |
-| [`get(t, ...)`](#fn-get) | Safely get nested value by keys. |
+| Function | Description |
+| ------------------------------------ | ---------------------------------------------------------------- |
+| [`deep_equal(a, b)`](#fn-deep-equal) | Return `true` if two tables are deeply equal. |
+| [`filter(t, pred)`](#fn-filter) | Filter entries by a value predicate. |
+| [`find(t, v)`](#fn-find) | Find the first key whose value equals the given value. |
+| [`find_if(t, pred)`](#fn-find-if) | Find first value and key matching predicate. |
+| [`get(t, ...)`](#fn-get) | Safely get nested value by keys. |
+| [`is_same(a, b)`](#fn-is-same) | Return `true` if two tables have the same keys and equal values. |
**Transforms**:
@@ -53,16 +61,57 @@ print(tbl.count({ a = 1, b = 2 })) --> 2
| [`update(t1, t2)`](#fn-update) | Merge entries from `t2` into `t1` and return `t1`. |
| [`values(t)`](#fn-values) | Return a list of all values in the table. |
-**Iteration**:
+### Copies
-| Function | Description |
-| ------------------------------- | -------------------------------------------- |
-| [`foreach(t, fn)`](#fn-foreach) | Call a function for each value in the table. |
-| [`spairs(t)`](#fn-spairs) | Iterate key-value pairs in sorted key order. |
+
+
+#### `copy(t)`
+
+Create a shallow copy of the table.
-### Basics
+**Parameters**:
-Core table utilities for clearing and counting.
+- `t` (`T`): Source table.
+
+**Return**:
+
+- `copy` (`T`): Shallow-copied table.
+
+**Example**:
+
+```lua
+t = copy({ a = 1, b = 2 }) --> { a = 1, b = 2 }
+```
+
+
+
+#### `deepcopy(v)`
+
+Create a deep copy of a value.
+
+**Parameters**:
+
+- `v` (`T`): Input value.
+
+**Return**:
+
+- `copiedValue` (`T`): Deep-copied value.
+
+**Example**:
+
+```lua
+t = deepcopy({ a = { b = 1 } }) --> { a = { b = 1 } }
+n = deepcopy(42) --> 42
+```
+
+> [!NOTE]
+>
+> If `v` is a table, all nested tables are copied recursively; other types are
+> returned as-is.
+
+### Core Utilities
+
+
#### `clear(t)`
@@ -103,57 +152,80 @@ Return the number of keys in the table.
n = count({ a = 1, b = 2 }) --> 2
```
-### Copying
+### Iterators
-Shallow and deep copy helpers.
+
-#### `copy(t)`
+#### `foreach(t, fn)`
-Create a shallow copy of the table.
+Call a function for each value in the table.
**Parameters**:
-- `t` (`T`): Source table.
+- `t` (`table`): Input table.
+- `fn` (`fun(value:V, key:K)`): Function invoked for each entry.
**Return**:
-- `copy` (`T`): Shallow-copied table.
+- `none` (`nil`)
**Example**:
```lua
-t = copy({ a = 1, b = 2 }) --> { a = 1, b = 2 }
+foreach({ a = 1, b = 2 }, function(v, k)
+ print(k, v)
+end)
```
-
+
-#### `deepcopy(v)`
+#### `spairs(t)`
-Create a deep copy of a value.
+Iterate key-value pairs in sorted key order.
**Parameters**:
-- `v` (`T`): Input value.
+- `t` (`T`): Input table.
**Return**:
-- `copiedValue` (`T`): Deep-copied value.
+- `iterator` (`fun(table: table, index?: K):(K, V)`): Sorted pairs
+ iterator.
+- **value** (`T`)
**Example**:
```lua
-t = deepcopy({ a = { b = 1 } }) --> { a = { b = 1 } }
-n = deepcopy(42) --> 42
+for k, v in spairs({ b = 2, a = 1 }) do
+ print(k, v)
+end
```
-> [!NOTE]
->
-> If `v` is a table, all nested tables are copied recursively; other types are
-> returned as-is.
+### Queries
+
+
-### Query
+#### `deep_equal(a, b)`
-Read-only lookup and selection helpers.
+Return `true` if two tables are deeply equal.
+
+**Parameters**:
+
+- `a` (`table`): Left table.
+- `b` (`table`): Right table.
+
+**Return**:
+
+- `isDeepEqual` (`boolean`): True when both tables are recursively equal.
+
+**Example**:
+
+```lua
+ok = deep_equal({ a = { b = 1 } }, { a = { b = 1 } }) --> true
+ok = deep_equal({ a = { b = 1 } }, { a = { b = 2 } }) --> false
+```
+
+
#### `filter(t, pred)`
@@ -197,28 +269,6 @@ Find the first key whose value equals the given value.
key = find({ a = 1, b = 2, c = 2 }, 2) --> "b" or "c"
```
-
-
-#### `same(a, b)`
-
-Return `true` if two tables have the same keys and equal values.
-
-**Parameters**:
-
-- `a` (`table`): Left table.
-- `b` (`table`): Right table.
-
-**Return**:
-
-- `isSame` (`boolean`): True when both tables have the same keys and values.
-
-**Example**:
-
-```lua
-ok = same({ a = 1, b = 2 }, { b = 2, a = 1 }) --> true
-ok = same({ a = {} }, { a = {} }) --> false
-```
-
#### `find_if(t, pred)`
@@ -270,9 +320,31 @@ v2 = get(t) --> { a = { b = { c = 1 } } }
>
> If no keys are provided, returns the input table.
+
+
+#### `is_same(a, b)`
+
+Return `true` if two tables have the same keys and equal values.
+
+**Parameters**:
+
+- `a` (`table`): Left table.
+- `b` (`table`): Right table.
+
+**Return**:
+
+- `isSame` (`boolean`): True when both tables have the same keys and values.
+
+**Example**:
+
+```lua
+ok = is_same({ a = 1, b = 2 }, { b = 2, a = 1 }) --> true
+ok = is_same({ a = {} }, { a = {} }) --> false
+```
+
### Transforms
-Table transformation and conversion utilities.
+
#### `invert(t)`
@@ -423,52 +495,3 @@ Return a list of all values in the table.
```lua
vals = values({ a = 1, b = 2 }) --> { 1, 2 }
```
-
-### Iteration
-
-Iterators and ordered traversal helpers.
-
-#### `foreach(t, fn)`
-
-Call a function for each value in the table.
-
-**Parameters**:
-
-- `t` (`table`): Input table.
-- `fn` (`fun(value:V, key:K)`): Function invoked for each entry.
-
-**Return**:
-
-- `none` (`nil`)
-
-**Example**:
-
-```lua
-foreach({ a = 1, b = 2 }, function(v, k)
- print(k, v)
-end)
-```
-
-
-
-#### `spairs(t)`
-
-Iterate key-value pairs in sorted key order.
-
-**Parameters**:
-
-- `t` (`T`): Input table.
-
-**Return**:
-
-- `iterator` (`fun(table: table, index?: K):(K, V)`): Sorted pairs
- iterator.
-- **value** (`T`)
-
-**Example**:
-
-```lua
-for k, v in spairs({ b = 2, a = 1 }) do
- print(k, v)
-end
-```
diff --git a/docs/src/modules/utils.md b/docs/src/modules/utils.md
index e7fa93e..aefed80 100644
--- a/docs/src/modules/utils.md
+++ b/docs/src/modules/utils.md
@@ -16,40 +16,57 @@ print(utils.quote('hello "world"')) --> 'hello "world"'
## Functions
-| Function | Description |
-| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
-| [`quote(v)`](#fn-quote) | Smart-quote a string for readable Lua-like output. |
-| [`keypath(...)`](#fn-keypath) | Format a key chain as a Lua-like table access path. |
-| [`args_repr(v)`](#fn-args-repr) | Format a list-like table as a comma-separated argument string. |
-| [`assert_arg(argn, v, validator?, optional?, msg?)`](#fn-assert-arg) | Assert argument value using [`mods.validate`](/modules/validate) and raise a Lua error on failure. |
-| [`validate(name, v, validator?, optional?, msg?)`](#fn-validate) | Validate a value using [`mods.validate`](/modules/validate) and raise a Lua error on failure. |
-| [`validate(path, v, validator?, optional?, msg?)`](#fn-validate) | Validate a value using [`mods.validate`](/modules/validate) and raise a Lua error on failure. |
-| [`lazy_module(name, err?)`](#fn-lazy-module) | Return a lazy proxy for a module. |
+**Formatting**:
-
+| Function | Description |
+| ------------------------------- | -------------------------------------------------------------- |
+| [`args_repr(v)`](#fn-args-repr) | Format a list-like table as a comma-separated argument string. |
+| [`keypath(...)`](#fn-keypath) | Format a key chain as a Lua-like table access path. |
+| [`quote(v)`](#fn-quote) | Smart-quote a string for readable Lua-like output. |
-### `quote(v)`
+**Lazy Loading**:
-Smart-quote a string for readable Lua-like output.
+| Function | Description |
+| -------------------------------------------- | --------------------------------- |
+| [`lazy_module(name, err?)`](#fn-lazy-module) | Return a lazy proxy for a module. |
+
+**Validation**:
+
+| Function | Description |
+| ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
+| [`assert_arg(argn, v, validator?, optional?, lv?)`](#fn-assert-arg) | Assert argument value using [`mods.validate`](/modules/validate) and raise a Lua error on failure. |
+| [`validate(name, v, validator?, optional?, msg?)`](#fn-validate) | Validate a value using [`mods.validate`](/modules/validate) and raise a Lua error on failure. |
+| [`validate(path, v, validator?, optional?, msg?)`](#fn-validate) | Validate a value using [`mods.validate`](/modules/validate) and raise a Lua error on failure. |
+
+### Formatting
+
+
+
+#### `args_repr(v)`
+
+Format a list-like table as a comma-separated argument string.
**Parameters**:
-- `v` (`string`): String to quote.
+- `v` (`any`): Value to format. `nil` returns an empty string.
**Return**:
-- `out` (`string`): Quoted string.
+- `out` (`string`): Argument list string.
**Example**:
```lua
-print(utils.quote('He said "hi"')) -- 'He said "hi"'
-print(utils.quote('say "hi" and \\'bye\\'')) -- "say \"hi\" and 'bye'"
+utils.args_repr({ "a", 1, true }) --> '"a", 1, true'
```
+> [!NOTE]
+>
+> Requires [`inspect`](https://github.com/kikito/inspect.lua)
+
-### `keypath(...)`
+#### `keypath(...)`
Format a key chain as a Lua-like table access path.
@@ -70,33 +87,66 @@ p3 = utils.keypath("ctx", "invalid-key") --> 'ctx["invalid-key"]'
p4 = utils.keypath() --> ""
```
-
+
-### `args_repr(v)`
+#### `quote(v)`
-Format a list-like table as a comma-separated argument string.
+Smart-quote a string for readable Lua-like output.
**Parameters**:
-- `v` (`table|any`): Value to format.
+- `v` (`string`): String to quote.
**Return**:
-- `out` (`string`): Argument list string.
+- `out` (`string`): Quoted string.
**Example**:
```lua
-utils.args_repr({ "a", 1, true }) --> '"a", 1, true'
+print(utils.quote('He said "hi"')) -- 'He said "hi"'
+print(utils.quote('say "hi" and \\'bye\\'')) -- "say \"hi\" and 'bye'"
+```
+
+### Lazy Loading
+
+
+
+#### `lazy_module(name, err?)`
+
+Return a lazy proxy for a module.
+
+The proxy rewrites its metamethods after first access while keeping the proxy
+table itself free of cached fields.
+
+**Parameters**:
+
+- `name` (`string`): Module name passed to `require`.
+- `err?` (`string`): Optional error message raised when loading fails.
+
+**Return**:
+
+- `module` (`{}`): Lazy proxy for the loaded module.
+
+**Example**:
+
+```lua
+local fs = utils.lazy_module("mods.fs")
+print(fs.exists("README.md"))
+
+local repr = utils.lazy_module("mods.repr")
+print(repr({ a = 1 }))
```
> [!NOTE]
>
-> Requires [`inspect`](https://github.com/kikito/inspect.lua)
+> Supports both table-returning modules and function-returning modules.
+
+### Validation
-### `assert_arg(argn, v, validator?, optional?, msg?)`
+#### `assert_arg(argn, v, validator?, optional?, lv?)`
Assert argument value using [`mods.validate`](/modules/validate) and raise a Lua
error on failure.
@@ -107,8 +157,7 @@ error on failure.
- `v` (`T`): Value to check.
- `validator?` (`modsValidatorName`): Validator name (defaults to `"truthy"`).
- `optional?` (`boolean`): Skip errors when `v` is `nil` (defaults to `false`).
-- `msg?` (`string`): Optional override template passed to
- [`mods.validate`](/modules/validate).
+- `lv?` (`integer`): Error level passed to `error` (defaults to `3`).
**Return**:
@@ -132,7 +181,7 @@ utils.assert_arg(3, "x", "number", false, "need {{expected}}, got {{got}}")
-### `validate(name, v, validator?, optional?, msg?)`
+#### `validate(name, v, validator?, optional?, msg?)`
Validate a value using [`mods.validate`](/modules/validate) and raise a Lua
error on failure.
@@ -161,7 +210,7 @@ utils.validate("count", "x", "number")
-### `validate(path, v, validator?, optional?, msg?)`
+#### `validate(path, v, validator?, optional?, msg?)`
Validate a value using [`mods.validate`](/modules/validate) and raise a Lua
error on failure.
@@ -191,35 +240,3 @@ utils.validate({ "ctx", "users", 1, "name" }, 123, "string")
>
> On failure, `path` is rendered with
> [`mods.utils.keypath`](/modules/utils#fn-keypath).
-
-
-
-### `lazy_module(name, err?)`
-
-Return a lazy proxy for a module.
-
-The proxy rewrites its metamethods after first access while keeping the proxy
-table itself free of cached fields.
-
-**Parameters**:
-
-- `name` (`string`): Module name passed to `require`.
-- `err?` (`string`): Optional error message raised when loading fails.
-
-**Return**:
-
-- `module` (`{}`): Lazy proxy for the loaded module.
-
-**Example**:
-
-```lua
-local fs = utils.lazy_module("mods.fs")
-print(fs.exists("README.md"))
-
-local repr = utils.lazy_module("mods.repr")
-print(repr({ a = 1 }))
-```
-
-> [!NOTE]
->
-> Supports both table-returning modules and function-returning modules.
diff --git a/docs/src/modules/validate.md b/docs/src/modules/validate.md
index f904c2d..196af83 100644
--- a/docs/src/modules/validate.md
+++ b/docs/src/modules/validate.md
@@ -26,6 +26,12 @@ validate(1) --> true, nil
validate(1, "nil") --> false, "nil expected, got number"
```
+> [!IMPORTANT]
+>
+> Path checks require **LuaFileSystem**
+> ([`lfs`](https://github.com/lunarmodules/luafilesystem)) and raise an error if
+> it is not installed.
+
## Validator Names
Validator names are case-insensitive for field access.
@@ -57,6 +63,26 @@ validate.string(123, "want {{expected}}, got {{got}}")
## Functions
+**Path Checks**:
+
+| Function | Description |
+| ------------------------------- | ------------------------------------------------------------------------------------------------------- |
+| [`block(v, msg?)`](#fn-block) | Returns `true` when `v` is a block device path. Otherwise returns `false` and an error message. |
+| [`char(v, msg?)`](#fn-char) | Returns `true` when `v` is a char device path. Otherwise returns `false` and an error message. |
+| [`device(v, msg?)`](#fn-device) | Returns `true` when `v` is a block or char device path. Otherwise returns `false` and an error message. |
+| [`dir(v, msg?)`](#fn-dir) | Returns `true` when `v` is a directory path. Otherwise returns `false` and an error message. |
+| [`fifo(v, msg?)`](#fn-fifo) | Returns `true` when `v` is a FIFO path. Otherwise returns `false` and an error message. |
+| [`file(v, msg?)`](#fn-file) | Returns `true` when `v` is a file path. Otherwise returns `false` and an error message. |
+| [`link(v, msg?)`](#fn-link) | Returns `true` when `v` is a symlink path. Otherwise returns `false` and an error message. |
+| [`path(v, msg?)`](#fn-path) | Returns `true` when `v` is a valid filesystem path. Otherwise returns `false` and an error message. |
+| [`socket(v, msg?)`](#fn-socket) | Returns `true` when `v` is a socket path. Otherwise returns `false` and an error message. |
+
+**Registration**:
+
+| Function | Description |
+| ------------------------------------------------------ | -------------------------------------------------- |
+| [`register(name, validator, template?)`](#fn-register) | Register or override a validator function by name. |
+
**Type Checks**:
| Function | Description |
@@ -74,41 +100,21 @@ validate.string(123, "want {{expected}}, got {{got}}")
| Function | Description |
| ----------------------------------- | ------------------------------------------------------------------------------------------- |
+| [`callable(v, msg?)`](#fn-callable) | Returns `true` when `v` is callable. Otherwise returns `false` and an error message. |
| [`false(v, msg?)`](#fn-false) | Returns `true` when `v` is exactly `false`. Otherwise returns `false` and an error message. |
-| [`true(v, msg?)`](#fn-true) | Returns `true` when `v` is exactly `true`. Otherwise returns `false` and an error message. |
| [`falsy(v, msg?)`](#fn-falsy) | Returns `true` when `v` is falsy. Otherwise returns `false` and an error message. |
-| [`callable(v, msg?)`](#fn-callable) | Returns `true` when `v` is callable. Otherwise returns `false` and an error message. |
| [`integer(v, msg?)`](#fn-integer) | Returns `true` when `v` is an integer. Otherwise returns `false` and an error message. |
+| [`true(v, msg?)`](#fn-true) | Returns `true` when `v` is exactly `true`. Otherwise returns `false` and an error message. |
| [`truthy(v, msg?)`](#fn-truthy) | Returns `true` when `v` is truthy. Otherwise returns `false` and an error message. |
-**Path Checks**:
-
-| Function | Description |
-| ------------------------------- | ------------------------------------------------------------------------------------------------------- |
-| [`path(v, msg?)`](#fn-path) | Returns `true` when `v` is a valid filesystem path. Otherwise returns `false` and an error message. |
-| [`block(v, msg?)`](#fn-block) | Returns `true` when `v` is a block device path. Otherwise returns `false` and an error message. |
-| [`char(v, msg?)`](#fn-char) | Returns `true` when `v` is a char device path. Otherwise returns `false` and an error message. |
-| [`device(v, msg?)`](#fn-device) | Returns `true` when `v` is a block or char device path. Otherwise returns `false` and an error message. |
-| [`dir(v, msg?)`](#fn-dir) | Returns `true` when `v` is a directory path. Otherwise returns `false` and an error message. |
-| [`fifo(v, msg?)`](#fn-fifo) | Returns `true` when `v` is a FIFO path. Otherwise returns `false` and an error message. |
-| [`file(v, msg?)`](#fn-file) | Returns `true` when `v` is a file path. Otherwise returns `false` and an error message. |
-| [`link(v, msg?)`](#fn-link) | Returns `true` when `v` is a symlink path. Otherwise returns `false` and an error message. |
-| [`socket(v, msg?)`](#fn-socket) | Returns `true` when `v` is a socket path. Otherwise returns `false` and an error message. |
-
-**Validator API**:
-
-| Function | Description |
-| ------------------------------------------------------ | -------------------------------------------------- |
-| [`register(name, validator, template?)`](#fn-register) | Register or override a validator function by name. |
-
-### Type Checks
+### Path Checks
-Basic Lua type validators (and their negated variants).
+
-#### `boolean(v, msg?)`
+#### `block(v, msg?)`
-Returns `true` when `v` is a boolean. Otherwise returns `false` and an error
-message.
+Returns `true` when `v` is a block device path. Otherwise returns `false` and an
+error message.
**Parameters**:
@@ -123,16 +129,15 @@ message.
**Example**:
```lua
-ok, err = validate.boolean(true) --> true, nil
-ok, err = validate.boolean(1) --> false, "boolean expected, got number"
+ok, err = validate.block(".")
```
-
+
-#### `function(v, msg?)`
+#### `char(v, msg?)`
-Returns `true` when `v` is a function. Otherwise returns `false` and an error
-message.
+Returns `true` when `v` is a char device path. Otherwise returns `false` and an
+error message.
**Parameters**:
@@ -147,17 +152,15 @@ message.
**Example**:
```lua
-ok, err = validate.Function(function() end) --> true, nil
-ok, err = validate.Function(1)
---> false, "function expected, got number"
+ok, err = validate.char(".")
```
-
+
-#### `nil(v, msg?)`
+#### `device(v, msg?)`
-Returns `true` when `v` is `nil`. Otherwise returns `false` and an error
-message.
+Returns `true` when `v` is a block or char device path. Otherwise returns
+`false` and an error message.
**Parameters**:
@@ -172,16 +175,15 @@ message.
**Example**:
```lua
-ok, err = validate.Nil(nil) --> true, nil
-ok, err = validate.Nil(0) --> false, "nil expected, got number"
+ok, err = validate.device(".")
```
-
+
-#### `number(v, msg?)`
+#### `dir(v, msg?)`
-Returns `true` when `v` is a number. Otherwise returns `false` and an error
-message.
+Returns `true` when `v` is a directory path. Otherwise returns `false` and an
+error message.
**Parameters**:
@@ -196,15 +198,14 @@ message.
**Example**:
```lua
-ok, err = validate.number(42) --> true, nil
-ok, err = validate.number("x") --> false, "number expected, got string"
+ok, err = validate.dir(".")
```
-
+
-#### `string(v, msg?)`
+#### `fifo(v, msg?)`
-Returns `true` when `v` is a string. Otherwise returns `false` and an error
+Returns `true` when `v` is a FIFO path. Otherwise returns `false` and an error
message.
**Parameters**:
@@ -220,15 +221,14 @@ message.
**Example**:
```lua
-ok, err = validate.string("hello") --> true, nil
-ok, err = validate.string(1) --> false, "string expected, got number"
+ok, err = validate.fifo(".")
```
-
+
-#### `table(v, msg?)`
+#### `file(v, msg?)`
-Returns `true` when `v` is a table. Otherwise returns `false` and an error
+Returns `true` when `v` is a file path. Otherwise returns `false` and an error
message.
**Parameters**:
@@ -244,16 +244,15 @@ message.
**Example**:
```lua
-ok, err = validate.table({}) --> true, nil
-ok, err = validate.table(1) --> false, "table expected, got number"
+ok, err = validate.file(".")
```
-
+
-#### `thread(v, msg?)`
+#### `link(v, msg?)`
-Returns `true` when `v` is a thread. Otherwise returns `false` and an error
-message.
+Returns `true` when `v` is a symlink path. Otherwise returns `false` and an
+error message.
**Parameters**:
@@ -268,17 +267,15 @@ message.
**Example**:
```lua
-co = coroutine.create(function() end)
-ok, err = validate.thread(co) --> true, nil
-ok, err = validate.thread(1) --> false, "thread expected, got number"
+ok, err = validate.link(".")
```
-
+
-#### `userdata(v, msg?)`
+#### `path(v, msg?)`
-Returns `true` when `v` is a userdata value. Otherwise returns `false` and an
-error message.
+Returns `true` when `v` is a valid filesystem path. Otherwise returns `false`
+and an error message.
**Parameters**:
@@ -293,19 +290,15 @@ error message.
**Example**:
```lua
-ok, err = validate.userdata(io.stdout) --> true, nil
-ok, err = validate.userdata(1) --> false, "userdata expected, got number"
+ok, err = validate.path("README.md")
```
-### Value Checks
-
-Value-state validators (exact true/false, truthy/falsy, callable, integer).
-
+
-#### `false(v, msg?)`
+#### `socket(v, msg?)`
-Returns `true` when `v` is exactly `false`. Otherwise returns `false` and an
-error message.
+Returns `true` when `v` is a socket path. Otherwise returns `false` and an error
+message.
**Parameters**:
@@ -320,39 +313,53 @@ error message.
**Example**:
```lua
-ok, err = validate.False(false) --> true, nil
-ok, err = validate.False(true) --> false, "false value expected, got true"
+ok, err = validate.socket(".")
```
-
+### Registration
-#### `true(v, msg?)`
+
-Returns `true` when `v` is exactly `true`. Otherwise returns `false` and an
-error message.
+#### `register(name, validator, template?)`
+
+Register or override a validator function by name.
**Parameters**:
-- `v` (`any`): Value to validate.
-- `msg?` (`string`): Optional override template.
+- `name` (`string`): Validator name.
+- `validator` (`fun(v:any):(ok:boolean)`): Validator function.
+- `template?` (`string`): Optional default message template.
**Return**:
-- `isValid` (`boolean`): Whether the check succeeds.
-- `err` (`string?`): Error message when the check fails.
+- `none` (`nil`)
**Example**:
```lua
-ok, err = validate.True(true) --> true, nil
-ok, err = validate.True(false) --> false, "true value expected, got false"
+validate.register("odd", function(v)
+ return type(v) == "number" and v % 2 == 1
+end, "{{value}} does not satisfy {{expected}}")
+
+ok, err = validate.odd(3) --> true, nil
+ok, err = validate.odd("x") --> false, '"x" does not satisfy odd'
+ok, err = validate(2, "odd") --> false, "2 does not satisfy odd"
```
-
+> [!NOTE]
+>
+> - If `template` is provided, it becomes the default message template for that
+> validator.
+> - If `template` is omitted, failures use:
+> `{{expected}} expected, got {{got}}`.
-#### `falsy(v, msg?)`
+### Type Checks
-Returns `true` when `v` is falsy. Otherwise returns `false` and an error
+
+
+#### `boolean(v, msg?)`
+
+Returns `true` when `v` is a boolean. Otherwise returns `false` and an error
message.
**Parameters**:
@@ -368,15 +375,15 @@ message.
**Example**:
```lua
-ok, err = validate.falsy(false) --> true, nil
-ok, err = validate.falsy(1) --> false, "falsy value expected, got 1"
+ok, err = validate.boolean(true) --> true, nil
+ok, err = validate.boolean(1) --> false, "boolean expected, got number"
```
-
+
-#### `callable(v, msg?)`
+#### `function(v, msg?)`
-Returns `true` when `v` is callable. Otherwise returns `false` and an error
+Returns `true` when `v` is a function. Otherwise returns `false` and an error
message.
**Parameters**:
@@ -392,15 +399,16 @@ message.
**Example**:
```lua
-ok, err = validate.callable(type) --> true, nil
-ok, err = validate.callable(1) --> false, "callable value expected, got 1"
+ok, err = validate.Function(function() end) --> true, nil
+ok, err = validate.Function(1)
+--> false, "function expected, got number"
```
-
+
-#### `integer(v, msg?)`
+#### `nil(v, msg?)`
-Returns `true` when `v` is an integer. Otherwise returns `false` and an error
+Returns `true` when `v` is `nil`. Otherwise returns `false` and an error
message.
**Parameters**:
@@ -416,15 +424,15 @@ message.
**Example**:
```lua
-ok, err = validate.integer(1) --> true, nil
-ok, err = validate.integer(1.5) --> false, "integer value expected, got 1.5"
+ok, err = validate.Nil(nil) --> true, nil
+ok, err = validate.Nil(0) --> false, "nil expected, got number"
```
-
+
-#### `truthy(v, msg?)`
+#### `number(v, msg?)`
-Returns `true` when `v` is truthy. Otherwise returns `false` and an error
+Returns `true` when `v` is a number. Otherwise returns `false` and an error
message.
**Parameters**:
@@ -440,24 +448,16 @@ message.
**Example**:
```lua
-ok, err = validate.truthy(1) --> true, nil
-ok, err = validate.truthy(false) --> false, "truthy value expected, got false"
+ok, err = validate.number(42) --> true, nil
+ok, err = validate.number("x") --> false, "number expected, got string"
```
-### Path Checks
-
-Filesystem path-kind validators backed by LuaFileSystem (`lfs`).
-
-> [!IMPORTANT]
->
-> Path checks require **LuaFileSystem**
-> ([`lfs`](https://github.com/lunarmodules/luafilesystem)) and raise an error if
-> it is not installed.
+
-#### `path(v, msg?)`
+#### `string(v, msg?)`
-Returns `true` when `v` is a valid filesystem path. Otherwise returns `false`
-and an error message.
+Returns `true` when `v` is a string. Otherwise returns `false` and an error
+message.
**Parameters**:
@@ -472,15 +472,16 @@ and an error message.
**Example**:
```lua
-ok, err = validate.path("README.md")
+ok, err = validate.string("hello") --> true, nil
+ok, err = validate.string(1) --> false, "string expected, got number"
```
-
+
-#### `block(v, msg?)`
+#### `table(v, msg?)`
-Returns `true` when `v` is a block device path. Otherwise returns `false` and an
-error message.
+Returns `true` when `v` is a table. Otherwise returns `false` and an error
+message.
**Parameters**:
@@ -495,15 +496,16 @@ error message.
**Example**:
```lua
-ok, err = validate.block(".")
+ok, err = validate.table({}) --> true, nil
+ok, err = validate.table(1) --> false, "table expected, got number"
```
-
+
-#### `char(v, msg?)`
+#### `thread(v, msg?)`
-Returns `true` when `v` is a char device path. Otherwise returns `false` and an
-error message.
+Returns `true` when `v` is a thread. Otherwise returns `false` and an error
+message.
**Parameters**:
@@ -518,15 +520,17 @@ error message.
**Example**:
```lua
-ok, err = validate.char(".")
+co = coroutine.create(function() end)
+ok, err = validate.thread(co) --> true, nil
+ok, err = validate.thread(1) --> false, "thread expected, got number"
```
-
+
-#### `device(v, msg?)`
+#### `userdata(v, msg?)`
-Returns `true` when `v` is a block or char device path. Otherwise returns
-`false` and an error message.
+Returns `true` when `v` is a userdata value. Otherwise returns `false` and an
+error message.
**Parameters**:
@@ -541,15 +545,18 @@ Returns `true` when `v` is a block or char device path. Otherwise returns
**Example**:
```lua
-ok, err = validate.device(".")
+ok, err = validate.userdata(io.stdout) --> true, nil
+ok, err = validate.userdata(1) --> false, "userdata expected, got number"
```
-
+### Value Checks
-#### `dir(v, msg?)`
+
-Returns `true` when `v` is a directory path. Otherwise returns `false` and an
-error message.
+#### `callable(v, msg?)`
+
+Returns `true` when `v` is callable. Otherwise returns `false` and an error
+message.
**Parameters**:
@@ -564,15 +571,16 @@ error message.
**Example**:
```lua
-ok, err = validate.dir(".")
+ok, err = validate.callable(type) --> true, nil
+ok, err = validate.callable(1) --> false, "callable value expected, got 1"
```
-
+
-#### `fifo(v, msg?)`
+#### `false(v, msg?)`
-Returns `true` when `v` is a FIFO path. Otherwise returns `false` and an error
-message.
+Returns `true` when `v` is exactly `false`. Otherwise returns `false` and an
+error message.
**Parameters**:
@@ -587,14 +595,15 @@ message.
**Example**:
```lua
-ok, err = validate.fifo(".")
+ok, err = validate.False(false) --> true, nil
+ok, err = validate.False(true) --> false, "false value expected, got true"
```
-
+
-#### `file(v, msg?)`
+#### `falsy(v, msg?)`
-Returns `true` when `v` is a file path. Otherwise returns `false` and an error
+Returns `true` when `v` is falsy. Otherwise returns `false` and an error
message.
**Parameters**:
@@ -610,15 +619,16 @@ message.
**Example**:
```lua
-ok, err = validate.file(".")
+ok, err = validate.falsy(false) --> true, nil
+ok, err = validate.falsy(1) --> false, "falsy value expected, got 1"
```
-
+
-#### `link(v, msg?)`
+#### `integer(v, msg?)`
-Returns `true` when `v` is a symlink path. Otherwise returns `false` and an
-error message.
+Returns `true` when `v` is an integer. Otherwise returns `false` and an error
+message.
**Parameters**:
@@ -633,15 +643,16 @@ error message.
**Example**:
```lua
-ok, err = validate.link(".")
+ok, err = validate.integer(1) --> true, nil
+ok, err = validate.integer(1.5) --> false, "integer value expected, got 1.5"
```
-
+
-#### `socket(v, msg?)`
+#### `true(v, msg?)`
-Returns `true` when `v` is a socket path. Otherwise returns `false` and an error
-message.
+Returns `true` when `v` is exactly `true`. Otherwise returns `false` and an
+error message.
**Parameters**:
@@ -656,49 +667,39 @@ message.
**Example**:
```lua
-ok, err = validate.socket(".")
+ok, err = validate.True(true) --> true, nil
+ok, err = validate.True(false) --> false, "true value expected, got false"
```
-### Validator API
-
-
+
-#### `register(name, validator, template?)`
+#### `truthy(v, msg?)`
-Register or override a validator function by name.
+Returns `true` when `v` is truthy. Otherwise returns `false` and an error
+message.
**Parameters**:
-- `name` (`string`): Validator name.
-- `validator` (`fun(v:any):(ok:boolean)`): Validator function.
-- `template?` (`string`): Optional default message template.
+- `v` (`any`): Value to validate.
+- `msg?` (`string`): Optional override template.
**Return**:
-- `none` (`nil`)
+- `isValid` (`boolean`): Whether the check succeeds.
+- `err` (`string?`): Error message when the check fails.
**Example**:
```lua
-validate.register("odd", function(v)
- return type(v) == "number" and v % 2 == 1
-end, "{{value}} does not satisfy {{expected}}")
-
-ok, err = validate.odd(3) --> true, nil
-ok, err = validate.odd("x") --> false, '"x" does not satisfy odd'
-ok, err = validate(2, "odd") --> false, "2 does not satisfy odd"
+ok, err = validate.truthy(1) --> true, nil
+ok, err = validate.truthy(false) --> false, "truthy value expected, got false"
```
-> [!NOTE]
->
-> - If `template` is provided, it becomes the default message template for that
-> validator.
-> - If `template` is omitted, failures use:
-> `{{expected}} expected, got {{got}}`.
-
## Fields
-### `messages`
+
+
+### `messages` (`modsValidatorMessages`)
Custom error-message templates for validator failures.