diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,64 @@
+## 0.1.0
+> 2026-04-05
+
+## Summary
+
+Initial release of `hslua-annotations`, a utility library to generate Lua type
+annotations and Markdown documentation for [hslua](https://github.com/hslua/hslua)
+modules and functions.
+
+### Commits
+- [[`1aa9062`](https://github.com/sondr3/hslua-annotations/commit/1aa9062)] Add Hackage version badge
+- [[`2cff0c6`](https://github.com/sondr3/hslua-annotations/commit/2cff0c6)] Add publishing step to CI
+- [[`5c2a75a`](https://github.com/sondr3/hslua-annotations/commit/5c2a75a)] Add category
+- [[`fe00c06`](https://github.com/sondr3/hslua-annotations/commit/fe00c06)] Update docs
+- [[`6b045ad`](https://github.com/sondr3/hslua-annotations/commit/6b045ad)] Remove redundant function calls
+- [[`a6ede1e`](https://github.com/sondr3/hslua-annotations/commit/a6ede1e)] Change return type to table, move description and version down
+- [[`ec3becb`](https://github.com/sondr3/hslua-annotations/commit/ec3becb)] Handle [] as having no return type
+- [[`331f5f0`](https://github.com/sondr3/hslua-annotations/commit/331f5f0)] Remove untyped return types
+- [[`6913e1d`](https://github.com/sondr3/hslua-annotations/commit/6913e1d)] Add ? for optional params in lua block
+- [[`7e2f9db`](https://github.com/sondr3/hslua-annotations/commit/7e2f9db)] Extract return type and create typed lua block for functions
+- [[`3e3215c`](https://github.com/sondr3/hslua-annotations/commit/3e3215c)] Indent params properly
+- [[`2b21218`](https://github.com/sondr3/hslua-annotations/commit/2b21218)] Take inspiration from rustdoc
+- [[`fb38417`](https://github.com/sondr3/hslua-annotations/commit/fb38417)] Render parameter docs as a table
+- [[`0ab42c9`](https://github.com/sondr3/hslua-annotations/commit/0ab42c9)] Change function headings to match fields
+- [[`f398aee`](https://github.com/sondr3/hslua-annotations/commit/f398aee)] Refactor to not use LambdaCase
+- [[`7e495ca`](https://github.com/sondr3/hslua-annotations/commit/7e495ca)] Fix multiline function descriptions
+- [[`e085ef9`](https://github.com/sondr3/hslua-annotations/commit/e085ef9)] Add type annotations to fields
+- [[`f8f5819`](https://github.com/sondr3/hslua-annotations/commit/f8f5819)] Refactor field rendering some more
+- [[`d9826e4`](https://github.com/sondr3/hslua-annotations/commit/d9826e4)] Create utility function for filtering unlines
+- [[`5bdf26c`](https://github.com/sondr3/hslua-annotations/commit/5bdf26c)] Wrap module headings with backticks
+- [[`b937e35`](https://github.com/sondr3/hslua-annotations/commit/b937e35)] Remove comments, refactor things slightly
+- [[`2a8b825`](https://github.com/sondr3/hslua-annotations/commit/2a8b825)] Render class fields as a table
+- [[`2a0aaac`](https://github.com/sondr3/hslua-annotations/commit/2a0aaac)] Extract 'typeToText' to shared module
+- [[`47c463b`](https://github.com/sondr3/hslua-annotations/commit/47c463b)] Import old, deprecated render functionality, update to latest HsLua
+- [[`ed7d2f8`](https://github.com/sondr3/hslua-annotations/commit/ed7d2f8)] Extract shared functions to shared module
+- [[`004b1ed`](https://github.com/sondr3/hslua-annotations/commit/004b1ed)] Rejigger tests, add annotation and markdown test trees
+- [[`9d4ed09`](https://github.com/sondr3/hslua-annotations/commit/9d4ed09)] Start sketching out markdown rendering
+- [[`a8b0827`](https://github.com/sondr3/hslua-annotations/commit/a8b0827)] Split out the annotation into an internal file
+- [[`a071088`](https://github.com/sondr3/hslua-annotations/commit/a071088)] Implement 'typeToText' to better align with annotations
+- [[`4aa5f81`](https://github.com/sondr3/hslua-annotations/commit/4aa5f81)] Slacken text package requirements
+- [[`34b76cb`](https://github.com/sondr3/hslua-annotations/commit/34b76cb)] Minor README adjustments
+- [[`e7b2247`](https://github.com/sondr3/hslua-annotations/commit/e7b2247)] Rename annotateMethod... not really a thing in Lua
+- [[`37901ee`](https://github.com/sondr3/hslua-annotations/commit/37901ee)] Filter out empty function descriptions
+- [[`285b8ed`](https://github.com/sondr3/hslua-annotations/commit/285b8ed)] Add dependabot
+- [[`d86ec4c`](https://github.com/sondr3/hslua-annotations/commit/d86ec4c)] Remove unused dependencies, move test only to test package
+- [[`cb45364`](https://github.com/sondr3/hslua-annotations/commit/cb45364)] Add README and document things
+- [[`605f044`](https://github.com/sondr3/hslua-annotations/commit/605f044)] Remove duplicate function annotation function
+- [[`6099d8e`](https://github.com/sondr3/hslua-annotations/commit/6099d8e)] Filter out empty lines
+- [[`dff4da6`](https://github.com/sondr3/hslua-annotations/commit/dff4da6)] And annotate operators
+- [[`fbf3e27`](https://github.com/sondr3/hslua-annotations/commit/fbf3e27)] Use T.unlines to make multilines more readable
+- [[`2a5523b`](https://github.com/sondr3/hslua-annotations/commit/2a5523b)] Annotate module functions
+- [[`54843d0`](https://github.com/sondr3/hslua-annotations/commit/54843d0)] Stick with GHC 9.12 for now
+- [[`5700b5e`](https://github.com/sondr3/hslua-annotations/commit/5700b5e)] Fix hlint suggestions
+- [[`3c9fe23`](https://github.com/sondr3/hslua-annotations/commit/3c9fe23)] Start sketching out module annotation, test on hslua-module-path
+- [[`bb48b5d`](https://github.com/sondr3/hslua-annotations/commit/bb48b5d)] Move factorial to test file
+- [[`99e1e70`](https://github.com/sondr3/hslua-annotations/commit/99e1e70)] Add a Justfile for running things
+- [[`84a1e1b`](https://github.com/sondr3/hslua-annotations/commit/84a1e1b)] Add parameter annotations to functions
+- [[`d81deb6`](https://github.com/sondr3/hslua-annotations/commit/d81deb6)] Get the function description and output it too
+- [[`f9f1de5`](https://github.com/sondr3/hslua-annotations/commit/f9f1de5)] Correct pipeline name
+- [[`969bf50`](https://github.com/sondr3/hslua-annotations/commit/969bf50)] Add CI pipeline
+- [[`34d0bce`](https://github.com/sondr3/hslua-annotations/commit/34d0bce)] Fix unused imports
+- [[`1c1c666`](https://github.com/sondr3/hslua-annotations/commit/1c1c666)] Initial pass at getting types from HsLua
+- [[`1dfaa20`](https://github.com/sondr3/hslua-annotations/commit/1dfaa20)] In the beginning there was darkness...
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2026 Sondre Aasemoen
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/LICENSE_APACHE b/LICENSE_APACHE
new file mode 100644
--- /dev/null
+++ b/LICENSE_APACHE
@@ -0,0 +1,176 @@
+                              Apache License
+                        Version 2.0, January 2004
+                     http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+   "License" shall mean the terms and conditions for use, reproduction,
+   and distribution as defined by Sections 1 through 9 of this document.
+
+   "Licensor" shall mean the copyright owner or entity authorized by
+   the copyright owner that is granting the License.
+
+   "Legal Entity" shall mean the union of the acting entity and all
+   other entities that control, are controlled by, or are under common
+   control with that entity. For the purposes of this definition,
+   "control" means (i) the power, direct or indirect, to cause the
+   direction or management of such entity, whether by contract or
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
+   outstanding shares, or (iii) beneficial ownership of such entity.
+
+   "You" (or "Your") shall mean an individual or Legal Entity
+   exercising permissions granted by this License.
+
+   "Source" form shall mean the preferred form for making modifications,
+   including but not limited to software source code, documentation
+   source, and configuration files.
+
+   "Object" form shall mean any form resulting from mechanical
+   transformation or translation of a Source form, including but
+   not limited to compiled object code, generated documentation,
+   and conversions to other media types.
+
+   "Work" shall mean the work of authorship, whether in Source or
+   Object form, made available under the License, as indicated by a
+   copyright notice that is included in or attached to the work
+   (an example is provided in the Appendix below).
+
+   "Derivative Works" shall mean any work, whether in Source or Object
+   form, that is based on (or derived from) the Work and for which the
+   editorial revisions, annotations, elaborations, or other modifications
+   represent, as a whole, an original work of authorship. For the purposes
+   of this License, Derivative Works shall not include works that remain
+   separable from, or merely link (or bind by name) to the interfaces of,
+   the Work and Derivative Works thereof.
+
+   "Contribution" shall mean any work of authorship, including
+   the original version of the Work and any modifications or additions
+   to that Work or Derivative Works thereof, that is intentionally
+   submitted to Licensor for inclusion in the Work by the copyright owner
+   or by an individual or Legal Entity authorized to submit on behalf of
+   the copyright owner. For the purposes of this definition, "submitted"
+   means any form of electronic, verbal, or written communication sent
+   to the Licensor or its representatives, including but not limited to
+   communication on electronic mailing lists, source code control systems,
+   and issue tracking systems that are managed by, or on behalf of, the
+   Licensor for the purpose of discussing and improving the Work, but
+   excluding communication that is conspicuously marked or otherwise
+   designated in writing by the copyright owner as "Not a Contribution."
+
+   "Contributor" shall mean Licensor and any individual or Legal Entity
+   on behalf of whom a Contribution has been received by Licensor and
+   subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   copyright license to reproduce, prepare Derivative Works of,
+   publicly display, publicly perform, sublicense, and distribute the
+   Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   (except as stated in this section) patent license to make, have made,
+   use, offer to sell, sell, import, and otherwise transfer the Work,
+   where such license applies only to those patent claims licensable
+   by such Contributor that are necessarily infringed by their
+   Contribution(s) alone or by combination of their Contribution(s)
+   with the Work to which such Contribution(s) was submitted. If You
+   institute patent litigation against any entity (including a
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
+   or a Contribution incorporated within the Work constitutes direct
+   or contributory patent infringement, then any patent licenses
+   granted to You under this License for that Work shall terminate
+   as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+   Work or Derivative Works thereof in any medium, with or without
+   modifications, and in Source or Object form, provided that You
+   meet the following conditions:
+
+   (a) You must give any other recipients of the Work or
+       Derivative Works a copy of this License; and
+
+   (b) You must cause any modified files to carry prominent notices
+       stating that You changed the files; and
+
+   (c) You must retain, in the Source form of any Derivative Works
+       that You distribute, all copyright, patent, trademark, and
+       attribution notices from the Source form of the Work,
+       excluding those notices that do not pertain to any part of
+       the Derivative Works; and
+
+   (d) If the Work includes a "NOTICE" text file as part of its
+       distribution, then any Derivative Works that You distribute must
+       include a readable copy of the attribution notices contained
+       within such NOTICE file, excluding those notices that do not
+       pertain to any part of the Derivative Works, in at least one
+       of the following places: within a NOTICE text file distributed
+       as part of the Derivative Works; within the Source form or
+       documentation, if provided along with the Derivative Works; or,
+       within a display generated by the Derivative Works, if and
+       wherever such third-party notices normally appear. The contents
+       of the NOTICE file are for informational purposes only and
+       do not modify the License. You may add Your own attribution
+       notices within Derivative Works that You distribute, alongside
+       or as an addendum to the NOTICE text from the Work, provided
+       that such additional attribution notices cannot be construed
+       as modifying the License.
+
+   You may add Your own copyright statement to Your modifications and
+   may provide additional or different license terms and conditions
+   for use, reproduction, or distribution of Your modifications, or
+   for any such Derivative Works as a whole, provided Your use,
+   reproduction, and distribution of the Work otherwise complies with
+   the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+   any Contribution intentionally submitted for inclusion in the Work
+   by You to the Licensor shall be under the terms and conditions of
+   this License, without any additional terms or conditions.
+   Notwithstanding the above, nothing herein shall supersede or modify
+   the terms of any separate license agreement you may have executed
+   with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+   names, trademarks, service marks, or product names of the Licensor,
+   except as required for reasonable and customary use in describing the
+   origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+   agreed to in writing, Licensor provides the Work (and each
+   Contributor provides its Contributions) on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied, including, without limitation, any warranties or conditions
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+   PARTICULAR PURPOSE. You are solely responsible for determining the
+   appropriateness of using or redistributing the Work and assume any
+   risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+   whether in tort (including negligence), contract, or otherwise,
+   unless required by applicable law (such as deliberate and grossly
+   negligent acts) or agreed to in writing, shall any Contributor be
+   liable to You for damages, including any direct, indirect, special,
+   incidental, or consequential damages of any character arising as a
+   result of this License or out of the use or inability to use the
+   Work (including but not limited to damages for loss of goodwill,
+   work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses), even if such Contributor
+   has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+   the Work or Derivative Works thereof, You may choose to offer,
+   and charge a fee for, acceptance of support, warranty, indemnity,
+   or other liability obligations and/or rights consistent with this
+   License. However, in accepting such obligations, You may act only
+   on Your own behalf and on Your sole responsibility, not on behalf
+   of any other Contributor, and only if You agree to indemnify,
+   defend, and hold each Contributor harmless for any liability
+   incurred by, or claims asserted against, such Contributor by reason
+   of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,69 @@
+<h1 align="center">hslua-annotations</h1>
+
+<p align="center">
+   <a href="https://github.com/sondr3/hslua-annotations/actions"><img alt="GitHub Actions Status" src="https://github.com/sondr3/hslua-annotations/workflows/pipeline/badge.svg" /></a>
+   <a href="https://hackage.haskell.org/package/hslua-annotations"><img alt="Hackage version" src="https://img.shields.io/hackage/v/hslua-annotations.svg?logo=haskell" /></a>
+   <br />
+</p>
+
+<p align="center">
+   <strong>A type annotation generator for HSLua targetting LuaLS and EmmyLua</strong>
+</p>
+
+<details>
+<summary>Table of Contents</summary>
+<br />
+
+**Table of Contents**
+
+- [Installation](#installation)
+- [Usage](#usage)
+- [License](#license)
+
+</details>
+
+This is a small library to generate [LuaLS](https://luals.github.io/wiki/annotations) and [EmmyLua](https://github.com/EmmyLuaLs/emmylua-analyzer-rust/blob/main/docs/emmylua_doc/annotations_EN/README.md) 
+annotations from HsLua.
+
+## Installation
+
+Add `hslua-annotations` to your Cabal file:
+
+```cabal
+build-depends:
+  hslua-annotations ^>= 0.1
+```
+
+## Usage
+
+```haskell
+import HsLua.Annotations (annotateModule, documentModule)
+import HsLua.Module.Path qualified as Path
+import HsLua.Packaging
+import HsLua.Core qualified as Lua
+import Data.Text (Text)
+import Data.Text.IO qualified as TIO
+
+-- utility function might be needed to narrow the `Module e` type
+renderDocs :: Module Lua.Exception -> Text
+renderDocs mod = documentModule mod
+
+-- utility function might be needed to narrow the `Module e` type
+renderAnnotations :: Module Lua.Exception -> Text
+renderAnnotations mod = annotateModule mod
+
+main :: IO ()
+main = do
+  TIO.writeFile "path.md" $ renderDocs Path.documentedModule
+  TIO.writeFile "path.lua" $ renderAnnotations Path.documentedModule
+```
+
+## License
+
+This project is licensed under either of
+
+- Apache License, Version 2.0, ([LICENSE_APACHE](LICENSE_APACHE) or
+  http://www.apache.org/licenses/LICENSE-2.0)
+- MIT license ([LICENSE](LICENSE) or http://opensource.org/licenses/MIT)
+
+at your option.
diff --git a/hslua-annotations.cabal b/hslua-annotations.cabal
new file mode 100644
--- /dev/null
+++ b/hslua-annotations.cabal
@@ -0,0 +1,84 @@
+cabal-version: 3.0
+name: hslua-annotations
+version: 0.1.0
+synopsis: A type annotation generator for HSLua
+description: A type annotation generator for HSLua targetting LuaLS and EmmyLua
+homepage: https://github.com/sondr3/hslua-annotations
+bug-reports: https://github.com/sondr3/hslua-annotations/issues
+license: (MIT OR Apache-2.0)
+license-file: LICENSE LICENSE_APACHE
+author: Sondre Aasemoen
+maintainer: sondre@eons.io
+category: Foreign
+build-type: Simple
+extra-doc-files:
+  CHANGELOG.md
+  README.md
+
+tested-with:
+  ghc ==9.10
+  ghc ==9.12
+  ghc ==9.14
+
+source-repository head
+  type: git
+  location: https://github.com/sondr3/hslua-annotations.git
+
+common config
+  default-language: GHC2021
+  default-extensions:
+    DerivingStrategies
+    DuplicateRecordFields
+    LambdaCase
+    MultiWayIf
+    NamedFieldPuns
+    OverloadedStrings
+    TypeFamilies
+
+  ghc-options:
+    -Weverything
+    -Wno-unsafe
+    -Wno-implicit-prelude
+    -Wno-missing-safe-haskell-mode
+    -Wno-missing-kind-signatures
+    -Wno-missing-import-lists
+    -Wno-missed-specialisations
+    -Wno-all-missed-specialisations
+    -Werror=missing-deriving-strategies
+    -fhide-source-paths
+    -fwrite-ide-info
+
+  build-depends:
+    base >=4.12 && <5,
+    hslua-core ^>=2.3,
+    hslua-packaging ^>=2.4,
+    text >=2 && <3,
+
+library
+  import: config
+  hs-source-dirs: lib
+  exposed-modules:
+    HsLua.Annotations
+
+  other-modules:
+    HsLua.Annotations.Internal
+    HsLua.Annotations.Markdown
+    HsLua.Annotations.Shared
+
+test-suite hslua-annotations-golden
+  import: config
+  type: exitcode-stdio-1.0
+  hs-source-dirs: test
+  main-is: Main.hs
+  build-depends:
+    hslua-annotations,
+    hslua-marshalling ^>=2.3,
+    hslua-module-path ^>=1.2,
+    hslua-module-version ^>=1.2,
+    tasty ^>=1.5,
+    tasty-golden ^>=2.3,
+
+  ghc-options:
+    -threaded
+    -rtsopts
+    -with-rtsopts=-N
diff --git a/lib/HsLua/Annotations.hs b/lib/HsLua/Annotations.hs
new file mode 100644
--- /dev/null
+++ b/lib/HsLua/Annotations.hs
@@ -0,0 +1,44 @@
+{-# OPTIONS_HADDOCK show-extensions #-}
+
+-- |
+-- Module: HsLua.Annotations
+-- Description: A type annotation generator for HSLua
+-- Copyright: (c) Sondre Aasemoen
+-- SPDX-License-Identifier: (MIT OR Apache-2.0)
+--
+-- Maintainer: Sondre Aasemoen <sondre@eons.io>
+-- Stability: experimental
+-- Portability: portable
+-- Safe Haskell: Safe
+--
+-- A type annotation generator for HSLua targetting LuaLS and EmmyLua
+--
+-- == Usage
+--
+-- To use this library you need to add it as a dependency in your project, e.g.
+-- by adding it to @build-depends@ in your @.cabal@ file.
+--
+-- @
+-- build-depends: hslua-annotations ^>= 0.1.0
+-- @
+--
+-- This module does not use common names and can be directly imported:
+--
+-- @
+-- __import__ HsLua.Annotations
+-- @
+module HsLua.Annotations
+  ( -- * Type annotation
+    annotateFunction,
+    annotateModule,
+
+    -- * Markdown rendering
+    documentFunction,
+    documentModule,
+  )
+where
+
+--
+
+import HsLua.Annotations.Internal (annotateFunction, annotateModule)
+import HsLua.Annotations.Markdown (documentFunction, documentModule)
diff --git a/lib/HsLua/Annotations/Internal.hs b/lib/HsLua/Annotations/Internal.hs
new file mode 100644
--- /dev/null
+++ b/lib/HsLua/Annotations/Internal.hs
@@ -0,0 +1,157 @@
+module HsLua.Annotations.Internal
+  ( annotateFunction,
+    annotateModule,
+  )
+where
+
+import Data.Text (Text)
+import Data.Text qualified as T
+import HsLua.Annotations.Shared (hasReturnType, nameToText, returnType, typeToText, unlinesNonEmpty)
+import HsLua.Core (Name (..))
+import HsLua.Packaging
+
+-- | Create type annotations for a documented function.
+--
+-- See the below example or the generated test annotations [here](https://github.com/sondr3/hslua-annotations/blob/main/test/golden/factorial.lua).
+--
+--
+-- === __Example__
+--
+-- Given the example `factorial` function from HsLua:
+--
+-- @
+-- factorial :: DocumentedFunction Lua.Exception
+-- factorial =
+--   defun "factorial" (liftPure $ \n -> product [1 .. n] :: Integer)
+--     <#> parameter peekIntegral "integer" "n" ""
+--     =#> functionResult pushIntegral "integer" "factorial"
+--     #? "Calculates the factorial of a positive integer."
+-- @
+--
+-- The output of running @annotateFunction factorial@ will be the following string:
+--
+-- @
+-- ---Calculates the factorial of a positive integer.
+-- ---\@param n integer
+-- ---\@return integer
+-- function factorial(n)
+-- @
+--
+-- @since 0.1.0
+annotateFunction :: DocumentedFunction a -> Text
+annotateFunction = annotateFunction' Nothing
+
+-- | Create type annotations for a documented module.
+--
+-- See the below example or the generated test annotations [here](https://github.com/sondr3/hslua-annotations/blob/main/test/golden/path.lua).
+--
+-- === __Example__
+--
+-- Building annotations for @HsLua.Module.Path@ using the exported @documentedModule@
+-- you can generate annotations for the whole module. Note that you probably will
+-- have to narrow the type of @Module a@ to for example @Module Lua.Exception@.
+--
+-- The output of running @annotateModule documentedModule@ will be the following string:
+--
+-- @
+-- ---\@meta path
+-- ---Module for file path manipulations.
+--
+-- ---\@class (exact) path
+-- ---\@field separator string The character that separates directories.
+-- ---\@field search_path_separator string The character that is used to separate the entries in the `PATH` environment variable.
+-- local path = {}
+--
+-- ---Gets the directory name, i.e., removes the last directory separator and everything after from the given path.
+-- ---\@param filepath string path
+-- ---\@return string
+-- function path.directory(filepath)
+--
+-- ---Get the file name.
+-- ---\@param filepath string path
+-- ---\@return string
+-- function path.filename(filepath)
+--
+-- ...snip
+-- @
+--
+-- @since 0.1.0
+annotateModule :: Module a -> Text
+annotateModule (Module {moduleName, moduleDescription, moduleFields, moduleFunctions, moduleOperations}) =
+  unlinesNonEmpty
+    [ "---@meta " <> nameToText moduleName,
+      "---" <> moduleDescription <> "\n",
+      "---@class (exact) " <> nameToText moduleName,
+      T.intercalate "\n" (map fieldDesc moduleFields),
+      T.intercalate "\n" (map opDesc moduleOperations),
+      "local " <> nameToText moduleName <> " = {}\n",
+      T.intercalate "\n" (map (annotateFunction' $ Just moduleName) moduleFunctions)
+    ]
+
+annotateFunction' :: Maybe Name -> DocumentedFunction a -> Text
+annotateFunction' parent (DocumentedFunction {functionName, functionDoc}) = do
+  let name = nameToText functionName
+      desc = functionDesc functionDoc
+      typ = returnType functionDoc
+      paramAnn = paramsDesc functionDoc
+      params = T.intercalate ", " $ paramNames functionDoc
+      paren = maybe "" (\n -> nameToText n <> ".") parent
+   in unlinesNonEmpty
+        [ if T.null desc then mempty else desc,
+          paramAnn,
+          if hasReturnType (funDocResults functionDoc) then "---@return " <> typ else "",
+          "function " <> paren <> name <> "(" <> params <> ")"
+        ]
+
+opDesc :: (Operation, DocumentedFunction a) -> Text
+opDesc (op, DocumentedFunction {functionDoc}) = "---@operator " <> opName op <> ":" <> returnType functionDoc
+
+fieldDesc :: Field a -> Text
+fieldDesc (Field {fieldName, fieldDoc}) = "---@field " <> nameToText fieldName <> " " <> docs fieldDoc
+  where
+    docs (FieldDoc {fieldDocDescription, fieldDocType}) = typeToText fieldDocType <> " " <> fieldDocDescription
+
+functionDesc :: FunctionDoc -> Text
+functionDesc (FunDoc {funDocDescription}) = T.strip . unlinesNonEmpty $ map ("---" <>) $ T.lines funDocDescription
+
+paramNames :: FunctionDoc -> [Text]
+paramNames (FunDoc {funDocParameters}) = map go funDocParameters
+  where
+    go (ParameterDoc {parameterName}) = parameterName
+
+paramsDesc :: FunctionDoc -> Text
+paramsDesc (FunDoc {funDocParameters}) = T.intercalate "\n" $ map paramDesc funDocParameters
+
+paramDesc :: ParameterDoc -> Text
+paramDesc (ParameterDoc {parameterName, parameterType, parameterDescription, parameterIsOptional}) = do
+  let isOpt = if parameterIsOptional then "?" else ""
+      typ = typeToText parameterType
+   in "---@param " <> parameterName <> isOpt <> " " <> typ <> " " <> parameterDescription
+
+opName :: Operation -> Text
+opName = \case
+  Add -> "add"
+  Sub -> "sub"
+  Mul -> "mul"
+  Div -> "div"
+  Mod -> "mod"
+  Pow -> "pow"
+  Unm -> "unm"
+  Idiv -> "idiv"
+  Band -> "band"
+  Bor -> "bor"
+  Bxor -> "bxor"
+  Bnot -> "bnot"
+  Shl -> "shl"
+  Shr -> "shr"
+  Concat -> "concat"
+  Len -> "len"
+  Eq -> "eq"
+  Lt -> "lt"
+  Le -> "le"
+  Index -> "index"
+  Newindex -> "newindex"
+  Call -> "call"
+  Tostring -> "tostring"
+  Pairs -> "pairs"
+  CustomOperation x -> nameToText x
diff --git a/lib/HsLua/Annotations/Markdown.hs b/lib/HsLua/Annotations/Markdown.hs
new file mode 100644
--- /dev/null
+++ b/lib/HsLua/Annotations/Markdown.hs
@@ -0,0 +1,184 @@
+module HsLua.Annotations.Markdown
+  ( documentFunction,
+    documentModule,
+  )
+where
+
+import Data.Bool (bool)
+import Data.Text (Text)
+import Data.Text qualified as T
+import Data.Version (showVersion)
+import HsLua.Annotations.Shared (hasReturnType, nameToText, returnType, typeToText, unlinesNonEmpty)
+import HsLua.Core
+import HsLua.Core.Utf8 qualified as Utf8
+import HsLua.Packaging
+
+-- | Create markdown documentation for a documented function.
+--
+-- See the below example or the generated test annotations [here](https://github.com/sondr3/hslua-annotations/blob/main/test/golden/factorial.md).
+--
+-- === __Example__
+--
+-- Given the example `factorial` function from HsLua:
+--
+-- @
+-- factorial :: DocumentedFunction Lua.Exception
+-- factorial =
+--   defun "factorial" (liftPure $ \n -> product [1 .. n] :: Integer)
+--     <#> parameter peekIntegral "integer" "n" ""
+--     =#> functionResult pushIntegral "integer" "factorial"
+--     #? "Calculates the factorial of a positive integer."
+-- @
+--
+-- The output of running @documentFunction factorial@ will be the following string:
+--
+-- @
+-- `function factorial(n)`
+--
+-- #### Parameters
+--
+-- | Name | Type | Description |
+-- | ---- | ---- | ----------- |
+-- | `n` | `integer` |  |
+--
+-- #### Returns
+--
+-- | Type | Description |
+-- | ---- | ----------- |
+-- | `integer` | factorial |
+--
+-- Calculates the factorial of a positive integer.
+--
+-- ```lua
+-- function factorial(n: integer): integer
+-- ```
+--
+-- *Since: 1.0.0*
+-- @
+--
+-- @since 0.1.0
+documentFunction :: DocumentedFunction e -> Text
+documentFunction fn =
+  let fnDoc = functionDoc fn
+      fnName = Utf8.toText $ fromName (functionName fn)
+      name =
+        if T.null fnName
+          then "<anonymous function>"
+          else fnName
+   in unlinesNonEmpty
+        [ "`function " <> name <> "(" <> renderFunctionParams fnDoc <> ")" <> "`\n",
+          renderFunctionDoc name fnDoc
+        ]
+
+-- | Create markdown documentation for a documented module.
+--
+-- See the below example or the generated test annotations [here](https://github.com/sondr3/hslua-annotations/blob/main/test/golden/path.md).
+--
+-- === __Example__
+--
+-- Building documentation for @HsLua.Module.Path@ using the exported @documentedModule@
+-- you can generate Markdown documentation for the whole module. Note that you probably will
+-- have to narrow the type of @Module a@ to for example @Module Lua.Exception@.
+--
+-- The output of running @documentModule documentedModule@ on it will be the
+-- following string:
+--
+-- @
+-- # `path`
+-- Module for file path manipulations.
+--
+-- ## Fields
+--
+-- ### `separator`
+-- The character that separates directories.
+--
+-- ```lua
+-- path.separator: string
+-- ```
+--
+-- ...snip
+-- @
+--
+-- @since 0.1.0
+documentModule :: Module e -> Text
+documentModule (Module {moduleFields, moduleName, moduleDescription, moduleFunctions}) =
+  unlinesNonEmpty
+    [ "# `" <> nameToText moduleName <> "`",
+      moduleDescription,
+      renderFields moduleName moduleFields,
+      renderFunctions moduleFunctions
+    ]
+
+renderFunctions :: [DocumentedFunction e] -> Text
+renderFunctions [] = mempty
+renderFunctions fs = "## Functions\n\n" <> T.intercalate "\n" (map (("### " <>) . documentFunction) fs)
+
+renderFunctionParams :: FunctionDoc -> Text
+renderFunctionParams (FunDoc {funDocParameters}) = T.intercalate ", " $ map parameterName funDocParameters
+
+renderFields :: Name -> [Field e] -> Text
+renderFields _ [] = mempty
+renderFields name fs =
+  unlinesNonEmpty
+    [ "\n## Fields",
+      T.intercalate "\n" (map (renderField name) fs)
+    ]
+
+renderField :: Name -> Field e -> Text
+renderField modName (Field {fieldName, fieldDoc}) = do
+  let name = nameToText fieldName
+      parent = nameToText modName
+      typ = typeToText $ fieldDocType fieldDoc
+  unlinesNonEmpty
+    [ "\n### `" <> name <> "`",
+      fieldDocDescription fieldDoc,
+      "\n```lua\n" <> parent <> "." <> name <> ": " <> typ <> "\n```"
+    ]
+
+renderFunctionDoc :: Text -> FunctionDoc -> Text
+renderFunctionDoc name fd@(FunDoc {funDocDescription, funDocSince, funDocParameters, funDocResults}) =
+  let sinceTag = case funDocSince of
+        Nothing -> mempty
+        Just version -> T.pack $ "*Since: " <> showVersion version <> "*"
+   in unlinesNonEmpty
+        [ renderParamTable funDocParameters,
+          renderResultsDoc funDocResults,
+          funDocDescription,
+          "\n```lua\n" <> "function " <> name <> renderParamType funDocParameters <> (if hasReturnType funDocResults then ": " <> returnType fd else "") <> "\n```\n",
+          sinceTag
+        ]
+
+renderParamTable :: [ParameterDoc] -> Text
+renderParamTable [] = mempty
+renderParamTable ps =
+  T.unlines
+    [ "#### Parameters\n",
+      "| Name | Type | Description |",
+      "| ---- | ---- | ----------- |",
+      T.intercalate "\n" (map renderParamField ps)
+    ]
+
+renderParamType :: [ParameterDoc] -> Text
+renderParamType [] = "()"
+renderParamType ps = "(" <> T.intercalate ", " (map (\p -> parameterName p <> bool "" "?" (parameterIsOptional p) <> ": " <> typeToText (parameterType p)) ps) <> ")"
+
+renderParamField :: ParameterDoc -> Text
+renderParamField (ParameterDoc {parameterName, parameterDescription, parameterType, parameterIsOptional}) =
+  "| `" <> parameterName <> "` | `" <> typeToText parameterType <> bool "" "?" parameterIsOptional <> "` | " <> parameterDescription <> " |"
+
+renderResultsDoc :: ResultsDoc -> Text
+renderResultsDoc (ResultsDocList []) = mempty
+renderResultsDoc (ResultsDocList rds) =
+  unlinesNonEmpty
+    [ "#### Returns\n",
+      "| Type | Description |",
+      "| ---- | ----------- |",
+      T.intercalate "\n" (map renderResultValueDoc rds)
+    ]
+renderResultsDoc (ResultsDocMult txt) = " -  " <> indent 4 txt
+
+renderResultValueDoc :: ResultValueDoc -> Text
+renderResultValueDoc (ResultValueDoc {resultValueType, resultValueDescription}) = "| `" <> typeToText resultValueType <> "` | " <> resultValueDescription <> " |"
+
+indent :: Int -> Text -> Text
+indent n = T.replace "\n" (T.replicate n " ")
diff --git a/lib/HsLua/Annotations/Shared.hs b/lib/HsLua/Annotations/Shared.hs
new file mode 100644
--- /dev/null
+++ b/lib/HsLua/Annotations/Shared.hs
@@ -0,0 +1,51 @@
+module HsLua.Annotations.Shared
+  ( nameToText,
+    typeToText,
+    unlinesNonEmpty,
+    hasReturnType,
+    returnType,
+  )
+where
+
+import Data.Char (toLower)
+import Data.Text (Text)
+import Data.Text qualified as T
+import Data.Text.Encoding (decodeUtf8)
+import HsLua.Core (Name (..), Type (..))
+import HsLua.Packaging
+
+unlinesNonEmpty :: [Text] -> Text
+unlinesNonEmpty xs = T.unlines $ filter (not . T.null) xs
+
+nameToText :: Name -> Text
+nameToText = decodeUtf8 . fromName
+
+typeToText :: TypeSpec -> Text
+typeToText = \case
+  BasicType t -> basicTypeName t
+  NamedType nt -> decodeUtf8 $ fromName nt
+  AnyType -> "any"
+  FunType {} -> "function"
+  RecType {} -> "table"
+  SeqType t -> typeToText t <> "[]"
+  SumType specs -> T.intercalate "|" (map typeToText specs)
+
+basicTypeName :: Type -> Text
+basicTypeName = \case
+  TypeLightUserdata -> "light userdata"
+  t -> T.pack $ map toLower . drop 4 $ show t
+
+hasReturnType :: ResultsDoc -> Bool
+hasReturnType (ResultsDocList []) = False
+hasReturnType (ResultsDocList _) = True
+hasReturnType _ = False
+
+returnType :: FunctionDoc -> Text
+returnType (FunDoc {funDocResults}) = go funDocResults
+  where
+    go (ResultsDocList [rs]) = resultValueDesc rs
+    go (ResultsDocList xs) = T.intercalate "|" $ map resultValueDesc xs
+    go (ResultsDocMult _) = ""
+
+resultValueDesc :: ResultValueDoc -> Text
+resultValueDesc (ResultValueDoc {resultValueType}) = typeToText resultValueType
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,52 @@
+module Main (main) where
+
+import Data.Text (Text)
+import Data.Text.Lazy (fromStrict)
+import Data.Text.Lazy.Encoding (encodeUtf8)
+import Data.Version (makeVersion)
+import HsLua.Annotations (annotateFunction, annotateModule, documentFunction, documentModule)
+import HsLua.Core qualified as Lua
+import HsLua.Marshalling (peekIntegral, pushIntegral)
+import HsLua.Module.Path qualified as Path
+import HsLua.Module.Version qualified as Version
+import HsLua.Packaging
+import Test.Tasty (TestTree, defaultMain, testGroup)
+import Test.Tasty.Golden
+
+main :: IO ()
+main = defaultMain tests
+
+tests :: TestTree
+tests = testGroup "Tests" [annotations, markdown]
+
+annotations :: TestTree
+annotations =
+  testGroup
+    "Annotations"
+    [ goldenVsString "factorial" "test/golden/factorial.lua" (pure $ encodeUtf8 (fromStrict $ annotateFunction factorial)),
+      goldenVsString "path" "test/golden/path.lua" (pure $ encodeUtf8 $ fromStrict (annModule Path.documentedModule)),
+      goldenVsString "version" "test/golden/version.lua" (pure $ encodeUtf8 $ fromStrict (annModule Version.documentedModule))
+    ]
+  where
+    annModule :: Module Lua.Exception -> Text
+    annModule = annotateModule
+
+markdown :: TestTree
+markdown =
+  testGroup
+    "Markdown"
+    [ goldenVsString "factorial" "test/golden/factorial.md" (pure $ encodeUtf8 (fromStrict $ documentFunction factorial)),
+      goldenVsString "path" "test/golden/path.md" (pure $ encodeUtf8 $ fromStrict (docModule Path.documentedModule)),
+      goldenVsString "version" "test/golden/version.md" (pure $ encodeUtf8 $ fromStrict (docModule Version.documentedModule))
+    ]
+  where
+    docModule :: Module Lua.Exception -> Text
+    docModule = documentModule
+
+factorial :: DocumentedFunction Lua.Exception
+factorial =
+  defun "factorial" (liftPure $ \n -> product [1 .. n] :: Integer)
+    <#> parameter peekIntegral "integer" "n" ""
+    =#> functionResult pushIntegral "integer" "factorial"
+    #? "Calculates the factorial of a positive integer."
+    `since` makeVersion [1, 0, 0]
