chore: add a markdown docs generation script for moonwave comments (#2)

This commit is contained in:
Erica Marigold 2025-02-21 19:39:14 +00:00 committed by GitHub
commit 25b87de27e
Signed by: DevComp
GPG key ID: B5690EEEBB952194
10 changed files with 693 additions and 6 deletions

56
.github/workflows/doc.yml vendored Normal file
View file

@ -0,0 +1,56 @@
name: Update API reference docs
on:
pull_request:
push:
branches:
- main
paths:
- 'lib/init.luau'
- '.lune/docsgen/**/*.luau'
- '**/.nix'
workflow_dispatch:
permissions:
contents: write
jobs:
update-ref:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install nix
uses: nixbuild/nix-quick-install-action@v29
- name: Restore and cache Nix store
uses: nix-community/cache-nix-action@v5
with:
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
restore-prefixes-first-match: nix-${{ runner.os }}-
gc-max-store-size-linux: 5368709000
purge: true
purge-prefixes: cache-${{ runner.os }}-
purge-created: 0
purge-primary-key: never
- name: Cache pesde data
uses: actions/cache@v4
with:
path: ~/.pesde
key: pesde-${{ runner.os }}-${{ hashFiles('pesde.toml') }}
- name: Install dependencies
run: nix develop -c pesde install --locked
- name: Update markdown API reference docs
run: nix develop -c lune run docsgen
- name: Commit & push
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
git commit -m "docs: update refs for https://github.com/0x5eal/luau-unzip/commit/${{ github.event.client_payload.sha }}" && \
git push || echo "warn: no changes to commit"

34
.lune/docsgen/init.luau Normal file
View file

@ -0,0 +1,34 @@
--> Generate markdown documentation from moonwave comments
local process = require("@lune/process")
local serde = require("@lune/serde")
local moonwave = require("./moonwave")
local logger = require("./log")
local writeMarkdown = require("./markdown")
local function extract(input: string): (number, { moonwave.Item }?)
local res = process.spawn("moonwave-extractor", { "extract", input }, {
stdio = {
stderr = "forward"
}
})
if not res.ok then
print()
logger.log("error", "`moonwave-extractor` failed with exit code", res.code)
return res.code, nil
end
local ok, items: { moonwave.Item } = pcall(serde.decode, "json" :: "json", res.stdout)
if not ok then
return 1, nil
end
return 0, items
end
local code, items = extract("lib/init.luau")
writeMarkdown("./docs/index.md", items :: { moonwave.Item })
process.exit(code)

24
.lune/docsgen/log.luau Normal file
View file

@ -0,0 +1,24 @@
local stdio = require("@lune/stdio")
local base = stdio.style("bold")
local STYLE_INFO = base .. `{stdio.color("green")}info{stdio.color("reset")}:`
local STYLE_WARN = base .. `{stdio.color("yellow")}warn{stdio.color("reset")}:`
local STYLE_ERROR = base .. `{stdio.color("red")}error{stdio.color("reset")}:`
export type LogType = "info" | "warn" | "error"
local styleMappings: { [LogType]: string } = {
info = STYLE_INFO,
warn = STYLE_WARN,
error = STYLE_ERROR,
}
return {
styles = styleMappings,
log = function<T...>(type: LogType, ...: T...): ()
local writer: (string) -> () = if type == "info" then stdio.write else stdio.ewrite
local fmtMsg = stdio.format(styleMappings[type], ...)
return writer(fmtMsg .. "\n")
end
}

177
.lune/docsgen/markdown.luau Normal file
View file

@ -0,0 +1,177 @@
local fs = require("@lune/fs")
local moonwave = require("./moonwave")
local logger = require("./log")
local function writeSectionHeader(buf: string, title: string)
buf ..= `## {title}\n`
return buf
end
local function writeRef(buf: string, name: string, fragment: string?)
buf ..= `\n[{name}]: #{fragment or name}\n`
return buf
end
local function writeClass(buf: string, name: string, desc: string)
buf ..= `# \`{name}\`\n`
buf ..= desc
buf ..= `\n\n`
return buf
end
local function writeDeclaration(buf: string, name: string, fields: { moonwave.Property })
buf ..= `\`\`\`luau\n`
buf ..= `export type {name} = \{\n`
for _, field in fields do
buf ..= `\t{field.name}: {field.lua_type},\n`
end
buf ..= "}\n"
buf ..= `\`\`\`\n`
return buf
end
local function writeProperty(buf: string, name: string, desc: string, type: string)
-- buf ..= `- **\`{name}: {type}\`** - {desc}\n`
buf ..= `- **{name}** - {desc}\n`
return buf
end
local function writeFunction(
buf: string,
class: string,
type: string,
name: string,
desc: string,
params: { moonwave.FunctionParam },
returns: { moonwave.FunctionReturn },
private: boolean
)
local sep = if type == "method" then ":" else "."
local declaredSignature = `{class}{sep}{name}`
buf ..= `### \`{name}\`\n`
if private then
buf ..= `> [!IMPORTANT]\n`
buf ..= `> This is a private API. It may be exported publically, but try to avoid\n`
buf ..= `> using this API, since it can have breaking changes at any time without\n`
buf ..= `> warning.\n\n`
end
buf ..= `{desc}\n`
buf ..= `\`\`\`luau\n`
buf ..= `{declaredSignature}(`
if #params > 0 then
buf ..= "\n"
for _, param in params do
buf ..= `\t{param.name}: {param.lua_type}, -- {param.desc}\n`
end
end
buf ..= `)`
if #returns > 0 then
if #returns == 1 then
buf ..= `: {returns[1].lua_type}\n`
else
for pos, ret in returns do
buf ..= `({ret.lua_type}`
if pos ~= #returns then
buf ..= `, `
end
end
buf ..= `)`
end
end
buf ..= `\n\`\`\`\n`
buf = writeRef(buf, declaredSignature, name)
return buf
end
local function writeType(buf: string, name: string, desc: string, type: string)
buf ..= `\`\`\`luau\n`
buf ..= `export type {name} = {type}\n`
buf ..= `\`\`\`\n`
return buf
end
local function writeMarkdown(path: string, items: { moonwave.Item })
local start = os.clock()
local buf = ""
for _, item in items do
logger.log("info", "Generating docs for", item.name)
buf = writeClass(buf, item.name, item.desc)
local props: { moonwave.Property } = {}
for pos, type in item.types do
if type.name == item.name then
table.remove(item.types, pos)
props = type.fields
end
end
buf = writeDeclaration(buf, item.name, props)
buf = writeSectionHeader(buf, "Properties")
for _, prop in props do
if prop.ignore then
continue
end
buf = writeProperty(buf, prop.name, prop.desc, prop.lua_type)
end
buf ..= "\n"
buf = writeSectionHeader(buf, "API")
for _, func in item.functions do
if func.ignore then
continue
end
buf = writeFunction(
buf,
item.name,
func.function_type,
func.name,
func.desc,
func.params,
func.returns,
func.private
)
end
buf ..= "\n"
buf = writeSectionHeader(buf, "Types")
for _, type in item.types do
if type.ignore then
continue
end
buf ..= `### \`{type.name}\`\n`
if type.private then
buf ..= `> [!IMPORTANT]\n`
buf ..= `> This is a private type. It may be exported publically, but try to avoid\n`
buf ..= `> using it, since its definition can have a breaking change at any time\n`
buf ..= `> without warning.\n\n`
end
buf ..= `{type.desc}\n`
if type.lua_type ~= nil then
buf = writeType(buf, type.name, type.desc, type.lua_type)
else
local fields: { moonwave.Property } = type.fields or {}
buf = writeDeclaration(buf, type.name, fields)
for _, field in fields do
buf = writeProperty(buf, field.name, field.desc, field.lua_type)
end
end
buf = writeRef(buf, type.name)
end
buf = writeRef(buf, item.name)
end
logger.log("info", string.format("Generated docs in %.2fms", (os.clock() - start) * 1000))
logger.log("info", "Writing to", path)
fs.writeFile(path, buf)
end
return writeMarkdown

View file

@ -0,0 +1,59 @@
--> Copied from https://github.com/lune-org/docs/blob/0a1e5a/.lune/moonwave.luau
export type Source = {
path: string,
line: number,
}
export type FunctionParam = {
name: string,
desc: string,
lua_type: string,
}
export type FunctionReturn = {
desc: string,
lua_type: string,
}
export type Function = {
name: string,
desc: string,
params: { FunctionParam },
returns: { FunctionReturn },
function_type: string,
tags: { string }?,
ignore: boolean,
private: boolean,
source: Source,
}
export type Property = {
name: string,
desc: string,
lua_type: string,
tags: { string }?,
ignore: boolean,
source: Source,
}
export type Type = {
name: string,
desc: string,
lua_type: string,
ignore: boolean,
private: boolean,
fields: { Property },
source: Source,
}
export type Item = {
name: string,
desc: string,
functions: { Function },
properties: { Property },
types: { Type },
source: Source,
}
return {}

View file

@ -6,7 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [Unreleased] ## [Unreleased]
### Added ### Added
- Documentation comments powered by [moonwave](https://github.com/evaera/moonwave) - Added doc comments powered by [moonwave](https://github.com/evaera/moonwave)
- Added markdown doc generator lune script and configured pesde docs
### Fixed ### Fixed
- Fixed incorrect type signatures for some functions - Fixed incorrect type signatures for some functions
### Changed ### Changed

View file

@ -111,6 +111,13 @@ pkgs.mkShell {
artifactName = "pesde-0.6.0-rc.8-linux-x86_64.zip"; artifactName = "pesde-0.6.0-rc.8-linux-x86_64.zip";
sha256 = "xjY8yPTAD32xeQokHDSWBOiGALLxPOU89Xlxi2Jdnno="; sha256 = "xjY8yPTAD32xeQokHDSWBOiGALLxPOU89Xlxi2Jdnno=";
}) })
(fromGithubRelease {
name = "evaera/moonwave";
exeName = "moonwave-extractor";
version = "v1.2.1";
artifactName = "moonwave-extractor-v1.2.1-linux.zip";
sha256 = "UPZ5ZIazNNBcqny7srFIkHqX0t09r0F1M9q4KyjLNgQ=";
})
(fromPesdeManifest { (fromPesdeManifest {
name = "JohnnyMorganz/luau-lsp"; name = "JohnnyMorganz/luau-lsp";
exeName = "luau-lsp"; exeName = "luau-lsp";

321
docs/index.md Normal file
View file

@ -0,0 +1,321 @@
# `ZipEntry`
A single entry (a file or a directory) in a ZIP file, and its properties.
```luau
export type ZipEntry = {
name: string,
versionMadeBy: { software: string, os: MadeByOS },
compressedSize: number,
size: number,
offset: number,
timestamp: number,
method: CompressionMethod,
crc: number,
isDirectory: boolean,
isText: boolean,
attributes: number,
parent: ZipEntry?,
children: { ZipEntry },
}
```
## Properties
- **name** - File path within ZIP, '/' suffix indicates directory
- **versionMadeBy** - Version of software and OS that created the ZIP
- **compressedSize** - Compressed size in bytes
- **size** - Uncompressed size in bytes
- **offset** - Absolute position of local header in ZIP
- **timestamp** - MS-DOS format timestamp
- **method** - Method used to compress the file
- **crc** - CRC32 checksum of the uncompressed data
- **isDirectory** - Whether the entry is a directory or not
- **isText** - Whether the entry is plain ASCII text or binary
- **attributes** - File attributes
- **parent** - Parent directory entry, `nil` if entry is root
- **children** - Children of the entry, if it was a directory, empty array for files
## API
### `new`
> [!IMPORTANT]
> This is a private API. It may be exported publically, but try to avoid
> using this API, since it can have breaking changes at any time without
> warning.
```luau
ZipEntry.new(
offset: number, -- Offset of the entry in the ZIP file
name: string, -- File path within ZIP, '/' suffix indicates directory
properties: ZipEntryProperties, -- Properties of the entry
): ZipEntry
```
[ZipEntry.new]: #new
### `isSymlink`
Returns whether the entry is a symlink.
```luau
ZipEntry:isSymlink(): boolean
```
[ZipEntry:isSymlink]: #isSymlink
### `getPath`
Resolves the path of the entry based on its relationship with other entries. It is recommended to use this
method instead of accessing the `name` property directly, although they should be equivalent.
> [!WARNING]
> Never use this method when extracting files from the ZIP, since it can contain absolute paths
> (say `/etc/passwd`) referencing directories outside the current directory (say `/tmp/extracted`),
> causing unintended overwrites of files.
```luau
ZipEntry:getPath(): string
```
[ZipEntry:getPath]: #getPath
### `getSafePath`
Resolves the path of the entry based on its relationship with other entries and returns it
only if it is safe to use for extraction, otherwise returns `nil`.
```luau
ZipEntry:getSafePath(): string?
```
[ZipEntry:getSafePath]: #getSafePath
### `sanitizePath`
Sanitizes the path of the entry, potentially losing information, but ensuring the path is
safe to use for extraction.
```luau
ZipEntry:sanitizePath(): string
```
[ZipEntry:sanitizePath]: #sanitizePath
### `compressionEfficiency`
Calculates the compression efficiency of the entry, or `nil` if the entry is a directory.
Uses the formula: `round((1 - compressedSize / size) * 100)` and outputs a percentage.
```luau
ZipEntry:compressionEfficiency(): number?
```
[ZipEntry:compressionEfficiency]: #compressionEfficiency
### `isFile`
Returns whether the entry is a file, i.e., not a directory or symlink.
```luau
ZipEntry:isFile(): boolean
```
[ZipEntry:isFile]: #isFile
### `unixMode`
Parses the entry's attributes to extract a UNIX mode, represented as a [UnixMode].
```luau
ZipEntry:unixMode(): UnixMode?
```
[ZipEntry:unixMode]: #unixMode
## Types
### `MadeByOS`
The OS that created the ZIP.
```luau
export type MadeByOS = "FAT" | "AMIGA" | "VMS" | "UNIX" | "VM/CMS" | "Atari ST" | "OS/2" | "MAC" | "Z-System" | "CP/M" | "NTFS" | "MVS" | "VSE" | "Acorn RISCOS" | "VFAT" | "Alternate MVS" | "BeOS" | "TANDEM" | "OS/400" | "OS/X" | "Unknown"
```
[MadeByOS]: #MadeByOS
### `CompressionMethod`
The method used to compress the file:
- `STORE` - No compression
- `DEFLATE` - Compressed raw deflate chunks
```luau
export type CompressionMethod = "STORE" | "DEFLATE"
```
[CompressionMethod]: #CompressionMethod
### `ZipEntryProperties`
> [!IMPORTANT]
> This is a private type. It may be exported publically, but try to avoid
> using it, since its definition can have a breaking change at any time
> without warning.
A set of properties that describe a ZIP entry. Used internally for construction of
[ZipEntry] objects.
```luau
export type ZipEntryProperties = {
versionMadeBy: number,
compressedSize: number,
size: number,
attributes: number,
timestamp: number,
method: CompressionMethod?,
crc: number,
}
```
- **versionMadeBy** - Version of software and OS that created the ZIP
- **compressedSize** - Compressed size in bytes
- **size** - Uncompressed size in bytes
- **attributes** - File attributes
- **timestamp** - MS-DOS format timestamp
- **method** - Method used
- **crc** - CRC32 checksum of the uncompressed data
[ZipEntryProperties]: #ZipEntryProperties
### `UnixMode`
A object representation of the UNIX mode.
```luau
export type UnixMode = {
perms: string,
typeFlags: string,
}
```
- **perms** - The permission octal
- **typeFlags** - The type flags octal
[UnixMode]: #UnixMode
[ZipEntry]: #ZipEntry
# `ZipReader`
The main class which represents a decoded state of a ZIP file, holding references
to its entries. This is the primary point of interaction with the ZIP file's contents.
```luau
export type ZipReader = {
data: buffer,
comment: string,
entries: { ZipEntry },
directories: { [string]: ZipEntry },
root: ZipEntry,
}
```
## Properties
- **data** - The buffer containing the raw bytes of the ZIP
- **comment** - Comment associated with the ZIP
- **entries** - The decoded entries present
- **directories** - The directories and their respective entries
- **root** - The entry of the root directory
## API
### `new`
Creates a new ZipReader instance from the raw bytes of a ZIP file.
**Errors if the ZIP file is invalid.**
```luau
ZipReader.new(
data: buffer, -- The buffer containing the raw bytes of the ZIP
): ZipReader
```
[ZipReader.new]: #new
### `parseCentralDirectory`
> [!IMPORTANT]
> This is a private API. It may be exported publically, but try to avoid
> using this API, since it can have breaking changes at any time without
> warning.
Parses the central directory of the ZIP file and populates the `entries` and `directories`
fields. Used internally during initialization of the [ZipReader].
**Errors if the ZIP file is invalid.**
```luau
ZipReader:parseCentralDirectory()
```
[ZipReader:parseCentralDirectory]: #parseCentralDirectory
### `buildDirectoryTree`
> [!IMPORTANT]
> This is a private API. It may be exported publically, but try to avoid
> using this API, since it can have breaking changes at any time without
> warning.
Builds the directory tree from the entries. Used internally during initialization of the
[ZipReader].
```luau
ZipReader:buildDirectoryTree()
```
[ZipReader:buildDirectoryTree]: #buildDirectoryTree
### `findEntry`
Finds a [ZipEntry] by its path in the ZIP archive.
```luau
ZipReader:findEntry(
path: string, -- Path to the entry to find
): ZipEntry?
```
[ZipReader:findEntry]: #findEntry
### `extract`
Extracts the specified [ZipEntry] from the ZIP archive. See [ZipReader:extractDirectory] for
extracting directories.
```luau
ZipReader:extract(
entry: ZipEntry, -- The entry to extract
options: ExtractionOptions?, -- Options for the extraction
): buffer | string
```
[ZipReader:extract]: #extract
### `extractDirectory`
Extracts all the files in a specified directory, skipping any directory entries.
**Errors if [ZipReader:extract] errors on an entry in the directory.**
```luau
ZipReader:extractDirectory(
path: string, -- The path to the directory to extract
options: ExtractionOptions?, -- Options for the extraction
): { [string]: buffer } | { [string]: string }
```
[ZipReader:extractDirectory]: #extractDirectory
### `listDirectory`
Lists the entries within a specified directory path.
```luau
ZipReader:listDirectory(
path: string, -- The path to the directory to list
): { ZipEntry }
```
[ZipReader:listDirectory]: #listDirectory
### `walk`
Recursively walks through the ZIP file, calling the provided callback for each entry
with the current entry and its depth.
```luau
ZipReader:walk(
callback: (entry: ZipEntry, depth: number) -> (), -- The function to call for each entry
)
```
[ZipReader:walk]: #walk
### `getStats`
Retrieves statistics about the ZIP file.
```luau
ZipReader:getStats(): ZipStatistics
```
[ZipReader:getStats]: #getStats
## Types
### `ZipStatistics`
```luau
export type ZipStatistics = {
fileCount: number,
dirCount: number,
totalSize: number,
}
```
- **fileCount** - The number of files in the ZIP
- **dirCount** - The number of directories in the ZIP
- **totalSize** - The total size of all files in the ZIP
[ZipStatistics]: #ZipStatistics
[ZipReader]: #ZipReader

View file

@ -168,6 +168,14 @@ export type CompressionMethod = "STORE" | "DEFLATE"
A set of properties that describe a ZIP entry. Used internally for construction of A set of properties that describe a ZIP entry. Used internally for construction of
[ZipEntry] objects. [ZipEntry] objects.
@field versionMadeBy number -- Version of software and OS that created the ZIP
@field compressedSize number -- Compressed size in bytes
@field size number -- Uncompressed size in bytes
@field attributes number -- File attributes
@field timestamp number -- MS-DOS format timestamp
@field method CompressionMethod? -- Method used
@field crc number -- CRC32 checksum of the uncompressed data
]=] ]=]
type ZipEntryProperties = { type ZipEntryProperties = {
versionMadeBy: number, versionMadeBy: number,
@ -234,11 +242,10 @@ end
Resolves the path of the entry based on its relationship with other entries. It is recommended to use this Resolves the path of the entry based on its relationship with other entries. It is recommended to use this
method instead of accessing the `name` property directly, although they should be equivalent. method instead of accessing the `name` property directly, although they should be equivalent.
:::warning > [!WARNING]
Never use this method when extracting files from the ZIP, since it can contain absolute paths > Never use this method when extracting files from the ZIP, since it can contain absolute paths
(say `/etc/passwd`) referencing directories outside the current directory (say `/tmp/extracted`), > (say `/etc/passwd`) referencing directories outside the current directory (say `/tmp/extracted`),
causing unintended overwrites of files. > causing unintended overwrites of files.
:::
@return string -- The path of the entry @return string -- The path of the entry
]=] ]=]

View file

@ -10,6 +10,7 @@ includes = [
"pesde.toml", "pesde.toml",
"lib/**/*.luau", "lib/**/*.luau",
"!tests/**/*", "!tests/**/*",
"docs/**/*.md"
] ]
[engines] [engines]