# LDoc: a LuaDoc compatible documentation generator that also reads C extensions

> LDoc grew out of the Penlight project and keeps LuaDoc compatible while adding diagnostics, Markdown rendering and support for Lua C extension sources. It fits teams that want to drop it into an existing LuaDoc workflow, and it will not replace a generator for other languages.

**lunarmodules/ldoc** — LDoc is a LuaDoc-compatible documentation generator which can also process C extension source. Markdown may be optionally used to render comments, as well as integrated readme documentation and pretty-printed example files.

- Repository: https://github.com/lunarmodules/ldoc
- Website: https://lunarmodules.github.io/ldoc/
- Stars: 869 · Forks: 182
- Language: Lua
- License: NOASSERTION
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/lunarmodules-ldoc

## LuaDoc markup, with diagnostics LuaDoc does not give you

LDoc is a documentation generator for Lua, and its stated goal is compatibility with LuaDoc, following the pattern set by the various Doc tools. The comment format is the familiar one, a triple dash summary followed by description lines and tag lines:

```lua
    --- Summary ends with a period.
    -- Some description, can be over several lines.
    -- @param p1 first parameter
    -- @param p2 second parameter
    -- @return a string value
    -- @see second_fun
    function mod1.first_fun(p1,p2)
    end
```

The payoff is that existing LuaDoc comments keep working, and the tool adds things LuaDoc did not do well. The first is diagnostics. If a @see reference cannot be resolved, LDoc reports the line number of the reference, which turns a silent documentation bug into a pinpointed one. The second is that LDoc knows about modules which do not use module(), which the README calls important because that function has been deprecated in Lua 5.2. A tool that required module() would have pushed people to keep calling a deprecated function.

Names of functions and modules can generally be inferred from the code, and tags such as see and usage are supported. Markdown may optionally be used to render comments, which removes the need to embed HTML in comments, and integrated readme documentation and pretty-printed example files are also features of the project.

One supporting project is named as the reference implementation. The Winapi documentation is generated from winapi.l.c, and it demonstrates the named sections and classes support in a real C extension.

## Penlight is the only dependency, and Penlight needs LuaFileSystem

The dependency chain is short and worth stating plainly, because for a documentation generator the install is often the part that goes wrong. The only external dependency is Penlight, and Penlight in turn needs LuaFileSystem. The README points out that both are already present in Lua for Windows, and that Penlight is also available through LuaRocks.

Penlight is installed through LuaRocks with luarocks install penlight, and LDoc itself with the same command form:

```bash
luarocks install ldoc
```

The manual route is unpack the sources somewhere and make an alias to ldoc.lua on your path, either as an executable script called ldoc:

```bash
    lua /path/to/ldoc/ldoc.lua $*
```

or as a batch file called ldoc.bat on Windows:

```batch
    @echo off
    lua \path\to\ldoc\ldoc.lua %*
```

The Makefile at the repository root offers a third route. It derives the Lua binary directory from the shell, writes a small wrapper script into the bindir that calls ldoc.lua with your arguments, and copies both ldoc.lua and the ldoc directory into the shared Lua 5.1 directory. There are separate install, install_luajit and uninstall targets, and the install target also installs the shared directory structure if it is missing.

The project grew out of the documentation needs of Penlight itself, and the README notes that depending on Penlight allowed the author to not write a lot of code. That is the design in one sentence: LDoc is a thin, careful layer over Penlight rather than a standalone parser stack.

## The Docker image builds Penlight dependencies in two stages

A container image is available, and the Dockerfile shows exactly what it contains. It is a two stage build. The builder stage starts from an Alpine image with Lua 5.4 and installs build tools including gcc and libc-dev, copies the source to /src and installs three separate rock trees into a staging directory: cmark, lunamark and lua-discount. Those three are the Markdown backends, which is why Markdown rendering works in the container even though it is optional in a plain install.

Then it runs luarocks make for the project itself, and rewrites the staging paths out of the installed files with a find and sed pass, so the tree can be copied into the final image without absolute build paths baked in.

The final stage is a plain Alpine Lua 5.4 image with dumb-init and setpriv, the labels for title, description, authors, licences, url and source, and a copy of the staged tree. The build ends by running ldoc --version as a smoke test. The working directory is /data, and the entrypoint is a small shell script.

That entrypoint exists for a specific reason, explained in a comment in the Dockerfile. GitHub Actions and some other environments insist on running the entrypoint as root inside the container even when it is being run by an unprivileged user on their own files, so the script compares the effective user id with the owner of the workdir and, when they differ, uses setpriv to clear groups and set the effective user and group to match the directory owner. Without it, generated files in a mounted directory end up owned by root.

## Running it: build, pull, or pull it into CI

Three commands cover most use cases. To build your own image from the source directory of the project:

```console
$ docker build -t ghcr.io/lunarmodules/ldoc:HEAD .
```

To use a prebuilt one from the GitHub Container Registry:

```console
$ docker pull ghcr.io/lunarmodules/ldoc:latest
```

And to run it against a file or a source tree, with the current directory mounted at /data:

```console
$ docker run -v "$(pwd):/data" ghcr.io/lunarmodules/ldoc:latest .
```

The final argument is the path to document, with a dot meaning the current directory. The README suggests an alias to avoid repeating the invocation:

```console
$ alias ldoc='docker run -v "$(pwd):/data" ghcr.io/lunarmodules/ldoc:latest'
```

For CI there is a GitHub Action, and the workflow file at the repository root shows the shape. A minimal job checks out the repository and runs the action:

```yaml
name: LDoc
on: [ push, pull_request ]
jobs:
  sile:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Generate docs with LDoc
        uses: lunarmodules/ldoc@v0
```

By default the action runs ldoc ., and you can override that default with your own args:

```yaml
      - name: Generate docs with LDoc
        uses: lunarmodules/ldoc@v0
        with:
            args: myfile.lua
```

Generating docs for your own Lua project through GitHub Actions is described separately in the doc.yml workflow, and the README is specific about two things that trip people up: use luarocks install ldoc rather than luarocks install --only-deps, and make sure publish_dir in doc.yml points at the same location as the dir parameter in config.ld. After pushing to master you watch the build checkmark, then a repository owner has to enable gh-pages under Settings and Pages with the source set to gh-pages and root.

## The test targets compare generated output against a checked in tree

The Makefile shows how the project checks itself, and the approach is worth noting because it is a snapshot test rather than a unit test suite. The test target chains four sub-targets: test-basic, test-example, test-md and test-tables. Each one changes into a directory under tests and runs the same comparison, which is to run ldoc.lua over the directory and then diff the result against a reference directory called cdocs.

The comparison step is exactly this: run the generator, then diff -r public cdocs and echo ok. The prep target prepares those reference trees by finding directories named public and syncing their contents into cdocs with rsync and the delete flag, so a reference tree can be regenerated when an output change is intentional.

The four test directories correspond to the feature areas the README lists: the basic case, an example project, the md-test directory for Markdown rendering, and the simple directory for tables. There is also a doc-site target that runs ldoc.lua over the repository root, which is how the project's own documentation is generated, and a clean target per test directory.

The repository layout supports all of this: a tests/ directory at the root, run-tests.lua for driving tests in Lua, a .luacheckrc with a Luacheck badge in the README, a config.ld for the project own configuration, manual.md for the manual, a CHANGELOG.md, a COPYRIGHT file naming Steve Donovan for 2011 to 2012, and rockspecs/ along with ldoc-dev-1.rockspec for packaging.

## Where LDoc is the wrong tool

The obvious limit is language. LDoc documents Lua and Lua C extensions. It will not generate documentation for a project written in another language, and there is no pluggable parser story described for adding one. If your codebase is mixed, you will end up running two generators.

The second limit is scope relative to LuaDoc compatibility itself. Compatibility means LDoc accepts the older markup, not that it reproduces every LuaDoc feature or every LuaDoc configuration option. The project describes itself as following the pattern set by the Doc tools rather than as a drop-in superset, and the docs to read before migrating are the manual at the root and the changelog.

The third is the dependency on Penlight and LuaFileSystem, which in turn means a working Lua installation. On a machine that has none, the container image is the practical route, and the container route has its own cost: a Docker daemon on the build machine, and the behaviour of the ownership fixing entrypoint if your CI runner and your files disagree about who owns what.

On maintenance, the repository is not archived and the last push was on 2026-09-03. The release history is thinner than the commit history, with one published release, v1.5.0 from 2023-05-02. That is fine for a tool with a stable command line interface and a long history of use, and it is a reason to pin your version rather than to track master. The licence position is worth checking directly as well: the container image labels itself MIT, and the repository carries a COPYRIGHT file, but the licence file itself is the thing to read before you rely on it.

## Conclusion

LDoc suits a Lua project that already uses LuaDoc comments and wants the same markup with better diagnostics, Markdown rendering and C extension parsing, and it does not suit a project that needs documentation generation for anything other than Lua. Before you adopt it, decide between the three install paths, since a source checkout with an alias, luarocks install ldoc and the ghcr.io/lunarmodules/ldoc container give different upgrade models, and check the version you would actually get, since the newest release is v1.5.0 from 2023-05-02 while the last push was on 2026-09-03.

## FAQ

### How do I install LDoc for Lua projects?

The only external dependency is Penlight, which in turn needs LuaFileSystem. Install Penlight with luarocks install penlight and LDoc with luarocks install ldoc, or unpack the sources and alias ldoc.lua on your path, or pull the container ghcr.io/lunarmodules/ldoc:latest.

### Is LDoc compatible with LuaDoc comments?

Yes. LDoc is intended to be compatible with LuaDoc and follows the same comment pattern with tags such as @param, @return and @see. It also understands modules that do not call module(), which matters because that function is deprecated in Lua 5.2.

### How do I run LDoc in a GitHub Actions workflow?

Use the lunarmodules/ldoc@v0 action in a step after checkout. By default it runs ldoc ., and you can pass your own args, for example args: myfile.lua, to replace the default input of a dot.

### Can LDoc document Lua C extension source?

Yes. LDoc works with Lua C extension code and offers shortcuts for it, and the Winapi documentation is generated from winapi.l.c as the reference example of named sections and classes support.

## Sources

- [Issues](https://github.com/lunarmodules/ldoc/issues)
- [lunarmodules/ldoc on GitHub](https://github.com/lunarmodules/ldoc)
- [Project website](https://lunarmodules.github.io/ldoc/)
- [README](https://github.com/lunarmodules/ldoc/blob/master/README.md)
- [Releases](https://github.com/lunarmodules/ldoc/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/lunarmodules-ldoc
