Repository: segmentio/terraform-docs
Pull request #208 (2020-02-25 22:24:12)

Add extensive tests coverage for all the packages
### Prerequisites

Put an `x` into the box(es) that apply:

- [ ] This pull request fixes a bug.
- [ ] This pull request adds a feature.
- [x] This pull request enhances existing functionality.
- [ ] This pull request introduces breaking change.

For more information, see the [Contributing Guide](https://github.com/segmentio/terraform-docs/tree/master/CONTRIBUTING.md).

### Description

Add more test coverage inside various packages.

### Issues Resolved

List any existing issues this pull request resolves.

### Checklist

Put an `x` into all boxes that apply:

- [ ] I have read the [Contributing Guidelines](https://github.com/segmentio/terraform-docs/tree/master/CONTRIBUTING.md).

#### Tests

- [x] I have added tests to cover my changes.
- [x] All tests pass when I run `make test`.

#### Documentation

- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.

#### Code Style

- [ ] My code follows the code style of this project.


codecov-io: # [Codecov](https://codecov.io/gh/segmentio/terraform-docs/pull/208?src=pr&el=h1) Report
> Merging [#208](https://codecov.io/gh/segmentio/terraform-docs/pull/208?src=pr&el=desc) into [master](https://codecov.io/gh/segmentio/terraform-docs/commit/755495a0ba9006515d969246620f595c5dd7d61e?src=pr&el=desc) will **decrease** coverage by `23.59%`.
> The diff coverage is `66.66%`.

```diff
@@            Coverage Diff             @@
##           master     #208      +/-   ##
==========================================
- Coverage   73.75%   50.15%   -23.6%     
==========================================
  Files           8       18      +10     
  Lines         320      626     +306     
==========================================
+ Hits          236      314      +78     
- Misses         78      302     +224     
- Partials        6       10       +4
```


| [Impacted Files](https://codecov.io/gh/segmentio/terraform-docs/pull/208?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [internal/module/module.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL21vZHVsZS5nbw==) | `0% <0%> (ø)` | |
| [internal/types/types.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvdHlwZXMvdHlwZXMuZ28=) | `45.03% <100%> (ø)` | :arrow_up: |
| [internal/reader/lines.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcmVhZGVyL2xpbmVzLmdv) | `63.63% <80%> (ø)` | |
| [pkg/tfconf/module.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-cGtnL3RmY29uZi9tb2R1bGUuZ28=) | `0% <0%> (ø)` | |
| [internal/module/provider.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL3Byb3ZpZGVyLmdv) | `100% <0%> (ø)` | |
| [internal/module/input.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL2lucHV0Lmdv) | `83.33% <0%> (ø)` | |
| [internal/module/output.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL291dHB1dC5nbw==) | `100% <0%> (ø)` | |
| [pkg/tfconf/output.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-cGtnL3RmY29uZi9vdXRwdXQuZ28=) | `19.44% <0%> (ø)` | |
| [pkg/tfconf/provider.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-cGtnL3RmY29uZi9wcm92aWRlci5nbw==) | `100% <0%> (ø)` | |
| ... and [3 more](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree-more) | |

ndhoule: # [Codecov](https://codecov.io/gh/segmentio/terraform-docs/pull/208?src=pr&el=h1) Report
> Merging [#208](https://codecov.io/gh/segmentio/terraform-docs/pull/208?src=pr&el=desc) into [master](https://codecov.io/gh/segmentio/terraform-docs/commit/755495a0ba9006515d969246620f595c5dd7d61e&el=desc) will **decrease** coverage by `21.49%`.
> The diff coverage is `n/a`.

```diff
@@             Coverage Diff             @@
##           master     #208       +/-   ##
===========================================
- Coverage   73.75%   52.25%   -21.50%     
===========================================
  Files           8       18       +10     
  Lines         320      622      +302     
===========================================
+ Hits          236      325       +89     
- Misses         78      288      +210     
- Partials        6        9        +3     
```


| [Impacted Files](https://codecov.io/gh/segmentio/terraform-docs/pull/208?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [internal/module/output.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL291dHB1dC5nbw==) | `100.00% <0.00%> (ø)` | |
| [pkg/tfconf/output.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-cGtnL3RmY29uZi9vdXRwdXQuZ28=) | `19.44% <0.00%> (ø)` | |
| [internal/module/module.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL21vZHVsZS5nbw==) | `0.00% <0.00%> (ø)` | |
| [pkg/tfconf/provider.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-cGtnL3RmY29uZi9wcm92aWRlci5nbw==) | `100.00% <0.00%> (ø)` | |
| [internal/module/provider.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL3Byb3ZpZGVyLmdv) | `100.00% <0.00%> (ø)` | |
| [internal/module/options.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL29wdGlvbnMuZ28=) | `83.33% <0.00%> (ø)` | |
| [internal/module/input.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL2lucHV0Lmdv) | `100.00% <0.00%> (ø)` | |
| [internal/reader/lines.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcmVhZGVyL2xpbmVzLmdv) | `100.00% <0.00%> (ø)` | |
| [pkg/tfconf/module.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-cGtnL3RmY29uZi9tb2R1bGUuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/tfconf/input.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-cGtnL3RmY29uZi9pbnB1dC5nbw==) | `77.77% <0.00%> (ø)` | |

codecov-io: # [Codecov](https://codecov.io/gh/segmentio/terraform-docs/pull/208?src=pr&el=h1) Report
> Merging [#208](https://codecov.io/gh/segmentio/terraform-docs/pull/208?src=pr&el=desc) into [master](https://codecov.io/gh/segmentio/terraform-docs/commit/755495a0ba9006515d969246620f595c5dd7d61e?src=pr&el=desc) will **decrease** coverage by `10.72%`.
> The diff coverage is `76.47%`.

```diff
@@             Coverage Diff             @@
##           master     #208       +/-   ##
===========================================
- Coverage   73.75%   63.02%   -10.73%     
===========================================
  Files           8       18       +10     
  Lines         320      622      +302     
===========================================
+ Hits          236      392      +156     
- Misses         78      219      +141     
- Partials        6       11        +5
```


| [Impacted Files](https://codecov.io/gh/segmentio/terraform-docs/pull/208?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [internal/module/module.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL21vZHVsZS5nbw==) | `0% <0%> (ø)` | |
| [internal/module/input.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL2lucHV0Lmdv) | `100% <100%> (ø)` | |
| [internal/types/types.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvdHlwZXMvdHlwZXMuZ28=) | `96.18% <100%> (+51.14%)` | :arrow_up: |
| [internal/reader/lines.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcmVhZGVyL2xpbmVzLmdv) | `100% <100%> (ø)` | |
| [internal/module/options.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL29wdGlvbnMuZ28=) | `83.33% <66.66%> (ø)` | |
| [pkg/tfconf/module.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-cGtnL3RmY29uZi9tb2R1bGUuZ28=) | `0% <0%> (ø)` | |
| [internal/module/provider.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL3Byb3ZpZGVyLmdv) | `100% <0%> (ø)` | |
| [internal/module/output.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL291dHB1dC5nbw==) | `100% <0%> (ø)` | |
| [pkg/tfconf/output.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-cGtnL3RmY29uZi9vdXRwdXQuZ28=) | `19.44% <0%> (ø)` | |
| ... and [6 more](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree-more) | |

ndhoule: # [Codecov](https://codecov.io/gh/segmentio/terraform-docs/pull/208?src=pr&el=h1) Report
> Merging [#208](https://codecov.io/gh/segmentio/terraform-docs/pull/208?src=pr&el=desc) into [master](https://codecov.io/gh/segmentio/terraform-docs/commit/755495a0ba9006515d969246620f595c5dd7d61e&el=desc) will **decrease** coverage by `10.72%`.
> The diff coverage is `n/a`.

```diff
@@             Coverage Diff             @@
##           master     #208       +/-   ##
===========================================
- Coverage   73.75%   63.02%   -10.73%     
===========================================
  Files           8       18       +10     
  Lines         320      622      +302     
===========================================
+ Hits          236      392      +156     
- Misses         78      219      +141     
- Partials        6       11        +5     
```


| [Impacted Files](https://codecov.io/gh/segmentio/terraform-docs/pull/208?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [internal/module/provider.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL3Byb3ZpZGVyLmdv) | `100.00% <0.00%> (ø)` | |
| [internal/module/input.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL2lucHV0Lmdv) | `100.00% <0.00%> (ø)` | |
| [pkg/tfconf/output.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-cGtnL3RmY29uZi9vdXRwdXQuZ28=) | `19.44% <0.00%> (ø)` | |
| [internal/module/options.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL29wdGlvbnMuZ28=) | `83.33% <0.00%> (ø)` | |
| [internal/reader/lines.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcmVhZGVyL2xpbmVzLmdv) | `100.00% <0.00%> (ø)` | |
| [pkg/tfconf/module.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-cGtnL3RmY29uZi9tb2R1bGUuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/tfconf/input.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-cGtnL3RmY29uZi9pbnB1dC5nbw==) | `77.77% <0.00%> (ø)` | |
| [internal/module/output.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL291dHB1dC5nbw==) | `100.00% <0.00%> (ø)` | |
| [pkg/tfconf/provider.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-cGtnL3RmY29uZi9wcm92aWRlci5nbw==) | `100.00% <0.00%> (ø)` | |
| [internal/module/module.go](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree#diff-aW50ZXJuYWwvbW9kdWxlL21vZHVsZS5nbw==) | `0.00% <0.00%> (ø)` | |
| ... and [1 more](https://codecov.io/gh/segmentio/terraform-docs/pull/208/diff?src=pr&el=tree-more) | |


Patch:
diff --git a/.editorconfig b/.editorconfig
index 00eaa50..8cb8f1f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -20,6 +20,10 @@ indent_size = 4
 trim_trailing_whitespace = false
 insert_final_newline = false
 
+[*.expected]
+trim_trailing_whitespace = false
+insert_final_newline = false
+
 [*.{yaml,yml}]
 indent_size = 2
 
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 32a9e34..fe26d50 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -11,10 +11,10 @@ jobs:
     name: Validate
     runs-on: ubuntu-latest
     steps:
-    - name: Set up Go 1.13.x
+    - name: Set up Go 1.14.x
       uses: actions/setup-go@v1
       with:
-        go-version: 1.13.x
+        go-version: 1.14.x
       id: go
 
     - name: Check out code into the Go module directory
@@ -37,20 +37,21 @@ jobs:
     name: Test
     runs-on: ubuntu-latest
     steps:
-    - name: Set up Go 1.13.x
+    - name: Set up Go 1.14.x
       uses: actions/setup-go@v1
       with:
-        go-version: 1.13.x
+        go-version: 1.14.x
       id: go
 
     - name: Check out code into the Go module directory
       uses: actions/checkout@v1
 
     - name: Run tests
-      run: make test-coverage
+      run: make test
 
     - name: Upload coverage to Codecov
       uses: codecov/codecov-action@v1
+      if: always()
       with:
         # Currently we're not able to build Codecov report for
         # PRs originates from the fork. There's an issue to track
@@ -67,10 +68,10 @@ jobs:
     name: Build
     runs-on: ubuntu-latest
     steps:
-    - name: Set up Go 1.13.x
+    - name: Set up Go 1.14.x
       uses: actions/setup-go@v1
       with:
-        go-version: 1.13.x
+        go-version: 1.14.x
       id: go
 
     - name: Check out code into the Go module directory
diff --git a/.github/workflows/prerelease.yaml b/.github/workflows/prerelease.yaml
index 5d503b0..24413a9 100644
--- a/.github/workflows/prerelease.yaml
+++ b/.github/workflows/prerelease.yaml
@@ -10,10 +10,10 @@ jobs:
     name: Pre-Release
     runs-on: ubuntu-latest
     steps:
-    - name: Set up Go 1.13.x
+    - name: Set up Go 1.14.x
       uses: actions/setup-go@v1
       with:
-        go-version: 1.13.x
+        go-version: 1.14.x
       id: go
 
     - name: Check out code into the Go module directory
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 536fe0a..ba33672 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -11,10 +11,10 @@ jobs:
     name: Release
     runs-on: ubuntu-latest
     steps:
-    - name: Set up Go 1.13.x
+    - name: Set up Go 1.14.x
       uses: actions/setup-go@v1
       with:
-        go-version: 1.13.x
+        go-version: 1.14.x
       id: go
 
     - name: Check out code into the Go module directory
diff --git a/Makefile b/Makefile
index df585fd..cacaa8a 100644
--- a/Makefile
+++ b/Makefile
@@ -10,10 +10,11 @@ LICENSE     := MIT
 PACKAGE     := github.com/$(VENDOR)/$(NAME)
 
 # Build variables
-BUILD_DIR   := bin
-COMMIT_HASH ?= $(shell git rev-parse --short HEAD 2>/dev/null)
-BUILD_DATE  ?= $(shell date +%FT%T%z)
-VERSION     ?= $(shell git describe --tags --exact-match 2>/dev/null || git describe --tags 2>/dev/null || echo "v0.0.0-$(COMMIT_HASH)")
+BUILD_DIR    := bin
+COMMIT_HASH  ?= $(shell git rev-parse --short HEAD 2>/dev/null)
+BUILD_DATE   ?= $(shell date +%FT%T%z)
+VERSION      ?= $(shell git describe --tags --exact-match 2>/dev/null || git describe --tags 2>/dev/null || echo "v0.0.0-$(COMMIT_HASH)")
+COVERAGE_OUT := coverage.txt
 
 # Go variables
 GOCMD       := GO111MODULE=on go
@@ -34,7 +35,7 @@ GORUN       ?= GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOCMD) run $(MODVENDOR)
 
 # Binary versions
 GITCHGLOG_VERSION := 0.9.1
-GOLANGCI_VERSION  := v1.23.1
+GOLANGCI_VERSION  := v1.23.7
 
 .PHONY: all
 all: clean verify checkfmt lint test build
@@ -53,7 +54,7 @@ checkfmt: ## Check formatting of all go files
 .PHONY: clean
 clean: ## Clean workspace
 	@ $(MAKE) --no-print-directory log-$@
-	rm -rf ./$(BUILD_DIR)
+	rm -rf ./$(BUILD_DIR) ./$(COVERAGE_OUT)
 
 .PHONY: deps
 deps: vendor ## Install dependencies
@@ -71,12 +72,7 @@ lint: ## Run linter
 .PHONY: test
 test: ## Run tests
 	@ $(MAKE) --no-print-directory log-$@
-	$(GOCMD) test $(MODVENDOR) -v $(GOPKGS)
-
-.PHONY: test-coverage
-test-coverage: ## Run tests with code coverage
-	@ $(MAKE) --no-print-directory log-$@
-	$(GOCMD) test -race -coverprofile=coverage.txt -covermode=atomic $(MODVENDOR) -v $(GOPKGS)
+	$(GOCMD) test -race -coverprofile=$(COVERAGE_OUT) -covermode=atomic $(MODVENDOR) -v $(GOPKGS)
 
 .PHONY: vendor
 vendor: ## Install 'vendor' dependencies
diff --git a/README.md b/README.md
index 1ce25d6..72846ef 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ If you are a Mac OS X user, you can use [Homebrew](https://brew.sh):
 brew install terraform-docs
 ```
 
-**NOTE:** please use the latest go to do this, ideally go 1.13.5 or greater.
+**NOTE:** please use the latest go to do this, we use 1.14 but ideally go 1.13.5 or greater.
 
 This will put `terraform-docs` in `$(go env GOPATH)/bin`. If you encounter the error `terraform-docs: command not found` after installation then you may need to either add that directory to your `$PATH` as shown [here](https://golang.org/doc/code.html#GOPATH) or do a manual installation by cloning the repo and run `make build` from the repository which will put `terraform-docs` in:
 
@@ -89,7 +89,7 @@ Visit [./docs](./docs/) for all documentation.
 
 ## Development Requirements
 
-- [Go](https://golang.org/) 1.13+
+- [Go](https://golang.org/) 1.14 (ideally 1.13+)
 - [goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports)
 - [git-chlog](https://github.com/git-chglog/git-chglog)
 - [golangci-lint](https://github.com/golangci/golangci-lint)
diff --git a/cmd/root.go b/cmd/root.go
index 70623a0..4d84e15 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -79,13 +79,16 @@ func FormatterCmds() []*cobra.Command {
 }
 
 func doPrint(path string, printer print.Format) error {
-	options.With(&module.Options{
+	_, err := options.With(&module.Options{
 		Path: path,
 		SortBy: &module.SortBy{
 			Name:     settings.SortByName,
 			Required: settings.SortByRequired,
 		},
 	})
+	if err != nil {
+		return err
+	}
 	tfmodule, err := module.LoadWithOptions(options)
 	if err != nil {
 		return err
diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
index f240a16..53904cb 100644
--- a/docs/USER_GUIDE.md
+++ b/docs/USER_GUIDE.md
@@ -1,3 +1,20 @@
 # User Guide
 
 TBA
+
+## Integrating With Your Terraform Repository
+
+A simple git hook `.git/hooks/pre-commit` added to your local terraform repository can keep your Terraform module documentation up to date whenever you make a commit. See also [ git hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) documentation.
+
+```sh
+#!/bin/sh
+
+# Keep module docs up to date
+for d in $(ls -1 modules)
+do
+  terraform-docs md modules/$d > modules/$d/README.md
+  if [ $? -eq 0 ] ; then
+    git add "./modules/$d/README.md"
+  fi
+done
+```
diff --git a/go.mod b/go.mod
index 800e5cc..80253b5 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/segmentio/terraform-docs
 
-go 1.13
+go 1.14
 
 require (
 	github.com/hashicorp/terraform-config-inspect v0.0.0-20200210163047-f7d8399e1194
diff --git a/internal/format/util.go b/internal/format/util.go
index 455edf1..2f3f952 100644
--- a/internal/format/util.go
+++ b/internal/format/util.go
@@ -11,14 +11,14 @@ func sanitize(markdown string) string {
 	result := markdown
 
 	// Preserve double spaces at the end of the line
-	result = regexp.MustCompile(` {2}(\r?\n)`).ReplaceAllString(result, "‡‡$1")
+	result = regexp.MustCompile(` {2}(\r?\n)`).ReplaceAllString(result, "‡‡‡DOUBLESPACES‡‡‡$1")
 
 	// Remove trailing spaces from the end of lines
 	result = regexp.MustCompile(` +(\r?\n)`).ReplaceAllString(result, "$1")
 	result = regexp.MustCompile(` +$`).ReplaceAllLiteralString(result, "")
 
 	// Preserve double spaces at the end of the line
-	result = regexp.MustCompile(`‡‡(\r?\n)`).ReplaceAllString(result, "  $1")
+	result = regexp.MustCompile(`‡‡‡DOUBLESPACES‡‡‡(\r?\n)`).ReplaceAllString(result, "  $1")
 
 	// Remove blank line with only double spaces in it
 	result = regexp.MustCompile(`(\r?\n)  (\r?\n)`).ReplaceAllString(result, "$1")
diff --git a/internal/module/input.go b/internal/module/input.go
index f0b176c..de4d6a7 100644
--- a/internal/module/input.go
+++ b/internal/module/input.go
@@ -17,17 +17,10 @@ type inputsSortedByRequired []*tfconf.Input
 func (a inputsSortedByRequired) Len() int      { return len(a) }
 func (a inputsSortedByRequired) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
 func (a inputsSortedByRequired) Less(i, j int) bool {
-	switch {
-	// i required, j not: i gets priority
-	case !a[i].HasDefault() && a[j].HasDefault():
-		return true
-	// j required, i not: i does not get priority
-	case a[i].HasDefault() && !a[j].HasDefault():
-		return false
-	// Otherwise, sort by name
-	default:
+	if a[i].HasDefault() == a[j].HasDefault() {
 		return a[i].Name < a[j].Name
 	}
+	return !a[i].HasDefault() && a[j].HasDefault()
 }
 
 type inputsSortedByPosition []*tfconf.Input
diff --git a/internal/module/module.go b/internal/module/module.go
index a7f5405..e22c9f5 100644
--- a/internal/module/module.go
+++ b/internal/module/module.go
@@ -4,7 +4,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
-	"log"
 	"os/exec"
 	"path/filepath"
 	"sort"
@@ -19,14 +18,36 @@ import (
 // LoadWithOptions returns new instance of Module with all the inputs and
 // outputs discovered from provided 'path' containing Terraform config
 func LoadWithOptions(options *Options) (*tfconf.Module, error) {
-	tfmodule := loadModule(options.Path)
+	tfmodule, err := loadModule(options.Path)
+	if err != nil {
+		return nil, err
+	}
+	module, err := loadModuleItems(tfmodule, options)
+	if err != nil {
+		return nil, err
+	}
+	sortItems(module, options.SortBy)
+	return module, nil
+}
+
+func loadModule(path string) (*tfconfig.Module, error) {
+	module, diag := tfconfig.LoadModule(path)
+	if diag != nil && diag.HasErrors() {
+		return nil, diag
+	}
+	return module, nil
+}
 
+func loadModuleItems(tfmodule *tfconfig.Module, options *Options) (*tfconf.Module, error) {
 	header := loadHeader(options.Path)
 	inputs, required, optional := loadInputs(tfmodule)
-	outputs := loadOutputs(tfmodule, options)
+	outputs, err := loadOutputs(tfmodule, options)
+	if err != nil {
+		return nil, err
+	}
 	providers := loadProviders(tfmodule)
 
-	module := &tfconf.Module{
+	return &tfconf.Module{
 		Header:    header,
 		Inputs:    inputs,
 		Outputs:   outputs,
@@ -34,17 +55,7 @@ func LoadWithOptions(options *Options) (*tfconf.Module, error) {
 
 		RequiredInputs: required,
 		OptionalInputs: optional,
-	}
-	sortItems(module, options.SortBy)
-	return module, nil
-}
-
-func loadModule(path string) *tfconfig.Module {
-	module, diag := tfconfig.LoadModule(path)
-	if diag != nil && diag.HasErrors() {
-		log.Fatal(diag)
-	}
-	return module
+	}, nil
 }
 
 func loadHeader(path string) string {
@@ -87,7 +98,7 @@ func loadInputs(tfmodule *tfconfig.Module) ([]*tfconf.Input, []*tfconf.Input, []
 	for _, input := range tfmodule.Variables {
 		inputDescription := input.Description
 		if inputDescription == "" {
-			inputDescription = loadComments(input.Pos.Filename, input.Pos.Line-1)
+			inputDescription = loadComments(input.Pos.Filename, input.Pos.Line)
 		}
 
 		i := &tfconf.Input{
@@ -111,20 +122,20 @@ func loadInputs(tfmodule *tfconfig.Module) ([]*tfconf.Input, []*tfconf.Input, []
 	return inputs, required, optional
 }
 
-func loadOutputs(tfmodule *tfconfig.Module, options *Options) []*tfconf.Output {
+func loadOutputs(tfmodule *tfconfig.Module, options *Options) ([]*tfconf.Output, error) {
 	outputs := make([]*tfconf.Output, 0, len(tfmodule.Outputs))
 	values := make(map[string]*TerraformOutput, 0)
 	if options.OutputValues {
 		var err error
 		values, err = loadOutputValues(options)
 		if err != nil {
-			log.Fatal(err)
+			return nil, err
 		}
 	}
 	for _, o := range tfmodule.Outputs {
 		description := o.Description
 		if description == "" {
-			description = loadComments(o.Pos.Filename, o.Pos.Line-1)
+			description = loadComments(o.Pos.Filename, o.Pos.Line)
 		}
 		output := &tfconf.Output{
 			Name:        o.Name,
@@ -145,7 +156,7 @@ func loadOutputs(tfmodule *tfconfig.Module, options *Options) []*tfconf.Output {
 		}
 		outputs = append(outputs, output)
 	}
-	return outputs
+	return outputs, nil
 }
 
 func loadOutputValues(options *Options) (map[string]*TerraformOutput, error) {
diff --git a/internal/module/options.go b/internal/module/options.go
index 2412175..9a9871c 100644
--- a/internal/module/options.go
+++ b/internal/module/options.go
@@ -1,7 +1,7 @@
 package module
 
 import (
-	"log"
+	"errors"
 
 	"github.com/imdario/mergo"
 )
@@ -32,9 +32,12 @@ func NewOptions() *Options {
 }
 
 // With override options with existing Options
-func (o *Options) With(override *Options) *Options {
-	if err := mergo.Merge(o, override); err != nil {
-		log.Fatal(err)
+func (o *Options) With(override *Options) (*Options, error) {
+	if override == nil {
+		return nil, errors.New("cannot use nil as override value")
 	}
-	return o
+	if err := mergo.Merge(o, *override); err != nil {
+		return nil, err
+	}
+	return o, nil
 }
diff --git a/internal/reader/lines.go b/internal/reader/lines.go
index 0f305f3..86af23a 100644
--- a/internal/reader/lines.go
+++ b/internal/reader/lines.go
@@ -28,16 +28,18 @@ func (l *Lines) Extract() ([]string, error) {
 	defer func() {
 		_ = f.Close()
 	}()
+	return l.extract(f)
+}
 
-	bf := bufio.NewReader(f)
+func (l *Lines) extract(r io.Reader) ([]string, error) {
+	bf := bufio.NewReader(r)
 	var lines = make([]string, 0)
-
 	for lnum := 0; ; lnum++ {
-		if l.LineNum != -1 && lnum >= l.LineNum {
+		if l.LineNum != -1 && lnum >= l.LineNum-1 {
 			break
 		}
 		line, err := bf.ReadString('\n')
-		if err == io.EOF {
+		if err == io.EOF && line == "" {
 			switch lnum {
 			case 0:
 				return nil, errors.New("no lines in file")
@@ -47,9 +49,6 @@ func (l *Lines) Extract() ([]string, error) {
 				return nil, fmt.Errorf("only %d lines", lnum)
 			}
 		}
-		if err != nil {
-			return nil, err
-		}
 		if l.Condition(line) {
 			if extracted, capture := l.Parser(line); capture {
 				lines = append(lines, extracted)
@@ -60,6 +59,5 @@ func (l *Lines) Extract() ([]string, error) {
 			lines = nil
 		}
 	}
-
 	return lines, nil
 }
diff --git a/internal/types/types.go b/internal/types/types.go
index b79c4ff..ab51032 100644
--- a/internal/types/types.go
+++ b/internal/types/types.go
@@ -196,8 +196,8 @@ func (b Bool) HasDefault() bool {
 // List represents a 'list' of values
 type List []interface{}
 
-// nolint
-func (l List) underlying() []interface{} {
+// Underlying returns the underlying elements in the form of '[]interface {}'
+func (l List) Underlying() []interface{} {
 	r := make([]interface{}, 0)
 	for _, i := range l {
 		r = append(r, i)
@@ -234,8 +234,8 @@ func (l List) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
 // Map represents a 'map' of values
 type Map map[string]interface{}
 
-// nolint
-func (m Map) underlying() map[string]interface{} {
+// Underlying returns the underlying elements in the form of 'map[string]interface {}'
+func (m Map) Underlying() map[string]interface{} {
 	r := make(map[string]interface{}, 0)
 	for k, e := range m {
 		r[k] = e
diff --git a/pkg/tfconf/output.go b/pkg/tfconf/output.go
index 49596e8..3435f9b 100644
--- a/pkg/tfconf/output.go
+++ b/pkg/tfconf/output.go
@@ -31,6 +31,9 @@ type withvalue struct {
 // If 'Value' is a primitive type, the primitive value of 'Value' will be returned
 // and not the JSON formatted of it.
 func (o *Output) GetValue() string {
+	if !o.ShowValue || o.Value == nil {
+		return ""
+	}
 	marshaled, err := json.MarshalIndent(o.Value, "", "  ")
 	if err != nil {
 		panic(err)
@@ -43,6 +46,9 @@ func (o *Output) GetValue() string {
 
 // HasDefault indicates if a Terraform output has a default value set.
 func (o *Output) HasDefault() bool {
+	if !o.ShowValue || o.Value == nil {
+		return false
+	}
 	return o.Value.HasDefault()
 }
 
@@ -65,6 +71,9 @@ func (o *Output) MarshalJSON() ([]byte, error) {
 	if o.ShowValue {
 		return fn(withvalue(*o))
 	}
+	// explicitly make these empty
+	o.Value = nil
+	o.Sensitive = false
 	return fn(*o)
 }
 
@@ -101,5 +110,8 @@ func (o *Output) MarshalYAML() (interface{}, error) {
 	if o.ShowValue {
 		return withvalue(*o), nil
 	}
+	// explicitly make these empty
+	o.Value = nil
+	o.Sensitive = false
 	return o, nil
 }
diff --git a/pkg/tmpl/sanitizer.go b/pkg/tmpl/sanitizer.go
index 6652543..529df0c 100644
--- a/pkg/tmpl/sanitizer.go
+++ b/pkg/tmpl/sanitizer.go
@@ -29,8 +29,8 @@ func sanitizeItemForDocument(s string, settings *print.Settings) string {
 		s,
 		"```",
 		func(segment string) string {
-			segment = convertMultiLineText(segment, false)
 			segment = escapeIllegalCharacters(segment, settings)
+			segment = convertMultiLineText(segment, false)
 			segment = normalizeURLs(segment, settings)
 			return segment
 		},
@@ -43,7 +43,7 @@ func sanitizeItemForDocument(s string, settings *print.Settings) string {
 			return segment
 		},
 	)
-	return strings.Replace(result, "<br>", "\n", -1)
+	return result
 }
 
 // sanitizeItemForTable converts passed 'string' to suitable Markdown representation
@@ -56,8 +56,8 @@ func sanitizeItemForTable(s string, settings *print.Settings) string {
 		s,
 		"```",
 		func(segment string) string {
-			segment = convertMultiLineText(segment, true)
 			segment = escapeIllegalCharacters(segment, settings)
+			segment = convertMultiLineText(segment, true)
 			segment = normalizeURLs(segment, settings)
 			return segment
 		},
@@ -86,15 +86,18 @@ func convertMultiLineText(s string, isTable bool) string {
 	// which is a know convention of Markdown for multi-lines paragprah.
 	// This doesn't apply on a markdown list for example, because all the
 	// consecutive lines start with hyphen which is a special character.
-	s = regexp.MustCompile(`(\S*)(\r?\n)(\w+)`).ReplaceAllString(s, "$1  $2$3")
+	s = regexp.MustCompile(`(\S*)(\r?\n)(\s*)(\w+)`).ReplaceAllString(s, "$1  $2$3$4")
 	s = strings.Replace(s, "    \n", "  \n", -1)
+	s = strings.Replace(s, "<br>  \n", "\n\n", -1)
 
 	if isTable {
 		// Convert space-space-newline to <br>
 		s = strings.Replace(s, "  \n", "<br>", -1)
 
-		// Convert single newline to space.
-		s = strings.Replace(s, "\n", " ", -1)
+		// Convert single newline to <br>.
+		s = strings.Replace(s, "\n", "<br>", -1)
+	} else {
+		s = strings.Replace(s, "<br>", "\n", -1)
 	}
 
 	return s
@@ -104,7 +107,16 @@ func convertMultiLineText(s string, isTable bool) string {
 func escapeIllegalCharacters(s string, settings *print.Settings) string {
 	// Escape pipe
 	if settings.EscapePipe {
-		s = strings.Replace(s, "|", "\\|", -1)
+		s = processSegments(
+			s,
+			"`",
+			func(segment string) string {
+				return strings.Replace(segment, "|", "\\|", -1)
+			},
+			func(segment string) string {
+				return fmt.Sprintf("`%s`", segment)
+			},
+		)
 	}
 
 	if settings.EscapeCharacters {
@@ -112,18 +124,39 @@ func escapeIllegalCharacters(s string, settings *print.Settings) string {
 			s,
 			"`",
 			func(segment string) string {
-				escape := func(char string) {
-					segment = strings.Replace(segment, char+char, "‡‡", -1)
-					segment = strings.Replace(segment, " "+char, " ‡", -1)
-					segment = strings.Replace(segment, char+" ", "‡ ", -1)
-					segment = strings.Replace(segment, char, "\\"+char, -1)
-					segment = strings.Replace(segment, "‡", char, -1)
-				}
-				// Escape underscore
-				escape("_")
-				// Escape asterisk
-				escape("*")
-				return segment
+				return executePerLine(segment, func(line string) string {
+					escape := func(char string) {
+						c := strings.Replace(char, "*", "\\*", -1)
+						cases := []struct {
+							pattern string
+							index   []int
+						}{
+							{
+								pattern: `^(\s*)(` + c + `+)(\s+)(.*)`,
+								index:   []int{2},
+							},
+							{
+								pattern: `(\s+)(` + c + `+)([^\t\n\f\r ` + c + `])(.*)([^\t\n\f\r ` + c + `])(` + c + `+)(\s+)`,
+								index:   []int{6, 2},
+							},
+						}
+						for _, c := range cases {
+							r := regexp.MustCompile(c.pattern)
+							m := r.FindAllStringSubmatch(line, -1)
+							i := r.FindAllStringSubmatchIndex(line, -1)
+							for j := range m {
+								for _, k := range c.index {
+									line = line[:i[j][k*2]] + strings.Replace(m[j][k], char, "‡‡‡DONTESCAPE‡‡‡", -1) + line[i[j][(k*2)+1]:]
+								}
+							}
+						}
+						line = strings.Replace(line, char, "\\"+char, -1)
+						line = strings.Replace(line, "‡‡‡DONTESCAPE‡‡‡", char, -1)
+					}
+					escape("_") // Escape underscore
+					escape("*") // Escape asterisk
+					return line
+				})
 			},
 			func(segment string) string {
 				segment = fmt.Sprintf("`%s`", segment)
@@ -185,3 +218,11 @@ func processSegments(s string, prefix string, normalFn func(segment string) stri
 	}
 	return buffer.String()
 }
+
+func executePerLine(s string, fn func(string) string) string {
+	lines := strings.Split(s, "\n")
+	for i, l := range lines {
+		lines[i] = fn(l)
+	}
+	return strings.Join(lines, "\n")
+}
diff --git a/pkg/tmpl/template.go b/pkg/tmpl/template.go
index 43940c1..c10df3b 100644
--- a/pkg/tmpl/template.go
+++ b/pkg/tmpl/template.go
@@ -94,51 +94,51 @@ func builtinFuncs(settings *print.Settings) template.FuncMap {
 			}
 			return d
 		},
-		"ternary": func(c interface{}, t string, f string) string {
-			var condition bool
-			switch x := fmt.Sprintf("%T", c); x {
+		"ternary": func(condition interface{}, trueValue string, falseValue string) string {
+			var c bool
+			switch x := fmt.Sprintf("%T", condition); x {
 			case "string":
-				condition = c.(string) != ""
+				c = condition.(string) != ""
 			case "int":
-				condition = c.(int) != 0
+				c = condition.(int) != 0
 			case "bool":
-				condition = c.(bool)
+				c = condition.(bool)
 			}
-			if condition {
-				return t
+			if c {
+				return trueValue
 			}
-			return f
+			return falseValue
 		},
 		"tostring": func(s types.String) string {
 			return string(s)
 		},
-		"trim": func(t string, s string) string {
+		"trim": func(cut string, s string) string {
 			if s != "" {
-				return strings.Trim(s, t)
+				return strings.Trim(s, cut)
 			}
 			return s
 		},
-		"trimLeft": func(t string, s string) string {
+		"trimLeft": func(cut string, s string) string {
 			if s != "" {
-				return strings.TrimLeft(s, t)
+				return strings.TrimLeft(s, cut)
 			}
 			return s
 		},
-		"trimRight": func(t string, s string) string {
+		"trimRight": func(cut string, s string) string {
 			if s != "" {
-				return strings.TrimRight(s, t)
+				return strings.TrimRight(s, cut)
 			}
 			return s
 		},
-		"trimPrefix": func(t string, s string) string {
+		"trimPrefix": func(prefix string, s string) string {
 			if s != "" {
-				return strings.TrimPrefix(s, t)
+				return strings.TrimPrefix(s, prefix)
 			}
 			return s
 		},
-		"trimSuffix": func(t string, s string) string {
+		"trimSuffix": func(suffix string, s string) string {
 			if s != "" {
-				return strings.TrimSuffix(s, t)
+				return strings.TrimSuffix(s, suffix)
 			}
 			return s
 		},
@@ -159,7 +159,10 @@ func builtinFuncs(settings *print.Settings) template.FuncMap {
 			return sanitizeItemForDocument(s, settings)
 		},
 		"sanitizeTbl": func(s string) string {
-			return sanitizeItemForTable(s, settings)
+			settings.EscapePipe = true
+			s = sanitizeItemForTable(s, settings)
+			settings.EscapePipe = false
+			return s
 		},
 	}
 }
diff --git a/pkg/tmpl/testdata/table/empty.golden b/pkg/tmpl/testdata/table/empty.golden
new file mode 100644
index 0000000..e69de29
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 7354718..8bcd4a6 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -28,8 +28,10 @@ github.com/hashicorp/hcl/v2/hclsyntax
 github.com/hashicorp/hcl/v2/hclwrite
 github.com/hashicorp/hcl/v2/json
 # github.com/hashicorp/terraform-config-inspect v0.0.0-20200210163047-f7d8399e1194
+## explicit
 github.com/hashicorp/terraform-config-inspect/tfconfig
 # github.com/imdario/mergo v0.3.8
+## explicit
 github.com/imdario/mergo
 # github.com/inconshreveable/mousetrap v1.0.0
 github.com/inconshreveable/mousetrap
@@ -38,10 +40,12 @@ github.com/mitchellh/go-wordwrap
 # github.com/pmezard/go-difflib v1.0.0
 github.com/pmezard/go-difflib/difflib
 # github.com/spf13/cobra v0.0.6
+## explicit
 github.com/spf13/cobra
 # github.com/spf13/pflag v1.0.3
 github.com/spf13/pflag
 # github.com/stretchr/testify v1.5.1
+## explicit
 github.com/stretchr/testify/assert
 # github.com/zclconf/go-cty v1.1.0
 github.com/zclconf/go-cty/cty
@@ -55,6 +59,8 @@ github.com/zclconf/go-cty/cty/set
 golang.org/x/text/transform
 golang.org/x/text/unicode/norm
 # gopkg.in/yaml.v2 v2.2.8
+## explicit
 gopkg.in/yaml.v2
 # mvdan.cc/xurls/v2 v2.1.0
+## explicit
 mvdan.cc/xurls/v2


Test patch:
diff --git a/internal/format/document_test.go b/internal/format/document_test.go
index 808df38..2bea32c 100644
--- a/internal/format/document_test.go
+++ b/internal/format/document_test.go
@@ -56,11 +56,13 @@ func TestDocumentSortByName(t *testing.T) {
 	expected, err := testutil.GetExpected("document", "document-SortByName")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		SortBy: &module.SortBy{
 			Name: true,
 		},
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
@@ -81,12 +83,14 @@ func TestDocumentSortByRequired(t *testing.T) {
 	expected, err := testutil.GetExpected("document", "document-SortByRequired")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		SortBy: &module.SortBy{
 			Name:     true,
 			Required: true,
 		},
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
@@ -370,10 +374,12 @@ func TestDocumentOutputValues(t *testing.T) {
 	expected, err := testutil.GetExpected("document", "document-OutputValues")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		OutputValues:     true,
 		OutputValuesPath: "output_values.json",
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
diff --git a/internal/format/json_test.go b/internal/format/json_test.go
index 73cae95..045cca1 100644
--- a/internal/format/json_test.go
+++ b/internal/format/json_test.go
@@ -36,11 +36,13 @@ func TestJsonSortByName(t *testing.T) {
 	expected, err := testutil.GetExpected("json", "json-SortByName")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		SortBy: &module.SortBy{
 			Name: true,
 		},
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
@@ -61,12 +63,14 @@ func TestJsonSortByRequired(t *testing.T) {
 	expected, err := testutil.GetExpected("json", "json-SortByRequired")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		SortBy: &module.SortBy{
 			Name:     true,
 			Required: true,
 		},
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
@@ -290,10 +294,12 @@ func TestJsonOutputValues(t *testing.T) {
 	expected, err := testutil.GetExpected("json", "json-OutputValues")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		OutputValues:     true,
 		OutputValuesPath: "output_values.json",
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
diff --git a/internal/format/pretty_test.go b/internal/format/pretty_test.go
index 21b2b26..5af7130 100644
--- a/internal/format/pretty_test.go
+++ b/internal/format/pretty_test.go
@@ -36,11 +36,13 @@ func TestPrettySortByName(t *testing.T) {
 	expected, err := testutil.GetExpected("pretty", "pretty-SortByName")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		SortBy: &module.SortBy{
 			Name: true,
 		},
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
@@ -61,12 +63,14 @@ func TestPrettySortByRequired(t *testing.T) {
 	expected, err := testutil.GetExpected("pretty", "pretty-SortByRequired")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		SortBy: &module.SortBy{
 			Name:     true,
 			Required: true,
 		},
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
@@ -290,10 +294,12 @@ func TestPrettyOutputValues(t *testing.T) {
 	expected, err := testutil.GetExpected("pretty", "pretty-OutputValues")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		OutputValues:     true,
 		OutputValuesPath: "output_values.json",
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
diff --git a/internal/format/table_test.go b/internal/format/table_test.go
index 91aca4b..f548c44 100644
--- a/internal/format/table_test.go
+++ b/internal/format/table_test.go
@@ -56,11 +56,13 @@ func TestTableSortByName(t *testing.T) {
 	expected, err := testutil.GetExpected("table", "table-SortByName")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		SortBy: &module.SortBy{
 			Name: true,
 		},
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
@@ -81,12 +83,14 @@ func TestTableSortByRequired(t *testing.T) {
 	expected, err := testutil.GetExpected("table", "table-SortByRequired")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		SortBy: &module.SortBy{
 			Name:     true,
 			Required: true,
 		},
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
@@ -370,10 +374,12 @@ func TestTableOutputValues(t *testing.T) {
 	expected, err := testutil.GetExpected("table", "table-OutputValues")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		OutputValues:     true,
 		OutputValuesPath: "output_values.json",
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
diff --git a/internal/format/util_test.go b/internal/format/util_test.go
new file mode 100644
index 0000000..d7c8e8c
--- /dev/null
+++ b/internal/format/util_test.go
@@ -0,0 +1,121 @@
+package format
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSanitizeMarkdown(t *testing.T) {
+	tests := []struct {
+		name     string
+		markdown string
+		expected string
+	}{
+		{
+			name:     "preserve double spaces",
+			markdown: "Lorem ipsum dolor sit amet,  \nconsectetur adipiscing elit",
+			expected: "Lorem ipsum dolor sit amet,  \nconsectetur adipiscing elit",
+		},
+		{
+			name:     "remove trailing space",
+			markdown: "Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit",
+			expected: "Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit",
+		},
+		{
+			name:     "remove blank line with only doubl spaces",
+			markdown: "Lorem ipsum dolor sit amet,\n  \nconsectetur adipiscing elit",
+			expected: "Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit",
+		},
+		{
+			name:     "remove multiple consecutive blank lines",
+			markdown: "Lorem ipsum dolor sit amet,\n\nconsectetur adipiscing elit",
+			expected: "Lorem ipsum dolor sit amet,\n\nconsectetur adipiscing elit",
+		},
+		{
+			name:     "remove multiple consecutive blank lines",
+			markdown: "Lorem ipsum dolor sit amet,\n\n\nconsectetur adipiscing elit",
+			expected: "Lorem ipsum dolor sit amet,\n\nconsectetur adipiscing elit",
+		},
+		{
+			name:     "remove multiple consecutive blank lines",
+			markdown: "Lorem ipsum dolor sit amet,\n\n\n\nconsectetur adipiscing elit",
+			expected: "Lorem ipsum dolor sit amet,\n\nconsectetur adipiscing elit",
+		},
+		{
+			name:     "remove multiple consecutive blank lines",
+			markdown: "Lorem ipsum dolor sit amet,\n\n\n\n\nconsectetur adipiscing elit",
+			expected: "Lorem ipsum dolor sit amet,\n\nconsectetur adipiscing elit",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			actual := sanitize(tt.markdown)
+
+			assert.Equal(tt.expected, actual)
+		})
+	}
+}
+
+func TestFenceCodeBlock(t *testing.T) {
+	tests := []struct {
+		name      string
+		code      string
+		language  string
+		expected  string
+		extraline bool
+	}{
+		{
+			name:      "single line",
+			code:      "foo",
+			language:  "json",
+			expected:  "`foo`",
+			extraline: false,
+		},
+		{
+			name:      "single line",
+			code:      "\"bar\"",
+			language:  "hcl",
+			expected:  "`\"bar\"`",
+			extraline: false,
+		},
+		{
+			name:      "single line",
+			code:      "fuzz_buzz",
+			language:  "",
+			expected:  "`fuzz_buzz`",
+			extraline: false,
+		},
+		{
+			name:      "multi lines",
+			code:      "[\n  \"foo\",\n  \"bar\",\n  \"baz\"\n]",
+			language:  "json",
+			expected:  "\n\n```json\n[\n  \"foo\",\n  \"bar\",\n  \"baz\"\n]\n```\n",
+			extraline: true,
+		},
+		{
+			name:      "multi lines",
+			code:      "variable \"foo\" {\n  default = true\n}",
+			language:  "hcl",
+			expected:  "\n\n```hcl\nvariable \"foo\" {\n  default = true\n}\n```\n",
+			extraline: true,
+		},
+		{
+			name:      "multi lines",
+			code:      "Usage:\n\nExample of 'foo_bar' module in `foo_bar.tf`.\n\n- list item 1\n- list item 2",
+			language:  "",
+			expected:  "\n\n```\nUsage:\n\nExample of 'foo_bar' module in `foo_bar.tf`.\n\n- list item 1\n- list item 2\n```\n",
+			extraline: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			actual, extraline := printFencedCodeBlock(tt.code, tt.language)
+
+			assert.Equal(tt.expected, actual)
+			assert.Equal(tt.extraline, extraline)
+		})
+	}
+}
diff --git a/internal/format/xml_test.go b/internal/format/xml_test.go
index 8e34a7f..5030d72 100644
--- a/internal/format/xml_test.go
+++ b/internal/format/xml_test.go
@@ -36,11 +36,13 @@ func TestXmlSortByName(t *testing.T) {
 	expected, err := testutil.GetExpected("xml", "xml-SortByName")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		SortBy: &module.SortBy{
 			Name: true,
 		},
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
@@ -61,12 +63,14 @@ func TestXmlSortByRequired(t *testing.T) {
 	expected, err := testutil.GetExpected("xml", "xml-SortByRequired")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		SortBy: &module.SortBy{
 			Name:     true,
 			Required: true,
 		},
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
@@ -270,10 +274,12 @@ func TestXmlOutputValues(t *testing.T) {
 	expected, err := testutil.GetExpected("xml", "xml-OutputValues")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		OutputValues:     true,
 		OutputValuesPath: "output_values.json",
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
diff --git a/internal/format/yaml_test.go b/internal/format/yaml_test.go
index 229636c..83bd4b6 100644
--- a/internal/format/yaml_test.go
+++ b/internal/format/yaml_test.go
@@ -36,11 +36,13 @@ func TestYamlSortByName(t *testing.T) {
 	expected, err := testutil.GetExpected("yaml", "yaml-SortByName")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		SortBy: &module.SortBy{
 			Name: true,
 		},
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
@@ -61,12 +63,14 @@ func TestYamlSortByRequired(t *testing.T) {
 	expected, err := testutil.GetExpected("yaml", "yaml-SortByRequired")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		SortBy: &module.SortBy{
 			Name:     true,
 			Required: true,
 		},
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
@@ -270,10 +274,12 @@ func TestYamlOutputValues(t *testing.T) {
 	expected, err := testutil.GetExpected("yaml", "yaml-OutputValues")
 	assert.Nil(err)
 
-	options := module.NewOptions().With(&module.Options{
+	options, err := module.NewOptions().With(&module.Options{
 		OutputValues:     true,
 		OutputValuesPath: "output_values.json",
 	})
+	assert.Nil(err)
+
 	module, err := testutil.GetModule(options)
 	assert.Nil(err)
 
diff --git a/internal/module/input_test.go b/internal/module/input_test.go
new file mode 100644
index 0000000..02a9a21
--- /dev/null
+++ b/internal/module/input_test.go
@@ -0,0 +1,98 @@
+package module
+
+import (
+	"sort"
+	"testing"
+
+	"github.com/segmentio/terraform-docs/internal/types"
+	"github.com/segmentio/terraform-docs/pkg/tfconf"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestInputsSortedByName(t *testing.T) {
+	assert := assert.New(t)
+	inputs := sampleInputs()
+
+	sort.Sort(inputsSortedByName(inputs))
+
+	expected := []string{"a", "b", "c", "d", "e"}
+	actual := make([]string, len(inputs))
+
+	for k, i := range inputs {
+		actual[k] = i.Name
+	}
+
+	assert.Equal(expected, actual)
+}
+
+func TestInputsSortedByRequired(t *testing.T) {
+	assert := assert.New(t)
+	inputs := sampleInputs()
+
+	sort.Sort(inputsSortedByRequired(inputs))
+
+	expected := []string{"b", "d", "a", "c", "e"}
+	actual := make([]string, len(inputs))
+
+	for k, i := range inputs {
+		actual[k] = i.Name
+	}
+
+	assert.Equal(expected, actual)
+}
+
+func TestInputsSortedByPosition(t *testing.T) {
+	assert := assert.New(t)
+	inputs := sampleInputs()
+
+	sort.Sort(inputsSortedByPosition(inputs))
+
+	expected := []string{"a", "d", "e", "b", "c"}
+	actual := make([]string, len(inputs))
+
+	for k, i := range inputs {
+		actual[k] = i.Name
+	}
+
+	assert.Equal(expected, actual)
+}
+
+func sampleInputs() []*tfconf.Input {
+	return []*tfconf.Input{
+		&tfconf.Input{
+			Name:        "e",
+			Type:        types.String(""),
+			Description: types.String("description of e"),
+			Default:     types.ValueOf(true),
+			Position:    tfconf.Position{Filename: "foo/variables.tf", Line: 35},
+		},
+		&tfconf.Input{
+			Name:        "a",
+			Type:        types.String("string"),
+			Description: types.String(""),
+			Default:     types.ValueOf("a"),
+			Position:    tfconf.Position{Filename: "foo/variables.tf", Line: 10},
+		},
+		&tfconf.Input{
+			Name:        "d",
+			Type:        types.String("string"),
+			Description: types.String("description for d"),
+			Default:     types.ValueOf(nil),
+			Position:    tfconf.Position{Filename: "foo/variables.tf", Line: 23},
+		},
+		&tfconf.Input{
+			Name:        "b",
+			Type:        types.String("number"),
+			Description: types.String("description of b"),
+			Default:     types.ValueOf(nil),
+			Position:    tfconf.Position{Filename: "foo/variables.tf", Line: 42},
+		},
+		&tfconf.Input{
+			Name:        "c",
+			Type:        types.String("list"),
+			Description: types.String("description of c"),
+			Default:     types.ValueOf("c"),
+			Position:    tfconf.Position{Filename: "foo/variables.tf", Line: 51},
+		},
+	}
+}
diff --git a/internal/module/module_test.go b/internal/module/module_test.go
new file mode 100644
index 0000000..a1e48ba
--- /dev/null
+++ b/internal/module/module_test.go
@@ -0,0 +1,429 @@
+package module
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLoadModuleWithOptions(t *testing.T) {
+	assert := assert.New(t)
+
+	options, _ := NewOptions().With(&Options{
+		Path: filepath.Join("testdata", "full-example"),
+	})
+	module, err := LoadWithOptions(options)
+
+	assert.Nil(err)
+	assert.Equal(true, module.HasHeader())
+	assert.Equal(true, module.HasInputs())
+	assert.Equal(true, module.HasOutputs())
+	assert.Equal(true, module.HasProviders())
+}
+
+func TestLoadModule(t *testing.T) {
+	tests := []struct {
+		name    string
+		path    string
+		wantErr bool
+	}{
+		{
+			name:    "load module from path",
+			path:    "full-example",
+			wantErr: false,
+		},
+		{
+			name:    "load module from path",
+			path:    "non-exist",
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			_, err := loadModule(filepath.Join("testdata", tt.path))
+			if tt.wantErr {
+				assert.NotNil(err)
+			} else {
+				assert.Nil(err)
+			}
+		})
+	}
+}
+
+func TestLoadHeader(t *testing.T) {
+	tests := []struct {
+		name     string
+		path     string
+		expected string
+	}{
+		{
+			name:     "load module header from path",
+			path:     "full-example",
+			expected: "Example of 'foo_bar' module in `foo_bar.tf`.\n\n- list item 1\n- list item 2\n\nEven inline **formatting** in _here_ is possible.\nand some [link](https://domain.com/)",
+		},
+		{
+			name:     "load module header from path",
+			path:     "empty-header",
+			expected: "",
+		},
+		{
+			name:     "load module header from path",
+			path:     "non-exist",
+			expected: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			actual := loadHeader(filepath.Join("testdata", tt.path))
+			assert.Equal(tt.expected, actual)
+		})
+	}
+}
+
+func TestLoadInputs(t *testing.T) {
+	type expected struct {
+		inputs    int
+		requireds int
+		optionals int
+	}
+	tests := []struct {
+		name     string
+		path     string
+		expected expected
+	}{
+		{
+			name: "load module inputs from path",
+			path: "full-example",
+			expected: expected{
+				inputs:    6,
+				requireds: 2,
+				optionals: 4,
+			},
+		},
+		{
+			name: "load module inputs from path",
+			path: "no-required-inputs",
+			expected: expected{
+				inputs:    6,
+				requireds: 0,
+				optionals: 6,
+			},
+		},
+		{
+			name: "load module inputs from path",
+			path: "no-optional-inputs",
+			expected: expected{
+				inputs:    6,
+				requireds: 6,
+				optionals: 0,
+			},
+		},
+		{
+			name: "load module inputs from path",
+			path: "no-inputs",
+			expected: expected{
+				inputs:    0,
+				requireds: 0,
+				optionals: 0,
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			module, _ := loadModule(filepath.Join("testdata", tt.path))
+			inputs, requireds, optionals := loadInputs(module)
+
+			assert.Equal(tt.expected.inputs, len(inputs))
+			assert.Equal(tt.expected.requireds, len(requireds))
+			assert.Equal(tt.expected.optionals, len(optionals))
+		})
+	}
+}
+
+func TestLoadOutputs(t *testing.T) {
+	type expected struct {
+		outputs int
+	}
+	tests := []struct {
+		name     string
+		path     string
+		expected expected
+	}{
+		{
+			name: "load module outputs from path",
+			path: "full-example",
+			expected: expected{
+				outputs: 3,
+			},
+		},
+		{
+			name: "load module outputs from path",
+			path: "no-outputs",
+			expected: expected{
+				outputs: 0,
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			options := NewOptions()
+			module, _ := loadModule(filepath.Join("testdata", tt.path))
+			outputs, err := loadOutputs(module, options)
+
+			assert.Nil(err)
+			assert.Equal(tt.expected.outputs, len(outputs))
+
+			for _, v := range outputs {
+				assert.Equal(false, v.ShowValue)
+			}
+		})
+	}
+}
+
+func TestLoadOutputsValues(t *testing.T) {
+	type expected struct {
+		outputs int
+	}
+	tests := []struct {
+		name       string
+		path       string
+		outputPath string
+		expected   expected
+		wantErr    bool
+	}{
+		{
+			name:       "load module outputs with values from path",
+			path:       "full-example",
+			outputPath: "output-values.json",
+			expected: expected{
+				outputs: 3,
+			},
+			wantErr: false,
+		},
+		{
+			name:       "load module outputs with values from path",
+			path:       "full-example",
+			outputPath: "no-file.json",
+			expected:   expected{},
+			wantErr:    true,
+		},
+		{
+			name:       "load module outputs with values from path",
+			path:       "no-outputs",
+			outputPath: "no-file.json",
+			expected:   expected{},
+			wantErr:    true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			options, _ := NewOptions().With(&Options{
+				OutputValues:     true,
+				OutputValuesPath: filepath.Join("testdata", tt.path, tt.outputPath),
+			})
+			module, _ := loadModule(filepath.Join("testdata", tt.path))
+			outputs, err := loadOutputs(module, options)
+
+			if tt.wantErr {
+				assert.NotNil(err)
+			} else {
+				assert.Nil(err)
+				assert.Equal(tt.expected.outputs, len(outputs))
+
+				for _, v := range outputs {
+					assert.Equal(true, v.ShowValue)
+				}
+			}
+		})
+	}
+}
+
+func TestLoadProviders(t *testing.T) {
+	type expected struct {
+		providers int
+	}
+	tests := []struct {
+		name     string
+		path     string
+		expected expected
+	}{
+		{
+			name: "load module providers from path",
+			path: "full-example",
+			expected: expected{
+				providers: 3,
+			},
+		},
+		{
+			name: "load module providers from path",
+			path: "no-providers",
+			expected: expected{
+				providers: 0,
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			module, _ := loadModule(filepath.Join("testdata", tt.path))
+			providers := loadProviders(module)
+
+			assert.Equal(tt.expected.providers, len(providers))
+		})
+	}
+}
+
+func TestLoadComments(t *testing.T) {
+	tests := []struct {
+		name       string
+		path       string
+		fileName   string
+		lineNumber int
+		expected   string
+	}{
+		{
+			name:       "load resource comment from file",
+			path:       "full-example",
+			fileName:   "variables.tf",
+			lineNumber: 2,
+			expected:   "D description",
+		},
+		{
+			name:       "load resource comment from file",
+			path:       "full-example",
+			fileName:   "variables.tf",
+			lineNumber: 16,
+			expected:   "A Description in multiple lines",
+		},
+		{
+			name:       "load resource comment from file with wrong line number",
+			path:       "full-example",
+			fileName:   "variables.tf",
+			lineNumber: 100,
+			expected:   "",
+		},
+		{
+			name:       "load resource comment from non existing file",
+			path:       "full-example",
+			fileName:   "non-exist.tf",
+			lineNumber: 5,
+			expected:   "",
+		},
+		{
+			name:       "load resource comment from non existing file",
+			path:       "non-exist",
+			fileName:   "variables.tf",
+			lineNumber: 5,
+			expected:   "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			actual := loadComments(filepath.Join("testdata", tt.path, tt.fileName), tt.lineNumber)
+			assert.Equal(tt.expected, actual)
+		})
+	}
+}
+
+func TestSortItems(t *testing.T) {
+	type expected struct {
+		inputs    []string
+		required  []string
+		optional  []string
+		outputs   []string
+		providers []string
+	}
+	tests := []struct {
+		name     string
+		path     string
+		sort     *SortBy
+		expected expected
+	}{
+		{
+			name: "sort module items",
+			path: "full-example",
+			sort: &SortBy{Name: false, Required: false},
+			expected: expected{
+				inputs:    []string{"D", "B", "E", "A", "C", "F"},
+				required:  []string{"A", "F"},
+				optional:  []string{"D", "B", "E", "C"},
+				outputs:   []string{"C", "A", "B"},
+				providers: []string{"tls", "aws", "null"},
+			},
+		},
+		{
+			name: "sort module items",
+			path: "full-example",
+			sort: &SortBy{Name: true, Required: false},
+			expected: expected{
+				inputs:    []string{"A", "B", "C", "D", "E", "F"},
+				required:  []string{"A", "F"},
+				optional:  []string{"B", "C", "D", "E"},
+				outputs:   []string{"A", "B", "C"},
+				providers: []string{"aws", "null", "tls"},
+			},
+		},
+		{
+			name: "sort module items",
+			path: "full-example",
+			sort: &SortBy{Name: false, Required: true},
+			expected: expected{
+				inputs:    []string{"D", "B", "E", "A", "C", "F"},
+				required:  []string{"A", "F"},
+				optional:  []string{"D", "B", "E", "C"},
+				outputs:   []string{"C", "A", "B"},
+				providers: []string{"tls", "aws", "null"},
+			},
+		},
+		{
+			name: "sort module items",
+			path: "full-example",
+			sort: &SortBy{Name: true, Required: true},
+			expected: expected{
+				inputs:    []string{"A", "F", "B", "C", "D", "E"},
+				required:  []string{"A", "F"},
+				optional:  []string{"B", "C", "D", "E"},
+				outputs:   []string{"A", "B", "C"},
+				providers: []string{"aws", "null", "tls"},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			path := filepath.Join("testdata", tt.path)
+			options, _ := NewOptions().With(&Options{
+				Path:   path,
+				SortBy: tt.sort,
+			})
+			tfmodule, _ := loadModule(path)
+			module, err := loadModuleItems(tfmodule, options)
+
+			assert.Nil(err)
+			sortItems(module, tt.sort)
+
+			for i, v := range module.Inputs {
+				assert.Equal(tt.expected.inputs[i], v.Name)
+			}
+			for i, v := range module.RequiredInputs {
+				assert.Equal(tt.expected.required[i], v.Name)
+			}
+			for i, v := range module.OptionalInputs {
+				assert.Equal(tt.expected.optional[i], v.Name)
+			}
+			for i, v := range module.Outputs {
+				assert.Equal(tt.expected.outputs[i], v.Name)
+			}
+			for i, v := range module.Providers {
+				assert.Equal(tt.expected.providers[i], v.Name)
+			}
+		})
+	}
+}
diff --git a/internal/module/options_test.go b/internal/module/options_test.go
new file mode 100644
index 0000000..e0593ac
--- /dev/null
+++ b/internal/module/options_test.go
@@ -0,0 +1,54 @@
+package module
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestOptionsOverrideWith(t *testing.T) {
+	assert := assert.New(t)
+
+	options := NewOptions()
+
+	assert.Equal(options.Path, "")
+	assert.Equal(options.OutputValues, false)
+	assert.Equal(options.OutputValuesPath, "")
+
+	_, err1 := options.With(&Options{
+		Path: "/path/to/foo",
+	})
+	assert.Nil(err1)
+
+	assert.Equal(options.Path, "/path/to/foo")
+	assert.Equal(options.OutputValues, false)
+	assert.Equal(options.OutputValuesPath, "")
+
+	_, err2 := options.With(&Options{
+		OutputValues:     true,
+		OutputValuesPath: "/path/to/output/values",
+	})
+	assert.Nil(err2)
+
+	assert.Equal(options.Path, "/path/to/foo")
+	assert.Equal(options.OutputValues, true)
+	assert.Equal(options.OutputValuesPath, "/path/to/output/values")
+
+	_, err3 := options.With(&Options{
+		Path:         "",
+		OutputValues: false,
+	})
+	assert.Nil(err3)
+
+	assert.NotEqual(options.Path, "")
+	assert.NotEqual(options.OutputValues, false)
+}
+
+func TestOptionsOverrideWithNil(t *testing.T) {
+	assert := assert.New(t)
+	options := NewOptions()
+
+	_, err := options.With(nil)
+
+	assert.NotNil(err)
+}
diff --git a/internal/module/output_test.go b/internal/module/output_test.go
new file mode 100644
index 0000000..6fb6ce3
--- /dev/null
+++ b/internal/module/output_test.go
@@ -0,0 +1,77 @@
+package module
+
+import (
+	"sort"
+	"testing"
+
+	"github.com/segmentio/terraform-docs/internal/types"
+	"github.com/segmentio/terraform-docs/pkg/tfconf"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestOutputsSortedByName(t *testing.T) {
+	assert := assert.New(t)
+	outputs := sampleOutputs()
+
+	sort.Sort(outputsSortedByName(outputs))
+
+	expected := []string{"a", "b", "c", "d", "e"}
+	actual := make([]string, len(outputs))
+
+	for k, o := range outputs {
+		actual[k] = o.Name
+	}
+
+	assert.Equal(expected, actual)
+}
+
+func TestOutputsSortedByPosition(t *testing.T) {
+	assert := assert.New(t)
+	outputs := sampleOutputs()
+
+	sort.Sort(outputsSortedByPosition(outputs))
+
+	expected := []string{"d", "a", "e", "b", "c"}
+	actual := make([]string, len(outputs))
+
+	for k, o := range outputs {
+		actual[k] = o.Name
+	}
+
+	assert.Equal(expected, actual)
+}
+
+func sampleOutputs() []*tfconf.Output {
+	return []*tfconf.Output{
+		&tfconf.Output{
+			Name:        "a",
+			Description: types.String("description of a"),
+			Value:       nil,
+			Position:    tfconf.Position{Filename: "foo/outputs.tf", Line: 25},
+		},
+		&tfconf.Output{
+			Name:        "d",
+			Description: types.String("description of d"),
+			Value:       nil,
+			Position:    tfconf.Position{Filename: "foo/outputs.tf", Line: 10},
+		},
+		&tfconf.Output{
+			Name:        "e",
+			Description: types.String("description of e"),
+			Value:       nil,
+			Position:    tfconf.Position{Filename: "foo/outputs.tf", Line: 33},
+		},
+		&tfconf.Output{
+			Name:        "b",
+			Description: types.String("description of b"),
+			Value:       nil,
+			Position:    tfconf.Position{Filename: "foo/outputs.tf", Line: 39},
+		},
+		&tfconf.Output{
+			Name:        "c",
+			Description: types.String("description of c"),
+			Value:       nil,
+			Position:    tfconf.Position{Filename: "foo/outputs.tf", Line: 42},
+		},
+	}
+}
diff --git a/internal/module/provider_test.go b/internal/module/provider_test.go
new file mode 100644
index 0000000..af362e9
--- /dev/null
+++ b/internal/module/provider_test.go
@@ -0,0 +1,89 @@
+package module
+
+import (
+	"sort"
+	"testing"
+
+	"github.com/segmentio/terraform-docs/internal/types"
+	"github.com/segmentio/terraform-docs/pkg/tfconf"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestProvidersSortedByName(t *testing.T) {
+	assert := assert.New(t)
+	providers := sampleProviders()
+
+	sort.Sort(providersSortedByName(providers))
+
+	expected := []string{"a", "b", "c", "d", "d.a", "e", "e.a"}
+	actual := make([]string, len(providers))
+
+	for k, p := range providers {
+		actual[k] = p.FullName()
+	}
+
+	assert.Equal(expected, actual)
+}
+
+func TestProvidersSortedByPosition(t *testing.T) {
+	assert := assert.New(t)
+	providers := sampleProviders()
+
+	sort.Sort(providersSortedByPosition(providers))
+
+	expected := []string{"e.a", "b", "d", "d.a", "a", "e", "c"}
+	actual := make([]string, len(providers))
+
+	for k, p := range providers {
+		actual[k] = p.FullName()
+	}
+
+	assert.Equal(expected, actual)
+}
+
+func sampleProviders() []*tfconf.Provider {
+	return []*tfconf.Provider{
+		&tfconf.Provider{
+			Name:     "d",
+			Alias:    types.String(""),
+			Version:  types.String("1.3.2"),
+			Position: tfconf.Position{Filename: "foo/main.tf", Line: 21},
+		},
+		&tfconf.Provider{
+			Name:     "d",
+			Alias:    types.String("a"),
+			Version:  types.String("> 1.x"),
+			Position: tfconf.Position{Filename: "foo/main.tf", Line: 25},
+		},
+		&tfconf.Provider{
+			Name:     "b",
+			Alias:    types.String(""),
+			Version:  types.String("= 2.1.0"),
+			Position: tfconf.Position{Filename: "foo/main.tf", Line: 13},
+		},
+		&tfconf.Provider{
+			Name:     "a",
+			Alias:    types.String(""),
+			Version:  types.String(""),
+			Position: tfconf.Position{Filename: "foo/main.tf", Line: 39},
+		},
+		&tfconf.Provider{
+			Name:     "c",
+			Alias:    types.String(""),
+			Version:  types.String("~> 0.5.0"),
+			Position: tfconf.Position{Filename: "foo/main.tf", Line: 53},
+		},
+		&tfconf.Provider{
+			Name:     "e",
+			Alias:    types.String(""),
+			Version:  types.String(""),
+			Position: tfconf.Position{Filename: "foo/main.tf", Line: 47},
+		},
+		&tfconf.Provider{
+			Name:     "e",
+			Alias:    types.String("a"),
+			Version:  types.String("> 1.0"),
+			Position: tfconf.Position{Filename: "foo/main.tf", Line: 5},
+		},
+	}
+}
diff --git a/internal/module/testdata/empty-header/main.tf b/internal/module/testdata/empty-header/main.tf
new file mode 100644
index 0000000..c4a4548
--- /dev/null
+++ b/internal/module/testdata/empty-header/main.tf
@@ -0,0 +1,7 @@
+resource "tls_private_key" "baz" {}
+
+data "aws_caller_identity" "current" {
+  provider = "aws"
+}
+
+resource "null_resource" "foo" {}
diff --git a/internal/module/testdata/full-example/main.tf b/internal/module/testdata/full-example/main.tf
new file mode 100644
index 0000000..2c34516
--- /dev/null
+++ b/internal/module/testdata/full-example/main.tf
@@ -0,0 +1,17 @@
+/**
+ * Example of 'foo_bar' module in `foo_bar.tf`.
+ *
+ * - list item 1
+ * - list item 2
+ *
+ * Even inline **formatting** in _here_ is possible.
+ * and some [link](https://domain.com/)
+ */
+
+resource "tls_private_key" "baz" {}
+
+data "aws_caller_identity" "current" {
+  provider = "aws"
+}
+
+resource "null_resource" "foo" {}
diff --git a/internal/module/testdata/full-example/output-values.json b/internal/module/testdata/full-example/output-values.json
new file mode 100644
index 0000000..844e39f
--- /dev/null
+++ b/internal/module/testdata/full-example/output-values.json
@@ -0,0 +1,17 @@
+{
+    "C": {
+        "sensitive": true,
+        "type": "string",
+        "value": "sensitive-c"
+    },
+    "A": {
+        "sensitive": false,
+        "type": "string",
+        "value": "a value"
+    },
+    "B": {
+        "sensitive": false,
+        "type": "string",
+        "value": "b value"
+    }
+}
diff --git a/internal/module/testdata/full-example/outputs.tf b/internal/module/testdata/full-example/outputs.tf
new file mode 100644
index 0000000..1024f48
--- /dev/null
+++ b/internal/module/testdata/full-example/outputs.tf
@@ -0,0 +1,14 @@
+output C {
+  description = "It's unquoted output."
+  value       = "c"
+}
+
+output "A" {
+  description = "A description"
+  value       = "a"
+}
+
+// B description
+output "B" {
+  value = "b"
+}
diff --git a/internal/module/testdata/full-example/variables.tf b/internal/module/testdata/full-example/variables.tf
new file mode 100644
index 0000000..5cf4230
--- /dev/null
+++ b/internal/module/testdata/full-example/variables.tf
@@ -0,0 +1,25 @@
+// D description
+variable "D" {
+  default = "d"
+}
+
+variable "B" {
+  default = "b"
+}
+
+variable "E" {
+  default = ""
+}
+
+# A Description
+# in multiple lines
+variable A {}
+
+variable "C" {
+  description = "C description"
+  default = "c"
+}
+
+variable "F" {
+  description = "F description"
+}
diff --git a/internal/module/testdata/no-inputs/variables.tf b/internal/module/testdata/no-inputs/variables.tf
new file mode 100644
index 0000000..e69de29
diff --git a/internal/module/testdata/no-optional-inputs/variables.tf b/internal/module/testdata/no-optional-inputs/variables.tf
new file mode 100644
index 0000000..9b04244
--- /dev/null
+++ b/internal/module/testdata/no-optional-inputs/variables.tf
@@ -0,0 +1,18 @@
+// D description
+variable "D" {}
+
+variable "B" {}
+
+variable "E" {}
+
+# A Description
+# in multiple lines
+variable A {}
+
+variable "C" {
+  description = "C description"
+}
+
+variable "F" {
+  description = "F description"
+}
diff --git a/internal/module/testdata/no-outputs/outputs.tf b/internal/module/testdata/no-outputs/outputs.tf
new file mode 100644
index 0000000..e69de29
diff --git a/internal/module/testdata/no-providers/main.tf b/internal/module/testdata/no-providers/main.tf
new file mode 100644
index 0000000..e69de29
diff --git a/internal/module/testdata/no-required-inputs/variables.tf b/internal/module/testdata/no-required-inputs/variables.tf
new file mode 100644
index 0000000..ecd7aa4
--- /dev/null
+++ b/internal/module/testdata/no-required-inputs/variables.tf
@@ -0,0 +1,28 @@
+// D description
+variable "D" {
+  default = "d"
+}
+
+variable "B" {
+  default = "b"
+}
+
+variable "E" {
+  default = ""
+}
+
+# A Description
+# in multiple lines
+variable A {
+  default = "a"
+}
+
+variable "C" {
+  description = "C description"
+  default = "c"
+}
+
+variable "F" {
+  description = "F description"
+  default = "f"
+}
diff --git a/internal/reader/lines_test.go b/internal/reader/lines_test.go
new file mode 100644
index 0000000..6528dcd
--- /dev/null
+++ b/internal/reader/lines_test.go
@@ -0,0 +1,256 @@
+package reader
+
+import (
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const textEmpty = ``
+
+const textOneLine = `Lorem ipsum dolor sit amet`
+
+const textWithLeadingComment = `
+/**
+ * Morbi vitae nulla in dui lobortis
+ * consectetur. Integer nec tempus
+ * felis. Ut quis suscipit risus.
+ * Donec lobortis consequat nunc, in
+ * efficitur mi maximus ac. Sed id
+ * felis posuere, aliquam purus eget,
+ * faucibus augue.
+ */
+
+Lorem ipsum dolor sit amet,
+consectetur adipiscing elit,
+
+# sed do eiusmod tempor incididunt
+ut labore et dolore magna aliqua.
+Ut enim ad minim veniam, quis
+
+# nostrud exercitation ullamco
+laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure
+
+# dolor in reprehenderit in voluptate
+velit esse cillum dolore eu fugiat
+nulla pariatur.
+
+# Excepteur sint occaecat cupidatat
+# non proident, sunt in culpa qui
+officia deserunt mollit anim id est laborum
+`
+
+const textWithoutLeadingComment = `
+Lorem ipsum dolor sit amet,
+consectetur adipiscing elit,
+
+# sed do eiusmod tempor incididunt
+ut labore et dolore magna aliqua.
+Ut enim ad minim veniam, quis
+
+# nostrud exercitation ullamco
+laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure
+
+# dolor in reprehenderit in voluptate
+velit esse cillum dolore eu fugiat
+nulla pariatur.
+
+# Excepteur sint occaecat cupidatat
+# non proident, sunt in culpa qui
+officia deserunt mollit anim id est laborum
+`
+
+func TestReadLinesFromFile(t *testing.T) {
+	tests := []struct {
+		name       string
+		fileName   string
+		lineNumber int
+		expected   string
+		wantError  bool
+	}{
+		{
+			name:       "extract lines from file",
+			fileName:   "testdata/sample.txt",
+			lineNumber: -1,
+			expected:   "Morbi vitae nulla in dui lobortis consectetur. Integer nec tempus felis. Ut quis suscipit risus. Donec lobortis consequat nunc, in efficitur mi maximus ac. Sed id felis posuere, aliquam purus eget, faucibus augue.",
+			wantError:  false,
+		},
+		{
+			name:       "extract lines from file",
+			fileName:   "testdata/sample.txt",
+			lineNumber: 15,
+			expected:   "sed do eiusmod tempor incididunt",
+			wantError:  false,
+		},
+		{
+			name:       "extract lines from file",
+			fileName:   "testdata/noop.txt",
+			lineNumber: -1,
+			expected:   "",
+			wantError:  true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			lines := Lines{
+				FileName: filepath.Join(tt.fileName),
+				LineNum:  tt.lineNumber,
+				Condition: func(line string) bool {
+					line = strings.TrimSpace(line)
+					return strings.HasPrefix(line, "#") || strings.HasPrefix(line, "/*") || strings.HasPrefix(line, "*") || strings.HasPrefix(line, "*/")
+				},
+				Parser: func(line string) (string, bool) {
+					line = strings.TrimSpace(line)
+					if strings.HasPrefix(line, "/*") || strings.HasPrefix(line, "*/") {
+						return "", false
+					}
+					if line == "*" {
+						return "", true
+					}
+					line = strings.TrimPrefix(line, "* ")
+					line = strings.TrimPrefix(line, "#")
+					line = strings.TrimSpace(line)
+					return line, true
+				},
+			}
+			comment, err := lines.Extract()
+			if tt.wantError {
+				assert.NotNil(err)
+			} else {
+				assert.Nil(err)
+				assert.Equal(tt.expected, strings.Join(comment, " "))
+			}
+		})
+	}
+}
+
+func TestReadLinesFromText(t *testing.T) {
+	tests := []struct {
+		name        string
+		textContent string
+		lineNumber  int
+		expected    string
+		wantError   bool
+		errorText   string
+	}{
+		{
+			name:        "extract lines from text",
+			textContent: textWithLeadingComment,
+			lineNumber:  -1,
+			expected:    "Morbi vitae nulla in dui lobortis consectetur. Integer nec tempus felis. Ut quis suscipit risus. Donec lobortis consequat nunc, in efficitur mi maximus ac. Sed id felis posuere, aliquam purus eget, faucibus augue.",
+			wantError:   false,
+			errorText:   "",
+		},
+		{
+			name:        "extract lines from text",
+			textContent: textEmpty,
+			lineNumber:  -1,
+			expected:    "",
+			wantError:   true,
+			errorText:   "no lines in file",
+		},
+		{
+			name:        "extract lines from text",
+			textContent: textOneLine,
+			lineNumber:  10,
+			expected:    "",
+			wantError:   true,
+			errorText:   "only 1 line",
+		},
+		{
+			name:        "extract lines from text",
+			textContent: textWithoutLeadingComment,
+			lineNumber:  -1,
+			expected:    "",
+			wantError:   false,
+			errorText:   "",
+		},
+		{
+			name:        "extract lines from text",
+			textContent: textWithLeadingComment,
+			lineNumber:  15,
+			expected:    "sed do eiusmod tempor incididunt",
+			wantError:   false,
+			errorText:   "",
+		},
+		{
+			name:        "extract lines from text",
+			textContent: textWithLeadingComment,
+			lineNumber:  19,
+			expected:    "nostrud exercitation ullamco",
+			wantError:   false,
+			errorText:   "",
+		},
+		{
+			name:        "extract lines from text",
+			textContent: textWithLeadingComment,
+			lineNumber:  23,
+			expected:    "dolor in reprehenderit in voluptate",
+			wantError:   false,
+			errorText:   "",
+		},
+		{
+			name:        "extract lines from text",
+			textContent: textWithLeadingComment,
+			lineNumber:  28,
+			expected:    "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui",
+			wantError:   false,
+			errorText:   "",
+		},
+		{
+			name:        "extract lines from text",
+			textContent: textWithLeadingComment,
+			lineNumber:  54,
+			expected:    "",
+			wantError:   true,
+			errorText:   "only 28 lines",
+		},
+		{
+			name:        "extract lines from text",
+			textContent: textEmpty,
+			lineNumber:  10,
+			expected:    "",
+			wantError:   true,
+			errorText:   "no lines in file",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			lines := Lines{
+				LineNum: tt.lineNumber,
+				Condition: func(line string) bool {
+					line = strings.TrimSpace(line)
+					return strings.HasPrefix(line, "#") || strings.HasPrefix(line, "/*") || strings.HasPrefix(line, "*") || strings.HasPrefix(line, "*/")
+				},
+				Parser: func(line string) (string, bool) {
+					line = strings.TrimSpace(line)
+					if strings.HasPrefix(line, "/*") || strings.HasPrefix(line, "*/") {
+						return "", false
+					}
+					if line == "*" {
+						return "", true
+					}
+					line = strings.TrimPrefix(line, "* ")
+					line = strings.TrimPrefix(line, "#")
+					line = strings.TrimSpace(line)
+					return line, true
+				},
+			}
+			r := strings.NewReader(strings.TrimSpace(tt.textContent))
+			comment, err := lines.extract(r)
+			if tt.wantError {
+				assert.NotNil(err)
+				assert.Equal(tt.errorText, err.Error())
+			} else {
+				assert.Nil(err)
+				assert.Equal(tt.expected, strings.Join(comment, " "))
+			}
+		})
+	}
+}
diff --git a/internal/reader/testdata/sample.txt b/internal/reader/testdata/sample.txt
new file mode 100644
index 0000000..85f3aef
--- /dev/null
+++ b/internal/reader/testdata/sample.txt
@@ -0,0 +1,28 @@
+/**
+ * Morbi vitae nulla in dui lobortis
+ * consectetur. Integer nec tempus
+ * felis. Ut quis suscipit risus.
+ * Donec lobortis consequat nunc, in
+ * efficitur mi maximus ac. Sed id
+ * felis posuere, aliquam purus eget,
+ * faucibus augue.
+ */
+
+Lorem ipsum dolor sit amet,
+consectetur adipiscing elit,
+
+# sed do eiusmod tempor incididunt
+ut labore et dolore magna aliqua.
+Ut enim ad minim veniam, quis
+
+# nostrud exercitation ullamco
+laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure
+
+# dolor in reprehenderit in voluptate
+velit esse cillum dolore eu fugiat
+nulla pariatur.
+
+# Excepteur sint occaecat cupidatat
+# non proident, sunt in culpa qui
+officia deserunt mollit anim id est laborum
diff --git a/internal/types/bool_test.go b/internal/types/bool_test.go
index f43ce27..5a67d52 100644
--- a/internal/types/bool_test.go
+++ b/internal/types/bool_test.go
@@ -2,6 +2,8 @@ package types
 
 import (
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestBool(t *testing.T) {
@@ -29,3 +31,25 @@ func TestBool(t *testing.T) {
 		},
 	})
 }
+
+func TestBoolUnderlying(t *testing.T) {
+	tests := []struct {
+		name  string
+		value bool
+	}{
+		{
+			name:  "bool underlying",
+			value: true,
+		},
+		{
+			name:  "bool underlying",
+			value: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			assert.Equal(tt.value, Bool(tt.value).underlying())
+		})
+	}
+}
diff --git a/internal/types/empty_test.go b/internal/types/empty_test.go
index c57e2bc..7f0b071 100644
--- a/internal/types/empty_test.go
+++ b/internal/types/empty_test.go
@@ -2,6 +2,8 @@ package types
 
 import (
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestEmpty(t *testing.T) {
@@ -29,3 +31,49 @@ func TestEmpty(t *testing.T) {
 		},
 	})
 }
+
+func TestEmptyUnderlying(t *testing.T) {
+	tests := []struct {
+		name  string
+		value string
+	}{
+		{
+			name:  "empty underlying",
+			value: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			assert.Equal(tt.value, Empty(tt.value).underlying())
+		})
+	}
+}
+
+func TestEmptyMarshalJSON(t *testing.T) {
+	tests := []struct {
+		name     string
+		value    string
+		expected string
+	}{
+		{
+			name:     "string marshal JSON",
+			value:    "foo",
+			expected: "\"\"",
+		},
+		{
+			name:     "string marshal JSON",
+			value:    "",
+			expected: "\"\"",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			actual, err := Empty(tt.value).MarshalJSON()
+
+			assert.Nil(err)
+			assert.Equal(tt.expected, string(actual))
+		})
+	}
+}
diff --git a/internal/types/list_test.go b/internal/types/list_test.go
index 8ce321d..adc7010 100644
--- a/internal/types/list_test.go
+++ b/internal/types/list_test.go
@@ -1,7 +1,11 @@
 package types
 
 import (
+	"bytes"
+	"encoding/xml"
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestList(t *testing.T) {
@@ -34,3 +38,88 @@ func TestList(t *testing.T) {
 		},
 	})
 }
+
+func TestListUnderlying(t *testing.T) {
+	tests := []struct {
+		name  string
+		value []interface{}
+	}{
+		{
+			name:  "list underlying",
+			value: List{"foo", "bar", "baz"},
+		},
+		{
+			name:  "list underlying",
+			value: List{"1", "2", "3"},
+		},
+		{
+			name:  "list underlying",
+			value: List{true, false, true},
+		},
+		{
+			name:  "list underlying",
+			value: List{10, float64(1000), int8(42)},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			assert.Equal(tt.value, List(tt.value).Underlying())
+		})
+	}
+}
+
+func TestListMarshalXML(t *testing.T) {
+	tests := []struct {
+		name     string
+		value    []interface{}
+		expected string
+	}{
+		{
+			name:     "list marshal XML",
+			value:    List{"foo", "bar", "baz"},
+			expected: "<test><item>foo</item><item>bar</item><item>baz</item></test>",
+		},
+		{
+			name:     "list marshal XML",
+			value:    List{"1", "2", "3"},
+			expected: "<test><item>1</item><item>2</item><item>3</item></test>",
+		},
+		{
+			name:     "list marshal XML",
+			value:    List{true, false, true},
+			expected: "<test><item>true</item><item>false</item><item>true</item></test>",
+		},
+		{
+			name:     "list marshal XML",
+			value:    List{10, float64(1000), int8(42)},
+			expected: "<test><item>10</item><item>1000</item><item>42</item></test>",
+		},
+		{
+			name:     "list marshal XML",
+			value:    List{},
+			expected: "<test></test>",
+		},
+		{
+			name:     "list marshal XML",
+			value:    List{nil, "something"},
+			expected: "<test><item></item><item>something</item></test>",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			var b bytes.Buffer
+			encoder := xml.NewEncoder(&b)
+			start := xml.StartElement{Name: xml.Name{Local: "test"}}
+
+			err := List(tt.value).MarshalXML(encoder, start)
+			assert.Nil(err)
+
+			err = encoder.Flush()
+			assert.Nil(err)
+
+			assert.Equal(tt.expected, b.String())
+		})
+	}
+}
diff --git a/internal/types/map_test.go b/internal/types/map_test.go
index 1768267..c307d13 100644
--- a/internal/types/map_test.go
+++ b/internal/types/map_test.go
@@ -1,7 +1,11 @@
 package types
 
 import (
+	"bytes"
+	"encoding/xml"
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestMap(t *testing.T) {
@@ -48,3 +52,99 @@ func TestMap(t *testing.T) {
 		},
 	})
 }
+
+func TestMapUnderlying(t *testing.T) {
+	tests := []struct {
+		name  string
+		value map[string]interface{}
+	}{
+		{
+			name: "map underlying",
+			value: Map{
+				"a": 1,
+				"b": 2,
+				"c": 3,
+			},
+		},
+		{
+			name: "map underlying",
+			value: Map{
+				"name": "hello",
+				"foo": map[string]string{
+					"foo": "foo",
+					"bar": "foo",
+				},
+				"bar": map[string]string{
+					"foo": "bar",
+					"bar": "bar",
+				},
+				"buzz": []string{"fizz", "buzz"},
+			},
+		},
+		{
+			name:  "map underlying",
+			value: Map{},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			assert.Equal(tt.value, Map(tt.value).Underlying())
+		})
+	}
+}
+
+func TestMapMarshalXML(t *testing.T) {
+	tests := []struct {
+		name     string
+		value    map[string]interface{}
+		expected string
+	}{
+		{
+			name: "map marshal XML",
+			value: Map{
+				"a": 1,
+				"b": 2,
+				"c": 3,
+			},
+			expected: "<test><a>1</a><b>2</b><c>3</c></test>",
+		},
+		{
+			name: "map marshal XML",
+			value: Map{
+				"name": "hello",
+				"foo": Map{
+					"foo": "foo",
+					"bar": "foo",
+				}.Underlying(),
+				"bar": Map{
+					"foo": "bar",
+					"bar": "bar",
+				}.Underlying(),
+				"buzz": List{"fizz", "buzz"}.Underlying(),
+			},
+			expected: "<test><bar><bar>bar</bar><foo>bar</foo></bar><buzz><item>fizz</item><item>buzz</item></buzz><foo><bar>foo</bar><foo>foo</foo></foo><name>hello</name></test>",
+		},
+		{
+			name:     "map marshal XML",
+			value:    Map{},
+			expected: "<test></test>",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			var b bytes.Buffer
+			encoder := xml.NewEncoder(&b)
+			start := xml.StartElement{Name: xml.Name{Local: "test"}}
+
+			err := Map(tt.value).MarshalXML(encoder, start)
+			assert.Nil(err)
+
+			err = encoder.Flush()
+			assert.Nil(err)
+
+			assert.Equal(tt.expected, b.String())
+		})
+	}
+}
diff --git a/internal/types/nil_test.go b/internal/types/nil_test.go
index 5cf6f3f..30581dd 100644
--- a/internal/types/nil_test.go
+++ b/internal/types/nil_test.go
@@ -1,7 +1,11 @@
 package types
 
 import (
+	"bytes"
+	"encoding/xml"
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestNil(t *testing.T) {
@@ -69,3 +73,73 @@ func TestNil(t *testing.T) {
 		},
 	})
 }
+
+func TestNilMarshalJSON(t *testing.T) {
+	tests := []struct {
+		name     string
+		expected string
+	}{
+		{
+			name:     "nil marshal JSON",
+			expected: "null",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			actual, err := new(Nil).MarshalJSON()
+
+			assert.Nil(err)
+			assert.Equal(tt.expected, string(actual))
+		})
+	}
+}
+
+func TestNilMarshalXML(t *testing.T) {
+	tests := []struct {
+		name     string
+		expected string
+	}{
+		{
+			name:     "nil marshal XML",
+			expected: "<test xsi:nil=\"true\"></test>",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			var b bytes.Buffer
+			encoder := xml.NewEncoder(&b)
+			start := xml.StartElement{Name: xml.Name{Local: "test"}}
+
+			err := new(Nil).MarshalXML(encoder, start)
+			assert.Nil(err)
+
+			err = encoder.Flush()
+			assert.Nil(err)
+
+			assert.Equal(tt.expected, b.String())
+		})
+	}
+}
+
+func TestNilMarshalYAML(t *testing.T) {
+	tests := []struct {
+		name     string
+		expected interface{}
+	}{
+		{
+			name:     "nil marshal YAML",
+			expected: nil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			actual, err := new(Nil).MarshalYAML()
+
+			assert.Nil(err)
+			assert.Equal(tt.expected, actual)
+		})
+	}
+}
diff --git a/internal/types/number_test.go b/internal/types/number_test.go
index b95e8c4..9f7b527 100644
--- a/internal/types/number_test.go
+++ b/internal/types/number_test.go
@@ -2,6 +2,8 @@ package types
 
 import (
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestNumber(t *testing.T) {
@@ -29,3 +31,45 @@ func TestNumber(t *testing.T) {
 		},
 	})
 }
+
+func TestNumberUnderlying(t *testing.T) {
+	tests := []struct {
+		name  string
+		value float64
+	}{
+		{
+			name:  "number underlying",
+			value: float64(int(0)),
+		},
+		{
+			name:  "number underlying",
+			value: float64(int8(42)),
+		},
+		{
+			name:  "number underlying",
+			value: float64(int16(-1200)),
+		},
+		{
+			name:  "number underlying",
+			value: float64(int32(1140085647)),
+		},
+		{
+			name:  "number underlying",
+			value: float64(int64(8922336854775807)),
+		},
+		{
+			name:  "number underlying",
+			value: float64(float32(13.75)),
+		},
+		{
+			name:  "number underlying",
+			value: float64(2317483.64),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			assert.Equal(tt.value, Number(tt.value).underlying())
+		})
+	}
+}
diff --git a/internal/types/string_test.go b/internal/types/string_test.go
index b5a89c4..1fa377a 100644
--- a/internal/types/string_test.go
+++ b/internal/types/string_test.go
@@ -1,7 +1,11 @@
 package types
 
 import (
+	"bytes"
+	"encoding/xml"
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestString(t *testing.T) {
@@ -29,3 +33,144 @@ func TestString(t *testing.T) {
 		},
 	})
 }
+
+func TestStringUnderlying(t *testing.T) {
+	tests := []struct {
+		name  string
+		value string
+	}{
+		{
+			name:  "string underlying",
+			value: "foo",
+		},
+		{
+			name:  "string underlying",
+			value: "42",
+		},
+		{
+			name:  "string underlying",
+			value: "false",
+		},
+		{
+			name:  "string underlying",
+			value: "true",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			assert.Equal(tt.value, String(tt.value).underlying())
+		})
+	}
+}
+
+func TestStringMarshalJSON(t *testing.T) {
+	tests := []struct {
+		name     string
+		value    string
+		expected string
+	}{
+		{
+			name:     "string marshal JSON",
+			value:    "foo",
+			expected: "\"foo\"",
+		},
+		{
+			name:     "string marshal JSON",
+			value:    "lorem \"ipsum\" dolor",
+			expected: "\"lorem \\\"ipsum\\\" dolor\"",
+		},
+		{
+			name:     "string marshal JSON",
+			value:    "lorem ipsum\ndolor",
+			expected: "\"lorem ipsum\\ndolor\"",
+		},
+		{
+			name:     "string marshal JSON",
+			value:    "",
+			expected: "null",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			actual, err := String(tt.value).MarshalJSON()
+
+			assert.Nil(err)
+			assert.Equal(tt.expected, string(actual))
+		})
+	}
+}
+
+func TestStringMarshalXML(t *testing.T) {
+	tests := []struct {
+		name     string
+		value    string
+		expected string
+	}{
+		{
+			name:     "string marshal XML",
+			value:    "foo",
+			expected: "<test>foo</test>",
+		},
+		{
+			name:     "string marshal XML",
+			value:    "lorem <\"ipsum\"> 'dolor'",
+			expected: "<test>lorem &lt;&#34;ipsum&#34;&gt; &#39;dolor&#39;</test>",
+		},
+		{
+			name:     "string marshal XML",
+			value:    "",
+			expected: "<test xsi:nil=\"true\"></test>",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			var b bytes.Buffer
+			encoder := xml.NewEncoder(&b)
+			start := xml.StartElement{Name: xml.Name{Local: "test"}}
+
+			err := String(tt.value).MarshalXML(encoder, start)
+			assert.Nil(err)
+
+			err = encoder.Flush()
+			assert.Nil(err)
+
+			assert.Equal(tt.expected, b.String())
+		})
+	}
+}
+
+func TestStringMarshalYAML(t *testing.T) {
+	tests := []struct {
+		name     string
+		value    string
+		expected interface{}
+	}{
+		{
+			name:     "string marshal YAML",
+			value:    "foo",
+			expected: String("foo"),
+		},
+		{
+			name:     "string marshal YAML",
+			value:    "lorem ipsum",
+			expected: String("lorem ipsum"),
+		},
+		{
+			name:     "string marshal YAML",
+			value:    "",
+			expected: nil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			actual, err := String(tt.value).MarshalYAML()
+
+			assert.Nil(err)
+			assert.Equal(tt.expected, actual)
+		})
+	}
+}
diff --git a/internal/types/types_test.go b/internal/types/types_test.go
index 1f13f4d..f3b9d1e 100644
--- a/internal/types/types_test.go
+++ b/internal/types/types_test.go
@@ -57,8 +57,8 @@ func testList(t *testing.T, tests []testlist) {
 			t.Run(tt.name, func(t *testing.T) {
 				assert := assert.New(t)
 
-				actualValue := ValueOf(tv.underlying())
-				actualType := TypeOf(tt.types, tv.underlying())
+				actualValue := ValueOf(tv.Underlying())
+				actualType := TypeOf(tt.types, tv.Underlying())
 
 				assert.Equal(tt.expected.typeName, string(actualType))
 				assert.Equal(tt.expected.valueKind, reflect.TypeOf(actualValue).String())
@@ -74,8 +74,8 @@ func testMap(t *testing.T, tests []testmap) {
 			t.Run(tt.name, func(t *testing.T) {
 				assert := assert.New(t)
 
-				actualValue := ValueOf(tv.underlying())
-				actualType := TypeOf(tt.types, tv.underlying())
+				actualValue := ValueOf(tv.Underlying())
+				actualType := TypeOf(tt.types, tv.Underlying())
 
 				assert.Equal(tt.expected.typeName, string(actualType))
 				assert.Equal(tt.expected.valueKind, reflect.TypeOf(actualValue).String())
diff --git a/pkg/tfconf/input_test.go b/pkg/tfconf/input_test.go
new file mode 100644
index 0000000..d7bd9f9
--- /dev/null
+++ b/pkg/tfconf/input_test.go
@@ -0,0 +1,163 @@
+package tfconf
+
+import (
+	"testing"
+
+	"github.com/segmentio/terraform-docs/internal/types"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestInputValue(t *testing.T) {
+	inputName := "input"
+	inputType := types.String("type")
+	inputDescr := types.String("description")
+	inputPos := Position{Filename: "foo.tf", Line: 13}
+
+	tests := []struct {
+		name          string
+		input         Input
+		expectValue   string
+		expectDefault bool
+	}{
+		{
+			name: "input Value and HasDefault",
+			input: Input{
+				Name:        inputName,
+				Type:        inputType,
+				Description: inputDescr,
+				Default:     types.ValueOf(nil),
+				Position:    inputPos,
+			},
+			expectValue:   "",
+			expectDefault: false,
+		},
+		{
+			name: "input Value and HasDefault",
+			input: Input{
+				Name:        inputName,
+				Type:        inputType,
+				Description: inputDescr,
+				Default:     types.ValueOf(true),
+				Position:    inputPos,
+			},
+			expectValue:   "true",
+			expectDefault: true,
+		},
+		{
+			name: "input Value and HasDefault",
+			input: Input{
+				Name:        inputName,
+				Type:        inputType,
+				Description: inputDescr,
+				Default:     types.ValueOf(false),
+				Position:    inputPos,
+			},
+			expectValue:   "false",
+			expectDefault: true,
+		},
+		{
+			name: "input Value and HasDefault",
+			input: Input{
+				Name:        inputName,
+				Type:        inputType,
+				Description: inputDescr,
+				Default:     types.ValueOf(""),
+				Position:    inputPos,
+			},
+			expectValue:   "\"\"",
+			expectDefault: true,
+		},
+		{
+			name: "input Value and HasDefault",
+			input: Input{
+				Name:        inputName,
+				Type:        inputType,
+				Description: inputDescr,
+				Default:     types.ValueOf("foo"),
+				Position:    inputPos,
+			},
+			expectValue:   "\"foo\"",
+			expectDefault: true,
+		},
+		{
+			name: "input Value and HasDefault",
+			input: Input{
+				Name:        inputName,
+				Type:        inputType,
+				Description: inputDescr,
+				Default:     types.ValueOf(42),
+				Position:    inputPos,
+			},
+			expectValue:   "42",
+			expectDefault: true,
+		},
+		{
+			name: "input Value and HasDefault",
+			input: Input{
+				Name:        inputName,
+				Type:        inputType,
+				Description: inputDescr,
+				Default:     types.ValueOf(13.75),
+				Position:    inputPos,
+			},
+			expectValue:   "13.75",
+			expectDefault: true,
+		},
+		{
+			name: "input Value and HasDefault",
+			input: Input{
+				Name:        inputName,
+				Type:        inputType,
+				Description: inputDescr,
+				Default:     types.ValueOf(types.List{"a", "b", "c"}.Underlying()),
+				Position:    inputPos,
+			},
+			expectValue:   "[\n  \"a\",\n  \"b\",\n  \"c\"\n]",
+			expectDefault: true,
+		},
+		{
+			name: "input Value and HasDefault",
+			input: Input{
+				Name:        inputName,
+				Type:        inputType,
+				Description: inputDescr,
+				Default:     types.ValueOf(types.List{}.Underlying()),
+				Position:    inputPos,
+			},
+			expectValue:   "[]",
+			expectDefault: true,
+		},
+		{
+			name: "input Value and HasDefault",
+			input: Input{
+				Name:        inputName,
+				Type:        inputType,
+				Description: inputDescr,
+				Default:     types.ValueOf(types.Map{"a": 1, "b": 2, "c": 3}.Underlying()),
+				Position:    inputPos,
+			},
+			expectValue:   "{\n  \"a\": 1,\n  \"b\": 2,\n  \"c\": 3\n}",
+			expectDefault: true,
+		},
+		{
+			name: "input Value and HasDefault",
+			input: Input{
+				Name:        inputName,
+				Type:        inputType,
+				Description: inputDescr,
+				Default:     types.ValueOf(types.Map{}.Underlying()),
+				Position:    inputPos,
+			},
+			expectValue:   "{}",
+			expectDefault: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+
+			assert.Equal(tt.expectValue, tt.input.GetValue())
+			assert.Equal(tt.expectDefault, tt.input.HasDefault())
+		})
+	}
+}
diff --git a/pkg/tfconf/output_test.go b/pkg/tfconf/output_test.go
new file mode 100644
index 0000000..fd1db15
--- /dev/null
+++ b/pkg/tfconf/output_test.go
@@ -0,0 +1,420 @@
+package tfconf
+
+import (
+	"bytes"
+	"encoding/xml"
+	"reflect"
+	"testing"
+
+	"github.com/segmentio/terraform-docs/internal/types"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestOutputValue(t *testing.T) {
+	outputs := sampleOutputs()
+	tests := []struct {
+		name          string
+		output        Output
+		expectValue   string
+		expectDefault bool
+	}{
+		{
+			name:          "output Value and HasDefault",
+			output:        outputs[0],
+			expectValue:   "",
+			expectDefault: false,
+		},
+		{
+			name:          "output Value and HasDefault",
+			output:        outputs[1],
+			expectValue:   "",
+			expectDefault: false,
+		},
+		{
+			name:          "output Value and HasDefault",
+			output:        outputs[2],
+			expectValue:   "false",
+			expectDefault: true,
+		},
+		{
+			name:          "output Value and HasDefault",
+			output:        outputs[3],
+			expectValue:   "\"\"",
+			expectDefault: true,
+		},
+		{
+			name:          "output Value and HasDefault",
+			output:        outputs[4],
+			expectValue:   "\"foo\"",
+			expectDefault: true,
+		},
+		{
+			name:          "output Value and HasDefault",
+			output:        outputs[5],
+			expectValue:   "",
+			expectDefault: false,
+		},
+		{
+			name:          "output Value and HasDefault",
+			output:        outputs[6],
+			expectValue:   "\"\\u003csensitive\\u003e\"",
+			expectDefault: true,
+		},
+		{
+			name:          "output Value and HasDefault",
+			output:        outputs[7],
+			expectValue:   "[\n  \"a\",\n  \"b\",\n  \"c\"\n]",
+			expectDefault: true,
+		},
+		{
+			name:          "output Value and HasDefault",
+			output:        outputs[8],
+			expectValue:   "[]",
+			expectDefault: true,
+		},
+		{
+			name:          "output Value and HasDefault",
+			output:        outputs[9],
+			expectValue:   "{\n  \"a\": 1,\n  \"b\": 2,\n  \"c\": 3\n}",
+			expectDefault: true,
+		},
+		{
+			name:          "output Value and HasDefault",
+			output:        outputs[10],
+			expectValue:   "{}",
+			expectDefault: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+
+			assert.Equal(tt.expectValue, tt.output.GetValue())
+			assert.Equal(tt.expectDefault, tt.output.HasDefault())
+		})
+	}
+}
+
+func TestOutputMarshalJSON(t *testing.T) {
+	outputs := sampleOutputs()
+	tests := []struct {
+		name     string
+		output   Output
+		expected string
+	}{
+		{
+			name:     "output marshal JSON",
+			output:   outputs[0],
+			expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":null,\"sensitive\":false}\n",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[1],
+			expected: "{\"name\":\"output\",\"description\":\"description\"}\n",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[2],
+			expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":false,\"sensitive\":false}\n",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[3],
+			expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":\"\",\"sensitive\":false}\n",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[4],
+			expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":\"foo\",\"sensitive\":false}\n",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[5],
+			expected: "{\"name\":\"output\",\"description\":\"description\"}\n",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[6],
+			expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":\"<sensitive>\",\"sensitive\":true}\n",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[7],
+			expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":[\"a\",\"b\",\"c\"],\"sensitive\":false}\n",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[8],
+			expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":[],\"sensitive\":false}\n",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[9],
+			expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":{\"a\":1,\"b\":2,\"c\":3},\"sensitive\":false}\n",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[10],
+			expected: "{\"name\":\"output\",\"description\":\"description\",\"value\":{},\"sensitive\":false}\n",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			actual, err := tt.output.MarshalJSON()
+
+			assert.Nil(err)
+			assert.Equal(tt.expected, string(actual))
+		})
+	}
+}
+
+func TestOutputMarshalXML(t *testing.T) {
+	outputs := sampleOutputs()
+	tests := []struct {
+		name     string
+		output   Output
+		expected string
+	}{
+		{
+			name:     "output marshal XML",
+			output:   outputs[0],
+			expected: "<output><name>output</name><description>description</description><value xsi:nil=\"true\"></value><sensitive>false</sensitive></output>",
+		},
+		{
+			name:     "output marshal XML",
+			output:   outputs[1],
+			expected: "<output><name>output</name><description>description</description></output>",
+		},
+		{
+			name:     "output marshal XML",
+			output:   outputs[2],
+			expected: "<output><name>output</name><description>description</description><value>false</value><sensitive>false</sensitive></output>",
+		},
+		{
+			name:     "output marshal XML",
+			output:   outputs[3],
+			expected: "<output><name>output</name><description>description</description><value></value><sensitive>false</sensitive></output>",
+		},
+		{
+			name:     "output marshal XML",
+			output:   outputs[4],
+			expected: "<output><name>output</name><description>description</description><value>foo</value><sensitive>false</sensitive></output>",
+		},
+		{
+			name:     "output marshal XML",
+			output:   outputs[5],
+			expected: "<output><name>output</name><description>description</description></output>",
+		},
+		{
+			name:     "output marshal XML",
+			output:   outputs[6],
+			expected: "<output><name>output</name><description>description</description><value>&lt;sensitive&gt;</value><sensitive>true</sensitive></output>",
+		},
+		{
+			name:     "output marshal XML",
+			output:   outputs[7],
+			expected: "<output><name>output</name><description>description</description><value><item>a</item><item>b</item><item>c</item></value><sensitive>false</sensitive></output>",
+		},
+		{
+			name:     "output marshal XML",
+			output:   outputs[8],
+			expected: "<output><name>output</name><description>description</description><value></value><sensitive>false</sensitive></output>",
+		},
+		{
+			name:     "output marshal XML",
+			output:   outputs[9],
+			expected: "<output><name>output</name><description>description</description><value><a>1</a><b>2</b><c>3</c></value><sensitive>false</sensitive></output>",
+		},
+		{
+			name:     "output marshal XML",
+			output:   outputs[10],
+			expected: "<output><name>output</name><description>description</description><value></value><sensitive>false</sensitive></output>",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			var b bytes.Buffer
+			encoder := xml.NewEncoder(&b)
+			start := xml.StartElement{Name: xml.Name{Local: "output"}}
+
+			err := tt.output.MarshalXML(encoder, start)
+			assert.Nil(err)
+
+			err = encoder.Flush()
+			assert.Nil(err)
+
+			assert.Nil(err)
+			assert.Equal(tt.expected, b.String())
+		})
+	}
+}
+
+func TestOutputMarshalYAML(t *testing.T) {
+	outputs := sampleOutputs()
+	tests := []struct {
+		name     string
+		output   Output
+		expected string
+	}{
+		{
+			name:     "output marshal JSON",
+			output:   outputs[0],
+			expected: "tfconf.withvalue",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[1],
+			expected: "*tfconf.Output",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[2],
+			expected: "tfconf.withvalue",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[3],
+			expected: "tfconf.withvalue",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[4],
+			expected: "tfconf.withvalue",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[5],
+			expected: "*tfconf.Output",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[6],
+			expected: "tfconf.withvalue",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[7],
+			expected: "tfconf.withvalue",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[8],
+			expected: "tfconf.withvalue",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[9],
+			expected: "tfconf.withvalue",
+		},
+		{
+			name:     "output marshal JSON",
+			output:   outputs[10],
+			expected: "tfconf.withvalue",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			actual, err := tt.output.MarshalYAML()
+
+			assert.Nil(err)
+			assert.Equal(tt.expected, reflect.TypeOf(actual).String())
+		})
+	}
+}
+
+func sampleOutputs() []Output {
+	name := "output"
+	description := types.String("description")
+	position := Position{Filename: "foo.tf", Line: 13}
+	return []Output{
+		{
+			Name:        name,
+			Description: description,
+			Value:       types.ValueOf(nil),
+			Sensitive:   false,
+			Position:    position,
+			ShowValue:   true,
+		},
+		{
+			Name:        name,
+			Description: description,
+			Position:    position,
+			ShowValue:   false,
+		},
+		{
+			Name:        name,
+			Description: description,
+			Value:       types.ValueOf(false),
+			Sensitive:   false,
+			Position:    position,
+			ShowValue:   true,
+		},
+		{
+			Name:        name,
+			Description: description,
+			Value:       types.ValueOf(""),
+			Sensitive:   false,
+			Position:    position,
+			ShowValue:   true,
+		},
+		{
+			Name:        name,
+			Description: description,
+			Value:       types.ValueOf("foo"),
+			Sensitive:   false,
+			Position:    position,
+			ShowValue:   true,
+		},
+		{
+			Name:        name,
+			Description: description,
+			Value:       types.ValueOf("this should be hidden"),
+			Sensitive:   false,
+			Position:    position,
+			ShowValue:   false,
+		},
+		{
+			Name:        name,
+			Description: description,
+			Value:       types.ValueOf("<sensitive>"),
+			Sensitive:   true,
+			Position:    position,
+			ShowValue:   true,
+		},
+		{
+			Name:        name,
+			Description: description,
+			Value:       types.ValueOf(types.List{"a", "b", "c"}.Underlying()),
+			Sensitive:   false,
+			Position:    position,
+			ShowValue:   true,
+		},
+		{
+			Name:        name,
+			Description: description,
+			Value:       types.ValueOf(types.List{}.Underlying()),
+			Sensitive:   false,
+			Position:    position,
+			ShowValue:   true,
+		},
+		{
+			Name:        name,
+			Description: description,
+			Value:       types.ValueOf(types.Map{"a": 1, "b": 2, "c": 3}.Underlying()),
+			Sensitive:   false,
+			Position:    position,
+			ShowValue:   true,
+		},
+		{
+			Name:        name,
+			Description: description,
+			Value:       types.ValueOf(types.Map{}.Underlying()),
+			Sensitive:   false,
+			Position:    position,
+			ShowValue:   true,
+		},
+	}
+}
diff --git a/pkg/tfconf/provider_test.go b/pkg/tfconf/provider_test.go
new file mode 100644
index 0000000..0ee7e97
--- /dev/null
+++ b/pkg/tfconf/provider_test.go
@@ -0,0 +1,30 @@
+package tfconf
+
+import (
+	"testing"
+
+	"github.com/segmentio/terraform-docs/internal/types"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestProviderNameWithoutAlias(t *testing.T) {
+	assert := assert.New(t)
+	provider := Provider{
+		Name:     "provider",
+		Alias:    types.String(""),
+		Version:  types.String(">= 1.2.3"),
+		Position: Position{Filename: "foo.tf", Line: 13},
+	}
+	assert.Equal("provider", provider.FullName())
+}
+
+func TestProviderNameWithAlias(t *testing.T) {
+	assert := assert.New(t)
+	provider := Provider{
+		Name:     "provider",
+		Alias:    types.String("alias"),
+		Version:  types.String(">= 1.2.3"),
+		Position: Position{Filename: "foo.tf", Line: 13},
+	}
+	assert.Equal("provider.alias", provider.FullName())
+}
diff --git a/pkg/tmpl/sanitizer_test.go b/pkg/tmpl/sanitizer_test.go
new file mode 100644
index 0000000..47dfd14
--- /dev/null
+++ b/pkg/tmpl/sanitizer_test.go
@@ -0,0 +1,526 @@
+package tmpl
+
+import (
+	"io/ioutil"
+	"path/filepath"
+	"testing"
+
+	"github.com/segmentio/terraform-docs/internal/testutil"
+	"github.com/segmentio/terraform-docs/pkg/print"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSanitizeName(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    string
+		escape   bool
+		expected string
+	}{
+		{
+			name:     "sanitize name with escape character",
+			input:    "abcdefgh",
+			escape:   true,
+			expected: "abcdefgh",
+		},
+		{
+			name:     "sanitize name with escape character",
+			input:    "abcd_efgh",
+			escape:   true,
+			expected: "abcd\\_efgh",
+		},
+		{
+			name:     "sanitize name with escape character",
+			input:    "_abcdefgh",
+			escape:   true,
+			expected: "\\_abcdefgh",
+		},
+		{
+			name:     "sanitize name with escape character",
+			input:    "abcd__efgh",
+			escape:   true,
+			expected: "abcd\\_\\_efgh",
+		},
+		{
+			name:     "sanitize name with escape character",
+			input:    "_",
+			escape:   true,
+			expected: "\\_",
+		},
+		{
+			name:     "sanitize name with escape character",
+			input:    "",
+			escape:   true,
+			expected: "",
+		},
+		{
+			name:     "sanitize name without escape character",
+			input:    "abcdefgh",
+			escape:   false,
+			expected: "abcdefgh",
+		},
+		{
+			name:     "sanitize name without escape character",
+			input:    "abcd_efgh",
+			escape:   false,
+			expected: "abcd_efgh",
+		},
+		{
+			name:     "sanitize name without escape character",
+			input:    "abcd__efgh",
+			escape:   false,
+			expected: "abcd__efgh",
+		},
+		{
+			name:     "sanitize name without escape character",
+			input:    "_",
+			escape:   false,
+			expected: "_",
+		},
+		{
+			name:     "sanitize name without escape character",
+			input:    "",
+			escape:   false,
+			expected: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			settings := testutil.Settings().With(&print.Settings{
+				EscapeCharacters: tt.escape,
+			}).Build()
+			actual := sanitizeName(tt.input, settings)
+
+			assert.Equal(tt.expected, actual)
+		})
+	}
+}
+
+func TestSanitizeItemForDocument(t *testing.T) {
+	tests := []struct {
+		name        string
+		filename    string
+		escapeChars bool
+		escapePipe  bool
+	}{
+		{
+			name:        "sanitize document item empty",
+			filename:    "empty",
+			escapeChars: true,
+			escapePipe:  true,
+		},
+		{
+			name:        "sanitize document item complex",
+			filename:    "complex",
+			escapeChars: true,
+			escapePipe:  false,
+		},
+		{
+			name:        "sanitize document item codeblock",
+			filename:    "codeblock",
+			escapeChars: true,
+			escapePipe:  true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			settings := testutil.Settings().With(&print.Settings{
+				EscapeCharacters: tt.escapeChars,
+			}).Build()
+			settings.EscapePipe = tt.escapePipe
+
+			bytes, err := ioutil.ReadFile(filepath.Join("testdata", "document", tt.filename+".golden"))
+			assert.Nil(err)
+
+			actual := sanitizeItemForDocument(string(bytes), settings)
+
+			expected, err := ioutil.ReadFile(filepath.Join("testdata", "document", tt.filename+".expected"))
+			assert.Nil(err)
+
+			assert.Equal(string(expected), actual)
+		})
+	}
+}
+
+func TestSanitizeItemForTable(t *testing.T) {
+	tests := []struct {
+		name        string
+		filename    string
+		escapeChars bool
+		escapePipe  bool
+	}{
+		{
+			name:        "sanitize table item empty",
+			filename:    "empty",
+			escapeChars: true,
+		},
+		{
+			name:        "sanitize table item complex",
+			filename:    "complex",
+			escapeChars: true,
+		},
+		{
+			name:        "sanitize table item codeblock",
+			filename:    "codeblock",
+			escapeChars: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			settings := testutil.Settings().With(&print.Settings{
+				EscapeCharacters: tt.escapeChars,
+			}).Build()
+
+			bytes, err := ioutil.ReadFile(filepath.Join("testdata", "table", tt.filename+".golden"))
+			assert.Nil(err)
+
+			actual := sanitizeItemForTable(string(bytes), settings)
+
+			expected, err := ioutil.ReadFile(filepath.Join("testdata", "table", tt.filename+".expected"))
+			assert.Nil(err)
+
+			assert.Equal(string(expected), actual)
+		})
+	}
+}
+
+func TestConvertMultiLineText(t *testing.T) {
+	tests := []struct {
+		name     string
+		filename string
+		isTable  bool
+		expected string
+	}{
+		{
+			name:     "convert multi-line newline-single",
+			filename: "newline-single",
+			isTable:  false,
+			expected: "Lorem ipsum dolor sit amet,\n\nconsectetur adipiscing elit,\n\nsed do eiusmod tempor incididunt\n\nut labore et dolore magna aliqua.",
+		},
+		{
+			name:     "convert multi-line newline-single",
+			filename: "newline-single",
+			isTable:  true,
+			expected: "Lorem ipsum dolor sit amet,<br><br>consectetur adipiscing elit,<br><br>sed do eiusmod tempor incididunt<br><br>ut labore et dolore magna aliqua.",
+		},
+		{
+			name:     "convert multi-line newline-double",
+			filename: "newline-double",
+			isTable:  false,
+			expected: "Lorem ipsum dolor sit amet,\n\n\nconsectetur adipiscing elit,\n\n\nsed do eiusmod tempor incididunt\n\n\nut labore et dolore magna aliqua.",
+		},
+		{
+			name:     "convert multi-line newline-double",
+			filename: "newline-double",
+			isTable:  true,
+			expected: "Lorem ipsum dolor sit amet,<br><br><br>consectetur adipiscing elit,<br><br><br>sed do eiusmod tempor incididunt<br><br><br>ut labore et dolore magna aliqua.",
+		},
+		{
+			name:     "convert multi-line paragraph",
+			filename: "paragraph",
+			isTable:  false,
+			expected: "Lorem ipsum dolor sit amet,  \nconsectetur adipiscing elit,  \nsed do eiusmod tempor incididunt  \nut labore et dolore magna aliqua.",
+		},
+		{
+			name:     "convert multi-line paragraph",
+			filename: "paragraph",
+			isTable:  true,
+			expected: "Lorem ipsum dolor sit amet,<br>consectetur adipiscing elit,<br>sed do eiusmod tempor incididunt<br>ut labore et dolore magna aliqua.",
+		},
+		{
+			name:     "convert multi-line list",
+			filename: "list",
+			isTable:  false,
+			expected: "- Lorem ipsum dolor sit amet,\n  * Lorem ipsum dolor sit amet,\n  * consectetur adipiscing elit,\n- consectetur adipiscing elit,\n- sed do eiusmod tempor incididunt\n- ut labore et dolore magna aliqua.",
+		},
+		{
+			name:     "convert multi-line list",
+			filename: "list",
+			isTable:  true,
+			expected: "- Lorem ipsum dolor sit amet,<br>  * Lorem ipsum dolor sit amet,<br>  * consectetur adipiscing elit,<br>- consectetur adipiscing elit,<br>- sed do eiusmod tempor incididunt<br>- ut labore et dolore magna aliqua.",
+		},
+		{
+			name:     "convert multi-line indentations",
+			filename: "indentations",
+			isTable:  false,
+			expected: "This is is a multline test which works\n\nKey  \n  Foo1: blah  \n  Foo2: blah\n\nKey2  \nFoo1: bar1  \nFoo2: bar2",
+		},
+		{
+			name:     "convert multi-line indentations",
+			filename: "indentations",
+			isTable:  true,
+			expected: "This is is a multline test which works<br><br>Key<br>  Foo1: blah<br>  Foo2: blah<br><br>Key2<br>Foo1: bar1<br>Foo2: bar2",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+
+			path := filepath.Join("testdata", "multiline", tt.filename+".golden")
+			bytes, err := ioutil.ReadFile(path)
+			assert.Nil(err)
+
+			actual := convertMultiLineText(string(bytes), tt.isTable)
+			assert.Equal(tt.expected, actual)
+		})
+	}
+}
+
+func TestEscapeIllegalCharacters(t *testing.T) {
+	tests := []struct {
+		name        string
+		input       string
+		escapePipe  bool
+		escapeChars bool
+		expected    string
+	}{
+		{
+			name:        "escape pipe",
+			input:       "lorem | ipsum |dolor|consectetur| `adipi | scing |elit|sit|`",
+			escapePipe:  true,
+			escapeChars: false,
+			expected:    "lorem \\| ipsum \\|dolor\\|consectetur\\| `adipi | scing |elit|sit|`",
+		},
+		{
+			name:        "escape pipe",
+			input:       "lorem || ipsum ||dolor||consectetur|| `adipi || scing ||elit||sit||`",
+			escapePipe:  true,
+			escapeChars: false,
+			expected:    "lorem \\|\\| ipsum \\|\\|dolor\\|\\|consectetur\\|\\| `adipi || scing ||elit||sit||`",
+		},
+		{
+			name:        "escape pipe",
+			input:       "lorem ||| ipsum |||dolor|||consectetur||| `adipi ||| scing |||elit|||sit|||`",
+			escapePipe:  true,
+			escapeChars: false,
+			expected:    "lorem \\|\\|\\| ipsum \\|\\|\\|dolor\\|\\|\\|consectetur\\|\\|\\| `adipi ||| scing |||elit|||sit|||`",
+		},
+		{
+			name:        "do not escape pipe",
+			input:       "lorem | ipsum |dolor|consectetur| `adipi | scing |elit|sit|`",
+			escapePipe:  false,
+			escapeChars: false,
+			expected:    "lorem | ipsum |dolor|consectetur| `adipi | scing |elit|sit|`",
+		},
+		{
+			name:        "do not escape pipe",
+			input:       "lorem || ipsum ||dolor||consectetur|| `adipi || scing ||elit||sit||`",
+			escapePipe:  false,
+			escapeChars: false,
+			expected:    "lorem || ipsum ||dolor||consectetur|| `adipi || scing ||elit||sit||`",
+		},
+		{
+			name:        "do not escape pipe",
+			input:       "lorem ||| ipsum |||dolor|||consectetur||| `adipi ||| scing |||elit|||sit|||`",
+			escapePipe:  false,
+			escapeChars: false,
+			expected:    "lorem ||| ipsum |||dolor|||consectetur||| `adipi ||| scing |||elit|||sit|||`",
+		},
+		{
+			name:        "escape underscore",
+			input:       "lorem _ ipsum _dolor_consectetur_ incid_idunt `adipi _ scing _elit_sit_`",
+			escapePipe:  false,
+			escapeChars: true,
+			expected:    "lorem \\_ ipsum _dolor\\_consectetur_ incid\\_idunt `adipi _ scing _elit_sit_`",
+		},
+		{
+			name:        "escape underscore",
+			input:       "lorem __ ipsum __dolor__consectetur__ incid__idunt `adipi __ scing __elit__sit__`",
+			escapePipe:  false,
+			escapeChars: true,
+			expected:    "lorem \\_\\_ ipsum __dolor\\_\\_consectetur__ incid\\_\\_idunt `adipi __ scing __elit__sit__`",
+		},
+		{
+			name:        "escape underscore",
+			input:       "lorem ___ ipsum ___dolor___consectetur___ incid___idunt `adipi ___ scing ___elit___sit___`",
+			escapePipe:  false,
+			escapeChars: true,
+			expected:    "lorem \\_\\_\\_ ipsum ___dolor\\_\\_\\_consectetur___ incid\\_\\_\\_idunt `adipi ___ scing ___elit___sit___`",
+		},
+		{
+			name:        "do not escape underscore",
+			input:       "lorem _ ipsum _dolor_consectetur_ incid_idunt `adipi _ scing _elit_sit_`",
+			escapePipe:  false,
+			escapeChars: false,
+			expected:    "lorem _ ipsum _dolor_consectetur_ incid_idunt `adipi _ scing _elit_sit_`",
+		},
+		{
+			name:        "do not escape underscore",
+			input:       "lorem __ ipsum __dolor__consectetur__ incid__idunt `adipi __ scing __elit__sit__`",
+			escapePipe:  false,
+			escapeChars: false,
+			expected:    "lorem __ ipsum __dolor__consectetur__ incid__idunt `adipi __ scing __elit__sit__`",
+		},
+		{
+			name:        "do not escape underscore",
+			input:       "lorem ___ ipsum ___dolor___consectetur___ incid___idunt `adipi ___ scing ___elit___sit___`",
+			escapePipe:  false,
+			escapeChars: false,
+			expected:    "lorem ___ ipsum ___dolor___consectetur___ incid___idunt `adipi ___ scing ___elit___sit___`",
+		},
+		{
+			name:        "escape asterisk",
+			input:       "* lorem * ipsum *dolor*consectetur* `adipi * scing *elit*sit*`",
+			escapePipe:  false,
+			escapeChars: true,
+			expected:    "* lorem \\* ipsum *dolor\\*consectetur* `adipi * scing *elit*sit*`",
+		},
+		{
+			name:        "escape asterisk",
+			input:       "** lorem ** ipsum **dolor**consectetur** `adipi ** scing **elit**sit**`",
+			escapePipe:  false,
+			escapeChars: true,
+			expected:    "** lorem \\*\\* ipsum **dolor\\*\\*consectetur** `adipi ** scing **elit**sit**`",
+		},
+		{
+			name:        "escape asterisk",
+			input:       "*** lorem *** ipsum ***dolor***consectetur*** `adipi *** scing ***elit***sit***`",
+			escapePipe:  false,
+			escapeChars: true,
+			expected:    "*** lorem \\*\\*\\* ipsum ***dolor\\*\\*\\*consectetur*** `adipi *** scing ***elit***sit***`",
+		},
+		{
+			name:        "do not escape asterisk",
+			input:       "* lorem * ipsum *dolor*consectetur* `adipi * scing *elit*sit*`",
+			escapePipe:  false,
+			escapeChars: false,
+			expected:    "* lorem * ipsum *dolor*consectetur* `adipi * scing *elit*sit*`",
+		},
+		{
+			name:        "do not escape asterisk",
+			input:       "** lorem ** ipsum **dolor**consectetur** `adipi ** scing **elit**sit**`",
+			escapePipe:  false,
+			escapeChars: false,
+			expected:    "** lorem ** ipsum **dolor**consectetur** `adipi ** scing **elit**sit**`",
+		},
+		{
+			name:        "do not escape asterisk",
+			input:       "*** lorem *** ipsum ***dolor***consectetur*** `adipi *** scing ***elit***sit***`",
+			escapePipe:  false,
+			escapeChars: false,
+			expected:    "*** lorem *** ipsum ***dolor***consectetur*** `adipi *** scing ***elit***sit***`",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			settings := testutil.Settings().With(&print.Settings{
+				EscapeCharacters: tt.escapeChars,
+			}).Build()
+			settings.EscapePipe = tt.escapePipe
+			actual := escapeIllegalCharacters(tt.input, settings)
+
+			assert.Equal(tt.expected, actual)
+		})
+	}
+}
+
+func TestNormalizeURLs(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    string
+		escape   bool
+		expected string
+	}{
+		{
+			name:     "normalize url with escape character",
+			input:    "https://www.foo\\_bar.com/",
+			escape:   true,
+			expected: "https://www.foo_bar.com/",
+		},
+		{
+			name:     "normalize url with escape character",
+			input:    "lorem ipsum https://www.foo\\_bar.com/ dolor sit amet",
+			escape:   true,
+			expected: "lorem ipsum https://www.foo_bar.com/ dolor sit amet",
+		},
+		{
+			name:     "normalize url with escape character",
+			input:    "lorem\\_ipsum https://www.foo\\_bar.com/ dolor sit amet",
+			escape:   true,
+			expected: "lorem\\_ipsum https://www.foo_bar.com/ dolor sit amet",
+		},
+		{
+			name:     "normalize url without escape character",
+			input:    "https://www.foo_bar.com/",
+			escape:   false,
+			expected: "https://www.foo_bar.com/",
+		},
+		{
+			name:     "normalize url without escape character",
+			input:    "https://www.foo\\_bar.com/",
+			escape:   false,
+			expected: "https://www.foo\\_bar.com/",
+		},
+		{
+			name:     "normalize url with escape character",
+			input:    "lorem\\_ipsum https://www.foo\\_bar.com/ dolor sit amet",
+			escape:   false,
+			expected: "lorem\\_ipsum https://www.foo\\_bar.com/ dolor sit amet",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			settings := testutil.Settings().With(&print.Settings{
+				EscapeCharacters: tt.escape,
+			}).Build()
+			actual := normalizeURLs(tt.input, settings)
+
+			assert.Equal(tt.expected, actual)
+		})
+	}
+}
+
+func TestGenerateIndentation(t *testing.T) {
+	tests := []struct {
+		name     string
+		base     int
+		extra    int
+		expected string
+	}{
+		{
+			name:     "generate indentation",
+			base:     2,
+			extra:    1,
+			expected: "###",
+		},
+		{
+			name:     "generate indentation",
+			extra:    2,
+			expected: "####",
+		},
+		{
+			name:     "generate indentation",
+			base:     4,
+			extra:    3,
+			expected: "#######",
+		},
+		{
+			name:     "generate indentation",
+			base:     0,
+			extra:    0,
+			expected: "##",
+		},
+		{
+			name:     "generate indentation",
+			base:     6,
+			extra:    1,
+			expected: "###",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			settings := testutil.Settings().With(&print.Settings{
+				MarkdownIndent: tt.base,
+			}).Build()
+			actual := generateIndentation(tt.extra, settings)
+
+			assert.Equal(tt.expected, actual)
+		})
+	}
+}
diff --git a/pkg/tmpl/template_test.go b/pkg/tmpl/template_test.go
new file mode 100644
index 0000000..e8dc815
--- /dev/null
+++ b/pkg/tmpl/template_test.go
@@ -0,0 +1,522 @@
+package tmpl
+
+import (
+	"fmt"
+	"reflect"
+	"strconv"
+	"strings"
+	"testing"
+	"text/template"
+
+	"github.com/segmentio/terraform-docs/internal/types"
+	"github.com/segmentio/terraform-docs/pkg/print"
+	"github.com/segmentio/terraform-docs/pkg/tfconf"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestTemplateRender(t *testing.T) {
+	sectionTpl := `
+	{{- with .Module.Header -}}
+		{{ custom . }}
+	{{- end -}}
+	`
+	customFuncs := template.FuncMap{
+		"custom": func(s string) string {
+			return fmt.Sprintf("customized <<%s>>", s)
+		},
+	}
+	module := &tfconf.Module{
+		Header: "sample header",
+	}
+	tests := []struct {
+		name     string
+		items    []*Item
+		expected string
+		wantErr  bool
+	}{
+		{
+			name: "template render with custom functions",
+			items: []*Item{
+				&Item{
+					Name: "all",
+					Text: `{{- template "section" . -}}`,
+				}, &Item{
+					Name: "section",
+					Text: sectionTpl,
+				},
+			},
+			expected: "customized <<sample header>>",
+			wantErr:  false,
+		},
+		{
+			name:     "template render with custom functions",
+			items:    []*Item{},
+			expected: "",
+			wantErr:  true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			tpl := NewTemplate(tt.items...)
+			tpl.Settings(print.NewSettings())
+			tpl.CustomFunc(customFuncs)
+			rendered, err := tpl.Render(module)
+			if tt.wantErr {
+				assert.NotNil(err)
+			} else {
+				assert.Nil(err)
+				assert.Equal(tt.expected, rendered)
+			}
+		})
+	}
+}
+
+func TestBuiltinFunc(t *testing.T) {
+	tests := []struct {
+		name       string
+		funcName   string
+		funcArgs   []string
+		escapeChar bool
+		escapePipe bool
+		expected   string
+	}{
+		// default
+		{
+			name:       "template builtin functions default",
+			funcName:   "default",
+			funcArgs:   []string{`"a"`, `"b"`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "b",
+		},
+		{
+			name:       "template builtin functions default",
+			funcName:   "default",
+			funcArgs:   []string{`"a"`, `""`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "a",
+		},
+		{
+			name:       "template builtin functions default",
+			funcName:   "default",
+			funcArgs:   []string{`""`, `"b"`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "b",
+		},
+		{
+			name:       "template builtin functions default",
+			funcName:   "default",
+			funcArgs:   []string{`""`, `""`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "",
+		},
+
+		// tostring
+		{
+			name:       "template builtin functions tostring",
+			funcName:   "tostring",
+			funcArgs:   []string{`"foo"`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "foo",
+		},
+		{
+			name:       "template builtin functions tostring",
+			funcName:   "tostring",
+			funcArgs:   []string{`""`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "",
+		},
+
+		// trim
+		{
+			name:       "template builtin functions trim",
+			funcName:   "trim",
+			funcArgs:   []string{`" "`, `"   foo   "`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "foo",
+		},
+		{
+			name:       "template builtin functions trim",
+			funcName:   "trim",
+			funcArgs:   []string{`" "`, `"foo"`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "foo",
+		},
+		{
+			name:       "template builtin functions trim",
+			funcName:   "trim",
+			funcArgs:   []string{`""`, `"foo"`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "foo",
+		},
+		{
+			name:       "template builtin functions trim",
+			funcName:   "trim",
+			funcArgs:   []string{`" "`, `""`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "",
+		},
+
+		// trimLeft
+		{
+			name:       "template builtin functions trimLeft",
+			funcName:   "trimLeft",
+			funcArgs:   []string{`" "`, `"   foo   "`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "foo   ",
+		},
+		{
+			name:       "template builtin functions trimLeft",
+			funcName:   "trimLeft",
+			funcArgs:   []string{`" "`, `"foo"`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "foo",
+		},
+		{
+			name:       "template builtin functions trimLeft",
+			funcName:   "trimLeft",
+			funcArgs:   []string{`""`, `"foo"`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "foo",
+		},
+		{
+			name:       "template builtin functions trimLeft",
+			funcName:   "trimLeft",
+			funcArgs:   []string{`" "`, `""`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "",
+		},
+
+		// trimRight
+		{
+			name:       "template builtin functions trimRight",
+			funcName:   "trimRight",
+			funcArgs:   []string{`" "`, `"   foo   "`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "   foo",
+		},
+		{
+			name:       "template builtin functions trimRight",
+			funcName:   "trimRight",
+			funcArgs:   []string{`" "`, `"foo"`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "foo",
+		},
+		{
+			name:       "template builtin functions trimRight",
+			funcName:   "trimRight",
+			funcArgs:   []string{`""`, `"foo"`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "foo",
+		},
+		{
+			name:       "template builtin functions trimRight",
+			funcName:   "trimRight",
+			funcArgs:   []string{`" "`, `""`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "",
+		},
+
+		// trimPrefix
+		{
+			name:       "template builtin functions trimPrefix",
+			funcName:   "trimPrefix",
+			funcArgs:   []string{`" "`, `"   foo   "`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "  foo   ",
+		},
+		{
+			name:       "template builtin functions trimPrefix",
+			funcName:   "trimPrefix",
+			funcArgs:   []string{`" "`, `"foo"`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "foo",
+		},
+		{
+			name:       "template builtin functions trimPrefix",
+			funcName:   "trimPrefix",
+			funcArgs:   []string{`""`, `"foo"`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "foo",
+		},
+		{
+			name:       "template builtin functions trimPrefix",
+			funcName:   "trimPrefix",
+			funcArgs:   []string{`" "`, `""`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "",
+		},
+
+		// trimSuffix
+		{
+			name:       "template builtin functions trimSuffix",
+			funcName:   "trimSuffix",
+			funcArgs:   []string{`" "`, `"   foo   "`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "   foo  ",
+		},
+		{
+			name:       "template builtin functions trimSuffix",
+			funcName:   "trimSuffix",
+			funcArgs:   []string{`" "`, `"foo"`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "foo",
+		},
+		{
+			name:       "template builtin functions trimSuffix",
+			funcName:   "trimSuffix",
+			funcArgs:   []string{`""`, `"foo"`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "foo",
+		},
+		{
+			name:       "template builtin functions trimSuffix",
+			funcName:   "trimSuffix",
+			funcArgs:   []string{`" "`, `""`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "",
+		},
+
+		// indent
+		{
+			name:       "template builtin functions indent",
+			funcName:   "indent",
+			funcArgs:   []string{`0`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "##",
+		},
+		{
+			name:       "template builtin functions indent",
+			funcName:   "indent",
+			funcArgs:   []string{`1`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "###",
+		},
+		{
+			name:       "template builtin functions indent",
+			funcName:   "indent",
+			funcArgs:   []string{`2`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "####",
+		},
+		{
+			name:       "template builtin functions indent",
+			funcName:   "indent",
+			funcArgs:   []string{`3`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "#####",
+		},
+
+		// name
+		{
+			name:       "template builtin functions name",
+			funcName:   "name",
+			funcArgs:   []string{`"foo"`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "foo",
+		},
+		{
+			name:       "template builtin functions name",
+			funcName:   "name",
+			funcArgs:   []string{`"foo_bar"`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "foo\\_bar",
+		},
+		{
+			name:       "template builtin functions name",
+			funcName:   "name",
+			funcArgs:   []string{`"foo_bar"`},
+			escapeChar: false,
+			escapePipe: true,
+			expected:   "foo_bar",
+		},
+		{
+			name:       "template builtin functions name",
+			funcName:   "name",
+			funcArgs:   []string{`""`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "",
+		},
+
+		// sanitizeHeader
+		{
+			name:       "template builtin functions sanitizeHeader",
+			funcName:   "sanitizeHeader",
+			funcArgs:   []string{"\"Example of 'foo_bar' module in `foo_bar.tf`.\n\n| Foo | Bar |\n|-----|-----|\n| foo | bar |\""},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "Example of 'foo\\_bar' module in `foo_bar.tf`.\n\n| Foo | Bar |\n|-----|-----|\n| foo | bar |",
+		},
+		{
+			name:       "template builtin functions sanitizeHeader",
+			funcName:   "sanitizeHeader",
+			funcArgs:   []string{"\"Example of 'foo_bar' module in `foo_bar.tf`.\n\n| Foo | Bar |\""},
+			escapeChar: false,
+			escapePipe: true,
+			expected:   "Example of 'foo_bar' module in `foo_bar.tf`.\n\n| Foo | Bar |",
+		},
+		{
+			name:       "template builtin functions sanitizeHeader",
+			funcName:   "sanitizeHeader",
+			funcArgs:   []string{"\"Example of 'foo_bar' module in `foo_bar.tf`.\n\n| Foo | Bar |\""},
+			escapeChar: false,
+			escapePipe: false,
+			expected:   "Example of 'foo_bar' module in `foo_bar.tf`.\n\n| Foo | Bar |",
+		},
+		{
+			name:       "template builtin functions sanitizeHeader",
+			funcName:   "sanitizeHeader",
+			funcArgs:   []string{`""`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "n/a",
+		},
+
+		// sanitizeDoc
+		{
+			name:       "template builtin functions sanitizeDoc",
+			funcName:   "sanitizeDoc",
+			funcArgs:   []string{"\"Example of 'foo_bar' module in `foo_bar.tf`.\n\n| Foo | Bar |\""},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "Example of 'foo\\_bar' module in `foo_bar.tf`.\n\n\\| Foo \\| Bar \\|",
+		},
+		{
+			name:       "template builtin functions sanitizeDoc",
+			funcName:   "sanitizeDoc",
+			funcArgs:   []string{"\"Example of 'foo_bar' module in `foo_bar.tf`.\n\n| Foo | Bar |\""},
+			escapeChar: false,
+			escapePipe: true,
+			expected:   "Example of 'foo_bar' module in `foo_bar.tf`.\n\n\\| Foo \\| Bar \\|",
+		},
+		{
+			name:       "template builtin functions sanitizeDoc",
+			funcName:   "sanitizeDoc",
+			funcArgs:   []string{"\"Example of 'foo_bar' module in `foo_bar.tf`.\n\n| Foo | Bar |\""},
+			escapeChar: false,
+			escapePipe: false,
+			expected:   "Example of 'foo_bar' module in `foo_bar.tf`.\n\n| Foo | Bar |",
+		},
+		{
+			name:       "template builtin functions sanitizeDoc",
+			funcName:   "sanitizeDoc",
+			funcArgs:   []string{`""`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "n/a",
+		},
+
+		// sanitizeTbl
+		{
+			name:       "template builtin functions sanitizeTbl",
+			funcName:   "sanitizeTbl",
+			funcArgs:   []string{"\"Example of 'foo_bar' module in `foo_bar.tf`.\n\n| Foo | Bar |\""},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "Example of 'foo\\_bar' module in `foo_bar.tf`.<br><br>\\| Foo \\| Bar \\|",
+		},
+		{
+			name:       "template builtin functions sanitizeTbl",
+			funcName:   "sanitizeTbl",
+			funcArgs:   []string{"\"Example of 'foo_bar' module in `foo_bar.tf`.\n\n| Foo | Bar |\""},
+			escapeChar: false,
+			escapePipe: true,
+			expected:   "Example of 'foo_bar' module in `foo_bar.tf`.<br><br>\\| Foo \\| Bar \\|",
+		},
+		{
+			name:       "template builtin functions sanitizeTbl",
+			funcName:   "sanitizeTbl",
+			funcArgs:   []string{"\"Example of 'foo_bar' module in `foo_bar.tf`.\n\n| Foo | Bar |\""},
+			escapeChar: false,
+			escapePipe: false,
+			expected:   "Example of 'foo_bar' module in `foo_bar.tf`.<br><br>\\| Foo \\| Bar \\|",
+		},
+		{
+			name:       "template builtin functions sanitizeTbl",
+			funcName:   "sanitizeTbl",
+			funcArgs:   []string{`""`},
+			escapeChar: true,
+			escapePipe: true,
+			expected:   "n/a",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert := assert.New(t)
+			settings := print.NewSettings()
+			settings.EscapeCharacters = tt.escapeChar
+			settings.EscapePipe = tt.escapePipe
+			funcs := builtinFuncs(settings)
+
+			fn, ok := funcs[tt.funcName]
+			assert.Truef(ok, "function %s is not defined", tt.funcName)
+
+			v := reflect.ValueOf(fn)
+			tp := v.Type()
+			assert.Equalf(len(tt.funcArgs), tp.NumIn(), "invalid number of arguments. got: %v, want: %v", len(tt.funcArgs), tp.NumIn())
+
+			argv := make([]reflect.Value, len(tt.funcArgs))
+
+			for i := range argv {
+				var argType reflect.Kind
+				if strings.HasPrefix(tt.funcArgs[i], "\"") {
+					if tt.funcName == "tostring" {
+						argType = reflect.TypeOf(types.String("")).Kind()
+						argv[i] = reflect.ValueOf(types.String(strings.Trim(tt.funcArgs[i], "\"")))
+					} else {
+						argType = reflect.String
+						argv[i] = reflect.ValueOf(strings.Trim(tt.funcArgs[i], "\""))
+					}
+				} else {
+					argType = reflect.Int
+					num, _ := strconv.Atoi(tt.funcArgs[i])
+					argv[i] = reflect.ValueOf(num)
+				}
+				if tp.In(i).Kind() != argType {
+					assert.Fail("Invalid argument. got: %v, want: %v", argType, tp.In(i).Kind())
+				}
+			}
+
+			result := v.Call(argv)
+
+			if len(result) != 1 || result[0].Kind() != reflect.String {
+				assert.Fail("function %s must return a one string value", tt.funcName)
+			}
+
+			assert.Equal(tt.expected, result[0].String())
+		})
+	}
+}
diff --git a/pkg/tmpl/testdata/document/codeblock.expected b/pkg/tmpl/testdata/document/codeblock.expected
new file mode 100644
index 0000000..7bd9129
--- /dev/null
+++ b/pkg/tmpl/testdata/document/codeblock.expected
@@ -0,0 +1,9 @@
+This is a complicated one. We need a newline.  
+And an example in a code block. Availeble options  
+are: foo \| bar \| baz
+
+```
+default = [
+  "foo"
+]
+```
\ No newline at end of file
diff --git a/pkg/tmpl/testdata/document/codeblock.golden b/pkg/tmpl/testdata/document/codeblock.golden
new file mode 100644
index 0000000..4e81a60
--- /dev/null
+++ b/pkg/tmpl/testdata/document/codeblock.golden
@@ -0,0 +1,8 @@
+This is a complicated one. We need a newline.  
+And an example in a code block. Availeble options
+are: foo | bar | baz
+
+```
+default = [
+  "foo"
+]```
\ No newline at end of file
diff --git a/pkg/tmpl/testdata/document/complex.expected b/pkg/tmpl/testdata/document/complex.expected
new file mode 100644
index 0000000..df0ce93
--- /dev/null
+++ b/pkg/tmpl/testdata/document/complex.expected
@@ -0,0 +1,37 @@
+Usage:
+
+Example of 'foo\_bar' module in `foo_bar.tf`.
+
+- list item 1
+- list item 2
+
+Even inline **formatting** in _here_ is possible.  
+and some [link](https://domain.com/)
+
+* list item 3
+* list item 4
+
+```hcl
+module "foo_bar" {
+  source = "github.com/foo/bar"
+
+  id   = "1234567890"
+  name = "baz"
+
+  zones = ["us-east-1", "us-west-1"]
+
+  tags = {
+    Name         = "baz"
+    Created-By   = "first.last@email.com"
+    Date-Created = "20180101"
+  }
+}
+```
+
+Here is some trailing text after code block,  
+followed by another line of text.
+
+| Name | Description     |
+|------|-----------------|
+| Foo  | Foo description |
+| Bar  | Bar description |
\ No newline at end of file
diff --git a/pkg/tmpl/testdata/document/complex.golden b/pkg/tmpl/testdata/document/complex.golden
new file mode 100644
index 0000000..303e201
--- /dev/null
+++ b/pkg/tmpl/testdata/document/complex.golden
@@ -0,0 +1,37 @@
+Usage:
+
+Example of 'foo_bar' module in `foo_bar.tf`.
+
+- list item 1
+- list item 2
+
+Even inline **formatting** in _here_ is possible.
+and some [link](https://domain.com/)
+
+* list item 3
+* list item 4
+
+```hcl
+module "foo_bar" {
+  source = "github.com/foo/bar"
+
+  id   = "1234567890"
+  name = "baz"
+
+  zones = ["us-east-1", "us-west-1"]
+
+  tags = {
+    Name         = "baz"
+    Created-By   = "first.last@email.com"
+    Date-Created = "20180101"
+  }
+}
+```
+
+Here is some trailing text after code block,
+followed by another line of text.
+
+| Name | Description     |
+|------|-----------------|
+| Foo  | Foo description |
+| Bar  | Bar description |
\ No newline at end of file
diff --git a/pkg/tmpl/testdata/document/empty.expected b/pkg/tmpl/testdata/document/empty.expected
new file mode 100644
index 0000000..d8e0871
--- /dev/null
+++ b/pkg/tmpl/testdata/document/empty.expected
@@ -0,0 +1,1 @@
+n/a
\ No newline at end of file
diff --git a/pkg/tmpl/testdata/document/empty.golden b/pkg/tmpl/testdata/document/empty.golden
new file mode 100644
index 0000000..e69de29
diff --git a/pkg/tmpl/testdata/multiline/indentations.golden b/pkg/tmpl/testdata/multiline/indentations.golden
new file mode 100644
index 0000000..4c66acb
--- /dev/null
+++ b/pkg/tmpl/testdata/multiline/indentations.golden
@@ -0,0 +1,9 @@
+This is is a multline test which works
+
+Key
+  Foo1: blah
+  Foo2: blah
+
+Key2
+Foo1: bar1
+Foo2: bar2
\ No newline at end of file
diff --git a/pkg/tmpl/testdata/multiline/list.golden b/pkg/tmpl/testdata/multiline/list.golden
new file mode 100644
index 0000000..5e4ab18
--- /dev/null
+++ b/pkg/tmpl/testdata/multiline/list.golden
@@ -0,0 +1,6 @@
+- Lorem ipsum dolor sit amet,
+  * Lorem ipsum dolor sit amet,
+  * consectetur adipiscing elit,
+- consectetur adipiscing elit,
+- sed do eiusmod tempor incididunt
+- ut labore et dolore magna aliqua.
\ No newline at end of file
diff --git a/pkg/tmpl/testdata/multiline/newline-double.golden b/pkg/tmpl/testdata/multiline/newline-double.golden
new file mode 100644
index 0000000..c28cc91
--- /dev/null
+++ b/pkg/tmpl/testdata/multiline/newline-double.golden
@@ -0,0 +1,10 @@
+Lorem ipsum dolor sit amet,
+
+
+consectetur adipiscing elit,
+
+
+sed do eiusmod tempor incididunt
+
+
+ut labore et dolore magna aliqua.
\ No newline at end of file
diff --git a/pkg/tmpl/testdata/multiline/newline-single.golden b/pkg/tmpl/testdata/multiline/newline-single.golden
new file mode 100644
index 0000000..3f90fde
--- /dev/null
+++ b/pkg/tmpl/testdata/multiline/newline-single.golden
@@ -0,0 +1,7 @@
+Lorem ipsum dolor sit amet,
+
+consectetur adipiscing elit,
+
+sed do eiusmod tempor incididunt
+
+ut labore et dolore magna aliqua.
\ No newline at end of file
diff --git a/pkg/tmpl/testdata/multiline/paragraph.golden b/pkg/tmpl/testdata/multiline/paragraph.golden
new file mode 100644
index 0000000..d7f897a
--- /dev/null
+++ b/pkg/tmpl/testdata/multiline/paragraph.golden
@@ -0,0 +1,4 @@
+Lorem ipsum dolor sit amet,
+consectetur adipiscing elit,
+sed do eiusmod tempor incididunt
+ut labore et dolore magna aliqua.
\ No newline at end of file
diff --git a/pkg/tmpl/testdata/table/codeblock.expected b/pkg/tmpl/testdata/table/codeblock.expected
new file mode 100644
index 0000000..8e60f2a
--- /dev/null
+++ b/pkg/tmpl/testdata/table/codeblock.expected
@@ -0,0 +1,1 @@
+This is a complicated one. We need a newline.<br>And an example in a code block. Availeble options<br>are: foo \| bar \| baz<pre>default = [<br>  "foo"<br>]</pre>
\ No newline at end of file
diff --git a/pkg/tmpl/testdata/table/codeblock.golden b/pkg/tmpl/testdata/table/codeblock.golden
new file mode 100644
index 0000000..4e81a60
--- /dev/null
+++ b/pkg/tmpl/testdata/table/codeblock.golden
@@ -0,0 +1,8 @@
+This is a complicated one. We need a newline.  
+And an example in a code block. Availeble options
+are: foo | bar | baz
+
+```
+default = [
+  "foo"
+]```
\ No newline at end of file
diff --git a/pkg/tmpl/testdata/table/complex.expected b/pkg/tmpl/testdata/table/complex.expected
new file mode 100644
index 0000000..c822e02
--- /dev/null
+++ b/pkg/tmpl/testdata/table/complex.expected
@@ -0,0 +1,1 @@
+Usage:<br><br>Example of 'foo\_bar' module in `foo_bar.tf`.<br><br>- list item 1<br>- list item 2<br><br>Even inline **formatting** in _here_ is possible.<br>and some [link](https://domain.com/)<br><br>* list item 3<br>* list item 4<pre>module "foo_bar" {<br>  source = "github.com/foo/bar"<br><br>  id   = "1234567890"<br>  name = "baz"<br><br>  zones = ["us-east-1", "us-west-1"]<br><br>  tags = {<br>    Name         = "baz"<br>    Created-By   = "first.last@email.com"<br>    Date-Created = "20180101"<br>  }<br>}</pre>Here is some trailing text after code block,<br>followed by another line of text.<br><br>\| Name \| Description     \|<br>\|------\|-----------------\|<br>\| Foo  \| Foo description \|<br>\| Bar  \| Bar description \|
\ No newline at end of file
diff --git a/pkg/tmpl/testdata/table/complex.golden b/pkg/tmpl/testdata/table/complex.golden
new file mode 100644
index 0000000..c8b4b9f
--- /dev/null
+++ b/pkg/tmpl/testdata/table/complex.golden
@@ -0,0 +1,37 @@
+Usage:
+
+Example of 'foo_bar' module in `foo_bar.tf`.
+
+- list item 1
+- list item 2
+
+Even inline **formatting** in _here_ is possible.
+and some [link](https://domain.com/)
+
+* list item 3
+* list item 4
+
+```
+module "foo_bar" {
+  source = "github.com/foo/bar"
+
+  id   = "1234567890"
+  name = "baz"
+
+  zones = ["us-east-1", "us-west-1"]
+
+  tags = {
+    Name         = "baz"
+    Created-By   = "first.last@email.com"
+    Date-Created = "20180101"
+  }
+}
+```
+
+Here is some trailing text after code block,
+followed by another line of text.
+
+| Name | Description     |
+|------|-----------------|
+| Foo  | Foo description |
+| Bar  | Bar description |
\ No newline at end of file
diff --git a/pkg/tmpl/testdata/table/empty.expected b/pkg/tmpl/testdata/table/empty.expected
new file mode 100644
index 0000000..d8e0871
--- /dev/null
+++ b/pkg/tmpl/testdata/table/empty.expected
@@ -0,0 +1,1 @@
+n/a
\ No newline at end of file