Skip to content

Commit

Permalink
Improve documentation and update dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
jillesvangurp committed Sep 3, 2024
1 parent 3d4565a commit 24366c3
Show file tree
Hide file tree
Showing 10 changed files with 381 additions and 148 deletions.
241 changes: 173 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ This project implements [literate programming](https://en.wikipedia.org/wiki/Lit
for documenting projects. Having working code in your documentation, ensures that the examples you include are correct
and always up to date. And making it easy to include examples with your code lowers the barrier for writing good documentation.

This library is intended for anyone that publishes some kind of Kotlin library or code and wants to document their code using Markdown files that contain working examples.

## Get started

Add the dependency to your project and start writing some documentation. See below for some examples.
Expand All @@ -32,42 +34,112 @@ examples in the documentation working was a challenge. I'd refactor or rename so
all my examples. Staying on top of that is a lot of work.

Instead of just using one of the many documentation tools out there that can grab chunks of source code based on
some string marker, I instead came up with a better solution.
some string marker, I instead came up with a **better solution**: Kotlin4example implements a **Markdown Kotlin DSL** that includes a few nifty features, including an `example` function that takes an arbitrary block of Kotlin code and turns it into a markdown code block.

I wanted something that can leverage Kotlin's fantastic support for so-called internal DSLs. Like Ruby, you
can create domain specific languages using Kotlin's language features. In Kotlin, this works with regular functions
that take a block of code as a parameter. If such a parameter is the last one in a function, you can move the block outside
the parentheses. And if there are no other parameters those are optional. And then I realized that I could use
reflection to figure exactly from where the function call is made. This became the core
of what kotlin4example does. Any time you call example, it figures out from where in the code it is called and grabs the source
code in the block.
So, to write documentation, you simply use the DSL to write your documentation in Kotlin. You don't have to write all of it in Kotlin of course; it can include regular markdown files as well. But when writing examples, you just write them in Kotlin and the library turns them into markdown code blocks.

The library has a few other features, which are detailed in the examples below. But the simple idea is what
differentiates kotlin4example from other solutions. I'm not aware of any better or more convenient way to write
documentation for Kotlin libraries.
There is of course more to this library. For more on that, check out the examples below. Which are of course generated with this library.

## Getting Started

## Usage
After adding this library to your (test) dependencies, you can start adding code
to generate markdown.

### Example blocks
### Creating a SourceRepository

With Kotlin4Example you can mix examples and markdown easily.
An example is a code block
and it is executed by default. Because it is a code block,
you are forced to ensure
it is syntactically correct and compiles.
The first thing you need is a `SourceRepository` definition. This is needed to tell
kotlin4example about your repository.

By executing it, you can further guarantee it does what it
is supposed to and you can
intercept output and integrate that into your documentation.
Some of the functions in kotlin4example construct links to files in your github repository,
or lookup code from files in your source code.

For example:
```kotlin
val k4ERepo = SourceRepository(
// used to construct markdown links to files in your repository
repoUrl = "https://github.com/jillesvangurp/kotlin4example",
// default is main
branch = "master",
// this is the default
sourcePaths = setOf(
"src/main/kotlin",
"src/test/kotlin"
)
)
```

### Creating markdown

```kotlin
print("Hello World")
val myMarkdown = k4ERepo.md {
section("Introduction")
+"""
Hello world!
""".trimIndent()
}
println(myMarkdown)
```

This example prints **Hello World** when it executes.
This will generate some markdown that looks as follows.

```markdown
## Introduction

Hello world!


```

### Using your Markdown to create a page

Kotlin4example has a simple page abstraction that you
can use to organize your markdown content into pages and files

```kotlin
val page = Page(title = "Hello!", fileName = "hello.md")
// creates hello.md
page.write(myMarkdown)
```

### This README is generated

This README.md is of course created from kotlin code that
runs as part of the test suite. You can look at the kotlin
source code that generates this markdown [here](https://github.com/jillesvangurp/kotlin4example/blob/master/src/test/kotlin/com/jillesvangurp/kotlin4example/docs/readme.kt).

The code that writes the `README.md file` is as follows:

```kotlin
/**
* The readme is generated when the tests run.
*/
class DocGenTest {
@Test
fun `generate readme for this project`() {
val readmePage = Page(
title = "Kotlin4Example",
fileName = "README.md"
)
// readmeMarkdown is a lazy of the markdown content
readmePage.write(markdown = readmeMarkdown)
}
}
```

Here's a link to the source code on Github: [`DocGenTest`](https://github.com/jillesvangurp/kotlin4example/blob/master/src/test/kotlin/com/jillesvangurp/kotlin4example/DocGenTest.kt)

## Usage

### Example blocks

With Kotlin4Example you can mix examples and markdown easily.
An example is a Kotlin code block. Because it is a code block,
you are forced to ensure it is syntactically correct and that it compiles.

By executing the block (you can disable this), you can further guarantee it does what it
is supposed to and you can intercept output and integrate that into your
documentation as well

For example:

```kotlin
// out is an ExampleOutput instance
Expand All @@ -83,8 +155,16 @@ val out = example {
""".trimIndent()
```

The block you pass to example can be a suspending block. It uses `runBlocking` to run it. Earlier
versions of this library had a separate function for this; this is no longer needed.
The block you pass to example can be a suspending block; so you can create examples for
your co-routine libraries too. Kotlin4example uses `runBlocking` to run your examples.

When you include the above in your Markdown it will render as follows:

```kotlin
print("Hello World")
```

This example prints **Hello World** when it executes.

### Configuring examples

Expand All @@ -97,11 +177,13 @@ example(
runExample = false,
) {
// your code goes here
// but it won't run
}
```

The library imposes a line length of 80 characters on your examples. The
reason is that code blocks with horizontal scroll bars look ugly.
The library imposes a default line length of 80 characters on your examples. The
reason is that code blocks with long lines look ugly on web pages. E.g. Github will give
you a horizontal scrollbar.

You can of course turn this off or turn on the built in wrapping (wraps at the 80th character)

Expand All @@ -110,10 +192,13 @@ You can of course turn this off or turn on the built in wrapping (wraps at the 8
// making sure the example fits in a web page
// long lines tend to look ugly in documentation
example(
// use longer line length
// default is 80
lineLength = 120,
// wrap lines that are too long
// default is false
wrap = true,
// don't fail on lines that are too long
// default is false
allowLongLines = true,

Expand All @@ -124,37 +209,67 @@ example(

### Code snippets

While it is nice to have executable blocks,
While it is nice to have executable blocks as examples,
sometimes you just want to grab
code directly from a file. You can do that with snippets.
code directly from some Kotlin file. You can do that with snippets.

```kotlin
// the BEGIN_ and END_ are optional but I find it
// helps for readability.
// BEGIN_MY_CODE_SNIPPET
println("Example code that shows in a snippet")
// END_MY_CODE_SNIPPET
exampleFromSnippet("readme.kt", "MY_CODE_SNIPPET")
```

### Markdown
```kotlin
println("Example code that shows in a snippet")
```

The `BEGIN_` and `END_` prefix are optional but I find it helps readability.

You include the code in your markdown as follows:

```kotlin
exampleFromSnippet(
sourceFileName = "com/jillesvangurp/kotlin4example/docs/readme.kt",
snippetId = "MY_CODE_SNIPPET"
)
```

### Misc Markdown

```kotlin
section("Section") {
+"""
You can use string literals, templates ${1 + 1},
and [links](https://github.com/jillesvangurp/kotlin4example)
or other markdown formatting.
""".trimIndent()
subSection("Sub Section") {
+"""
You can use string literals, templates ${1 + 1},
and [links](https://github.com/jillesvangurp/kotlin4example)
or other markdown formatting.
""".trimIndent()
}
}
section("Links") {

// you can also just include markdown files
// useful if you have a lot of markdown
// content without code examples
includeMdFile("intro.md")

// link to things in your git repository
mdLink(DocGenTest::class)

// link to things in one of your source directories
// you can customize where it looks in SourceRepository
mdLinkToRepoResource(
title = "A file",
relativeUrl = "com/jillesvangurp/kotlin4example/Kotlin4Example.kt"
)

val anotherPage = Page("Page 2", "page2.md")
// link to another page in your manual
mdPageLink(anotherPage)

// and of course you can link to your self
mdLinkToSelf("This class")
}
// you can also just include markdown files
// useful if you have a lot of markdown
// content without code examples
includeMdFile("intro.md")
// link to things in your git repository
mdLink(DocGenTest::class)
mdLinkToRepoResource("build file", "build.gradle.kts")
mdLinkToSelf("This class")
```

### Source code blocks
Expand All @@ -170,27 +285,11 @@ mdCodeBlock(
)
```

### This README is generated
## Advanced topics

This README.md is of course created from kotlin code that
runs as part of the test suite. You can look at the kotlin
source code that generates this markdown [here](https://github.com/jillesvangurp/kotlin4example/blob/master/src/test/kotlin/com/jillesvangurp/kotlin4example/docs/readme.kt).
### Organizing pages

The code that writes the `README.md file` is as follows:

```kotlin
/**
* The readme is generated when the tests run.
*/
class DocGenTest {
@Test
fun `generate readme for this project`() {
val readmePage = Page("Kotlin4Example", fileName = "README.md")
// readmeMarkdown is a lazy of the markdown content
readmePage.write(markdown = readmeMarkdown)
}
}
```
A manual typically contains multiple pages. So, it helps to get organized a little.

### Context receivers

Expand Down Expand Up @@ -229,7 +328,13 @@ kotlin {

For more elaborate examples of using this library, checkout my
[kt-search](https://github.com/jillesvangurp/kt-search) project. That
project is where this project emerged from and all markdown in that project is generated by kotlin4example. Give it a
try on one of your own projects and let me know what you think.
project is where this project emerged from and all markdown in that project is generated by kotlin4example. Give it a try on one of your own projects and let me know what you think.

## Projects that use kotlin4example

- [kt-search](https://github.com/jillesvangurp/kt-search)
- [kotlin-opencage-client](https://github.com/jillesvangurp/kotlin-opencage-client)
- [json-dsl](https://github.com/jillesvangurp/json-dsl)

Create a pull request against [outro.md](https://github.com/jillesvangurp/kotlin4example/blob/master/src/test/kotlin/com/jillesvangurp/kotlin4example/docs/outro.md) if you want to add your project here.

Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
20 changes: 10 additions & 10 deletions gradlew.bat
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

Expand All @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ import kotlin.reflect.KClass
private val logger: KLogger = KotlinLogging.logger { }

fun mdLink(title: String, target: String) = "[$title]($target)"
@Deprecated("use mdPageLink", ReplaceWith("mdPageLink"))
fun mdLink(page: Page) = mdLink(page.title, page.fileName)

fun mdPageLink(page: Page) = mdLink(page.title, page.fileName)

fun md(sourceRepository: SourceRepository, block: Kotlin4Example.() -> Unit) =
lazyOf(Kotlin4Example.markdown(sourceRepository, block))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import org.junit.jupiter.api.Test
class DocGenTest {
@Test
fun `generate readme for this project`() {
val readmePage = Page("Kotlin4Example", fileName = "README.md")
val readmePage = Page(
title = "Kotlin4Example",
fileName = "README.md"
)
// readmeMarkdown is a lazy of the markdown content
readmePage.write(markdown = readmeMarkdown)
}
Expand Down
Loading

0 comments on commit 24366c3

Please sign in to comment.