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]
### 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 incorrect type signatures for some functions
### Changed

View file

@ -111,6 +111,13 @@ pkgs.mkShell {
artifactName = "pesde-0.6.0-rc.8-linux-x86_64.zip";
sha256 = "xjY8yPTAD32xeQokHDSWBOiGALLxPOU89Xlxi2Jdnno=";
})
(fromGithubRelease {
name = "evaera/moonwave";
exeName = "moonwave-extractor";
version = "v1.2.1";
artifactName = "moonwave-extractor-v1.2.1-linux.zip";
sha256 = "UPZ5ZIazNNBcqny7srFIkHqX0t09r0F1M9q4KyjLNgQ=";
})
(fromPesdeManifest {
name = "JohnnyMorganz/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
[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 = {
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
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.
:::
> [!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.
@return string -- The path of the entry
]=]

View file

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