├── .github └── workflows │ └── gradle.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts ├── src ├── main │ └── kotlin │ │ └── com │ │ └── jillesvangurp │ │ └── kotlin4example │ │ ├── BlockOutputCapture.kt │ │ ├── ExampleOutput.kt │ │ ├── Kotlin4Example.kt │ │ ├── Page.kt │ │ └── SourceRepository.kt └── test │ └── kotlin │ └── com │ └── jillesvangurp │ └── kotlin4example │ ├── DocGenTest.kt │ ├── KotlinForExampleTest.kt │ └── docs │ ├── intro.md │ ├── outro.md │ └── readme.kt └── versions.properties /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: CI-gradle-build 2 | 3 | on: [push, pull_request] 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout the code 9 | uses: actions/checkout@master 10 | - name: setup jdk 11 | uses: actions/setup-java@v3 12 | with: 13 | java-version: 17 14 | distribution: adopt 15 | java-package: jdk 16 | architecture: x64 17 | cache: gradle 18 | 19 | - name: gradle build 20 | uses: gradle/gradle-build-action@v2 21 | with: 22 | arguments: build 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | out 4 | build 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2020, Jilles van Gurp 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin4Example 2 | 3 | [![](https://jitpack.io/v/jillesvangurp/kotlin4example.svg)](https://jitpack.io/#jillesvangurp/kotlin4example) 4 | [![Actions Status](https://github.com/jillesvangurp/kotlin4example/workflows/CI-gradle-build/badge.svg)](https://github.com/jillesvangurp/kotlin4example/actions) 5 | 6 | This project implements [literate programming](https://en.wikipedia.org/wiki/Literate_programming) in Kotlin. Literate programming is useful 7 | for documenting projects. Having working code in your documentation, ensures that the examples you include are correct 8 | and always up to date. And making it easy to include examples with your code lowers the barrier for writing good documentation. 9 | 10 | 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. 11 | 12 | Write your documentation using a kotlin markdown DSL. Use simple `example { // code goes here }` blocks to provide examples. Kotlin4example will generate nice markdown with the code inside that block added as code blocks. See below for a detailed introduction. 13 | 14 | This README is of course generated with kotlin4example. 15 | 16 | ## Gradle 17 | 18 | Add the dependency to your project and start writing some documentation. See below for some examples. 19 | I tend to put my documentation code in my tests so running the tests produces the documentation as a side effect. 20 | 21 | ```kotlin 22 | implementation("com.github.jillesvangurp:kotlin4example:") 23 | ``` 24 | 25 | You will also need to add the Jitpack repository: 26 | 27 | ```kotlin 28 | repositories { 29 | mavenCentral() 30 | maven { url = uri("https://jitpack.io") } 31 | } 32 | ``` 33 | 34 | ## Getting Started 35 | 36 | After adding this library to your (test) dependencies, you can start adding code 37 | to generate markdown. 38 | 39 | ### Creating a SourceRepository 40 | 41 | The first thing you need is a `SourceRepository` definition. This is needed to tell 42 | kotlin4example about your repository, where to link, and where your code is. 43 | 44 | Some of the functions in kotlin4example construct links to files in your github repository, 45 | or lookup code from files in your source code. 46 | 47 | ```kotlin 48 | val k4ERepo = SourceRepository( 49 | // used to construct markdown links to files in your repository 50 | repoUrl = "https://github.com/jillesvangurp/kotlin4example", 51 | // default is main 52 | branch = "master", 53 | // this is the default 54 | sourcePaths = setOf( 55 | "src/main/kotlin", 56 | "src/test/kotlin" 57 | ) 58 | ) 59 | ``` 60 | 61 | ### Creating markdown 62 | 63 | ```kotlin 64 | val myMarkdown = k4ERepo.md { 65 | section("Introduction") 66 | +""" 67 | Hello world! 68 | """.trimIndent() 69 | } 70 | println(myMarkdown) 71 | ``` 72 | 73 | This will generate some markdown that looks as follows. 74 | 75 | ```markdown 76 | ## Introduction 77 | 78 | Hello world! 79 | 80 | 81 | ``` 82 | 83 | ### Using your Markdown to create a page 84 | 85 | Kotlin4example has a simple page abstraction that you 86 | can use to organize your markdown content into pages and files 87 | 88 | ```kotlin 89 | val page = Page(title = "Hello!", fileName = "hello.md") 90 | // creates hello.md 91 | page.write(myMarkdown) 92 | ``` 93 | 94 | ### This README is generated 95 | 96 | This README.md is of course created from kotlin code that 97 | runs as part of the test suite. You can look at the kotlin 98 | source code that generates this markdown [here](https://github.com/jillesvangurp/kotlin4example/blob/master/src/test/kotlin/com/jillesvangurp/kotlin4example/docs/readme.kt). 99 | 100 | The code that writes the `README.md file` is as follows: 101 | 102 | ```kotlin 103 | /** 104 | * The readme is generated when the tests run. 105 | */ 106 | class DocGenTest { 107 | @Test 108 | fun `generate readme for this project`() { 109 | val readmePage = Page( 110 | title = "Kotlin4Example", 111 | fileName = "README.md" 112 | ) 113 | // readmeMarkdown is a lazy of the markdown content 114 | readmePage.write(markdown = readmeMarkdown) 115 | } 116 | } 117 | ``` 118 | 119 | 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). 120 | 121 | The code that constructs the markdown is a bit longer, you can find it 122 | [here](https://github.com/jillesvangurp/kotlin4examplecom/jillesvangurp/kotlin4example/docs/readme.kt). 123 | 124 | ## Example blocks 125 | 126 | With Kotlin4Example you can mix examples and markdown easily. 127 | An example is a Kotlin code block. Because it is a code block, 128 | you are forced to ensure it is syntactically correct and that it compiles. 129 | 130 | By executing the block (you can disable this), you can further guarantee it does what it 131 | is supposed to and you can intercept output and integrate that into your 132 | documentation as well 133 | 134 | For example: 135 | 136 | ```kotlin 137 | // out is an ExampleOutput instance 138 | // with both stdout and the return 139 | // value as a Result. Any exceptions 140 | // are captured as well. 141 | val out = example { 142 | print("Hello World") 143 | } 144 | // this is how you can append arbitrary markdown 145 | +""" 146 | This example prints **${out.stdOut}** when it executes. 147 | """.trimIndent() 148 | ``` 149 | 150 | The block you pass to example can be a suspending block; so you can create examples for 151 | your co-routine libraries too. Kotlin4example uses `runBlocking` to run your examples. 152 | 153 | When you include the above in your Markdown it will render as follows: 154 | 155 | ```kotlin 156 | print("Hello World") 157 | ``` 158 | 159 | This example prints **Hello World** when it executes. 160 | 161 | ### Configuring examples 162 | 163 | Sometimes you just want to show but not run the code. You can control this with the 164 | `runExample` parameter. 165 | 166 | ```kotlin 167 | // 168 | example( 169 | runExample = false, 170 | ) { 171 | // your code goes here 172 | // but it won't run 173 | } 174 | ``` 175 | 176 | The library imposes a default line length of 80 characters on your examples. The 177 | reason is that code blocks with long lines look ugly on web pages. E.g. Github will give 178 | you a horizontal scrollbar. 179 | 180 | You can of course turn this off or turn on the built in wrapping (wraps at the 80th character) 181 | 182 | ```kotlin 183 | 184 | // making sure the example fits in a web page 185 | // long lines tend to look ugly in documentation 186 | example( 187 | // use longer line length 188 | // default is 80 189 | lineLength = 120, 190 | // wrap lines that are too long 191 | // default is false 192 | wrap = true, 193 | // don't fail on lines that are too long 194 | // default is false 195 | allowLongLines = true, 196 | 197 | ) { 198 | // your code goes here 199 | } 200 | ``` 201 | 202 | ### Code snippets 203 | 204 | While it is nice to have executable blocks as examples, 205 | sometimes you just want to grab 206 | code directly from some Kotlin file. You can do that with snippets. 207 | 208 | ```kotlin 209 | // BEGIN_MY_CODE_SNIPPET 210 | println("Example code that shows in a snippet") 211 | // END_MY_CODE_SNIPPET 212 | ``` 213 | 214 | ```kotlin 215 | println("Example code that shows in a snippet") 216 | ``` 217 | 218 | The `BEGIN_` and `END_` prefix are optional but it helps readability. 219 | 220 | You include the code in your markdown as follows: 221 | 222 | ```kotlin 223 | exampleFromSnippet( 224 | // relative path to your source file 225 | // setup your source modules in the repository 226 | sourceFileName = "com/jillesvangurp/kotlin4example/docs/readme.kt", 227 | snippetId = "MY_CODE_SNIPPET" 228 | ) 229 | ``` 230 | 231 | ### Misc Markdown 232 | 233 | #### Simple markdown 234 | 235 | This is how you can use the markdown dsl to generate markdown. 236 | 237 | ## Section 238 | 239 | ### Sub Section 240 | 241 | You can use string literals, templates 2, 242 | and [links](https://github.com/jillesvangurp/kotlin4example) 243 | or other **markdown** formatting. 244 | 245 | #### Sub sub section 246 | 247 | There's more 248 | 249 | - bullets 250 | - **bold** 251 | - *italic 252 | 253 | 1. one 254 | 2. two 255 | 3. three 256 | 257 | > The difference between code and poetry ... 258 | > 259 | > ... poetry doesn’t need to compile. 260 | 261 | ```kotlin 262 | section("Section") { 263 | subSection("Sub Section") { 264 | +""" 265 | You can use string literals, templates ${1 + 1}, 266 | and [links](https://github.com/jillesvangurp/kotlin4example) 267 | or other **markdown** formatting. 268 | """.trimIndent() 269 | 270 | 271 | subSubSection("Sub sub section") { 272 | +""" 273 | There's more 274 | """.trimIndent() 275 | 276 | unorderedList("bullets","**bold**","*italic") 277 | 278 | orderedList("one","two","three") 279 | 280 | blockquote(""" 281 | The difference between code and poetry ... 282 | 283 | ... poetry doesn’t need to compile. 284 | """.trimIndent() 285 | ) 286 | 287 | } 288 | } 289 | } 290 | ``` 291 | 292 | #### Links 293 | 294 | Linking to different things in your repository. 295 | 296 | ```kotlin 297 | 298 | // you can also just include markdown files 299 | // useful if you have a lot of markdown 300 | // content without code examples 301 | includeMdFile("intro.md") 302 | 303 | // link to things in your git repository 304 | mdLink(DocGenTest::class) 305 | 306 | // link to things in one of your source directories 307 | // you can customize where it looks in SourceRepository 308 | mdLinkToRepoResource( 309 | title = "A file", 310 | relativeUrl = "com/jillesvangurp/kotlin4example/Kotlin4Example.kt" 311 | ) 312 | 313 | val anotherPage = Page("Page 2", "page2.md") 314 | // link to another page in your manual 315 | mdPageLink(anotherPage) 316 | 317 | // and of course you can link to your self 318 | mdLinkToSelf("This class") 319 | ``` 320 | 321 | #### Tables 322 | 323 | Including tables is easy if you don't want to manually format them. 324 | 325 | | Function | Explanation | 326 | | -------------------- | ------------------------------------------------ | 327 | | mdLink | Add a link to a class or file in your repository | 328 | | mdLinkToRepoResource | Add a file in your repository | 329 | | includeMdFile | include a markdown file | 330 | | example | Example code block | 331 | 332 | ```kotlin 333 | table(listOf("Function","Explanation"),listOf( 334 | listOf("mdLink","Add a link to a class or file in your repository"), 335 | listOf("mdLinkToRepoResource","Add a file in your repository"), 336 | listOf("includeMdFile","include a markdown file"), 337 | listOf("example","Example code block"), 338 | )) 339 | ``` 340 | 341 | ### Source code blocks 342 | 343 | You can add your own source code blocks as well. 344 | 345 | ```kotlin 346 | mdCodeBlock( 347 | code = """ 348 | Useful if you have some **non kotlin code** that you want to show 349 | """.trimIndent(), 350 | type = "markdown" 351 | ) 352 | ``` 353 | 354 | For more elaborate examples of using this library, checkout my 355 | [kt-search](https://github.com/jillesvangurp/kt-search) project. That 356 | 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. 357 | 358 | ## Why another documentation tool? 359 | 360 | When I started writing documentation for my [Kotlin Client for Elasticsearch](https://githubcom/jillesvangurp/es-kotlin-wrapper-client), I quickly discovered that keeping the 361 | examples in the documentation working was a challenge. I'd refactor or rename something which then would invalidate 362 | all my examples. Staying on top of that is a lot of work. 363 | 364 | Instead of just using one of the many documentation tools out there that can grab chunks of source code based on 365 | 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. 366 | 367 | 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. 368 | 369 | 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. 370 | 371 | ## Projects that use kotlin4example 372 | 373 | - [kt-search](https://github.com/jillesvangurp/kt-search) 374 | - [kotlin-opencage-client](https://github.com/jillesvangurp/kotlin-opencage-client) 375 | - [json-dsl](https://github.com/jillesvangurp/json-dsl) 376 | 377 | 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. 378 | 379 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("GradlePackageVersionRange") // bs warning because we use refreshVersions 2 | 3 | plugins { 4 | kotlin("jvm") 5 | `maven-publish` 6 | id("org.jetbrains.dokka") 7 | } 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | implementation(kotlin("stdlib-jdk8")) 15 | api("io.github.microutils:kotlin-logging:_") 16 | implementation(KotlinX.Coroutines.core) 17 | 18 | testImplementation(Testing.junit.jupiter.api) 19 | testRuntimeOnly(Testing.junit.jupiter.engine) 20 | testImplementation("org.junit.platform:junit-platform-launcher:_") 21 | testImplementation(Testing.kotest.assertions.core) 22 | 23 | // setup logging 24 | testImplementation("org.slf4j:slf4j-api:_") 25 | testImplementation("org.slf4j:jcl-over-slf4j:_") 26 | testImplementation("org.slf4j:log4j-over-slf4j:_") 27 | testImplementation("org.slf4j:jul-to-slf4j:_") 28 | testImplementation("org.apache.logging.log4j:log4j-to-slf4j:_") // es seems to insist on log4j2 29 | testImplementation("ch.qos.logback:logback-classic:_") 30 | 31 | } 32 | 33 | tasks.withType { 34 | useJUnitPlatform() 35 | testLogging.exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL 36 | testLogging.showStandardStreams = true 37 | testLogging.showExceptions = true 38 | testLogging.showStackTraces = true 39 | testLogging.events = setOf( 40 | org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED, 41 | org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED, 42 | org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED, 43 | org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR, 44 | org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_OUT 45 | ) 46 | } 47 | 48 | val artifactName = "kotlin4example" 49 | val artifactGroup = "com.github.jillesvangurp" 50 | 51 | 52 | val dokkaOutputDir = "${layout.buildDirectory.get()}/dokka" 53 | 54 | tasks { 55 | dokkaHtml { 56 | outputDirectory.set(file(dokkaOutputDir)) 57 | } 58 | } 59 | 60 | val javadocJar by tasks.registering(Jar::class) { 61 | dependsOn(tasks.dokkaHtml) 62 | archiveClassifier.set("javadoc") 63 | from(dokkaOutputDir) 64 | } 65 | 66 | publishing { 67 | publications { 68 | create("lib") { 69 | groupId = artifactGroup 70 | artifactId = artifactName 71 | from(components["java"]) 72 | artifact(javadocJar.get()) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jillesvangurp/kotlin4example/11a8a0b14e5fc39d0d3567f1df1e491230bfbdd1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "kotlin4example" 2 | 3 | pluginManagement { 4 | repositories { 5 | mavenCentral() 6 | gradlePluginPortal() 7 | } 8 | } 9 | 10 | plugins { 11 | id("de.fayard.refreshVersions") version "0.60.5" 12 | } 13 | 14 | refreshVersions { 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/jillesvangurp/kotlin4example/BlockOutputCapture.kt: -------------------------------------------------------------------------------- 1 | package com.jillesvangurp.kotlin4example 2 | 3 | import java.io.ByteArrayOutputStream 4 | import java.io.PrintWriter 5 | 6 | /** 7 | * Simple facade that captures calls to print and println and collects 8 | * what would have been printed in a buffer. 9 | */ 10 | class BlockOutputCapture { 11 | private val byteArrayOutputStream = ByteArrayOutputStream() 12 | private val printWriter = PrintWriter(byteArrayOutputStream) 13 | 14 | fun print(message: Any?) { 15 | printWriter.print(message) 16 | } 17 | 18 | fun println(message: Any?) { 19 | printWriter.println(message) 20 | } 21 | 22 | fun output(): String { 23 | printWriter.flush() 24 | return byteArrayOutputStream.toString() 25 | } 26 | 27 | fun reset() { 28 | printWriter.flush() 29 | byteArrayOutputStream.reset() 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/jillesvangurp/kotlin4example/ExampleOutput.kt: -------------------------------------------------------------------------------- 1 | package com.jillesvangurp.kotlin4example 2 | 3 | /** 4 | * When you use [Kotlin4Example.example] it uses this as the return value. 5 | */ 6 | data class ExampleOutput( 7 | val result: Result, 8 | val stdOut: String, 9 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/jillesvangurp/kotlin4example/Kotlin4Example.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.jillesvangurp.kotlin4example 4 | 5 | import kotlinx.coroutines.runBlocking 6 | import mu.KLogger 7 | import mu.KotlinLogging 8 | import java.io.File 9 | import kotlin.reflect.KClass 10 | 11 | private val logger: KLogger = KotlinLogging.logger { } 12 | 13 | fun mdLink(title: String, target: String) = "[$title]($target)" 14 | @Deprecated("use mdPageLink", ReplaceWith("mdPageLink")) 15 | fun mdLink(page: Page) = mdLink(page.title, page.fileName) 16 | 17 | fun mdPageLink(page: Page) = mdLink(page.title, page.fileName) 18 | 19 | fun md(sourceRepository: SourceRepository, block: Kotlin4Example.() -> Unit) = 20 | lazyOf(Kotlin4Example.markdown(sourceRepository, block)) 21 | 22 | @DslMarker 23 | annotation class Kotlin4ExampleDSL 24 | 25 | /** 26 | * A Kotlin DSL (Domain Specific Language) that you can use to generate markdown documentation 27 | * for your Kotlin code. 28 | * 29 | * The [sourceRepository] is used to create markdown links to files in your github repository. 30 | */ 31 | @Suppress("MemberVisibilityCanBePrivate") 32 | @Kotlin4ExampleDSL 33 | class Kotlin4Example( 34 | private val sourceRepository: SourceRepository 35 | ) { 36 | private val buf = StringBuilder() 37 | 38 | private val patternForBlock = "(suspendingBlock|block|example|suspendingExample).*?\\{+".toRegex(RegexOption.MULTILINE) 39 | 40 | /** 41 | * Append some arbitrary markdown. Tip, you can use raw strings and string templates """${1+1}""" 42 | */ 43 | operator fun String.unaryPlus() { 44 | buf.appendLine(this.trimIndent().trimMargin()) 45 | buf.appendLine() 46 | } 47 | 48 | /** 49 | * Create a section (## [title]) and use the [block] to specify what should be in the section. 50 | */ 51 | fun section(title: String, block: (Kotlin4Example.() -> Unit)? = null) { 52 | buf.appendLine("## $title") 53 | buf.appendLine() 54 | block?.invoke(this) 55 | } 56 | 57 | /** 58 | * Create a sub section (### [title]) and use the [block] to specify what should be in the section. 59 | */ 60 | fun subSection(title: String, block: (Kotlin4Example.() -> Unit)? = null) { 61 | buf.appendLine("### $title") 62 | buf.appendLine() 63 | block?.invoke(this) 64 | } 65 | 66 | /** 67 | * Create a sub section (### [title]) and use the [block] to specify what should be in the section. 68 | */ 69 | fun subSubSection(title: String, block: (Kotlin4Example.() -> Unit)? = null) { 70 | buf.appendLine("#### $title") 71 | buf.appendLine() 72 | block?.invoke(this) 73 | } 74 | 75 | /** 76 | * Create a markdown code block for some [code] of a particular [type]. 77 | * 78 | * Use [allowLongLines] turn off the check for lines longer than [lineLength] 79 | * 80 | * Use [wrap] to wrap your code. Note, all it does is add a new line at the 80th character. 81 | * 82 | * Use [reIndent] to change the indentation of the code to [reIndentSize]. Shorter indentation helps 83 | * keeping the lines short. Defaults to true 84 | */ 85 | fun mdCodeBlock( 86 | code: String, 87 | type: String, 88 | allowLongLines: Boolean = false, 89 | wrap: Boolean = false, 90 | lineLength: Int = 80, 91 | reIndent: Boolean = true, 92 | reIndentSize: Int = 2 93 | ) { 94 | // reindenting is useful when including source snippets that are indented with 4 or 8 spaces 95 | var c = if(reIndent) code.reIndent(reIndentSize) else code 96 | if (wrap) { 97 | var l = 1 98 | c = c.lines().flatMap { line -> 99 | if (line.length <= lineLength) { 100 | listOf(line) 101 | } else { 102 | logger.warn { "wrapping line longer than 80 characters at line $l:\n$line" } 103 | line.chunked(lineLength) 104 | }.also { 105 | l++ 106 | } 107 | }.joinToString("\n") 108 | } 109 | if (!allowLongLines) { 110 | var l = 1 111 | var error = 0 112 | c.lines().forEach { 113 | if (it.length > lineLength) { 114 | logger.error { "code block contains lines longer than 80 characters at line $l:\n$it" } 115 | error++ 116 | } 117 | l++ 118 | } 119 | if (error > 0) { 120 | throw IllegalArgumentException("code block exceeds line length of $lineLength") 121 | } 122 | } 123 | 124 | buf.appendLine("```$type\n$c\n```\n") 125 | } 126 | 127 | /** 128 | * Include the content of markdown file. The name the relative path to the directory your documentation is in. 129 | */ 130 | fun includeMdFile(name: String) { 131 | val dir = sourceDirOfCaller() 132 | val file = "$dir${File.separatorChar}$name" 133 | val markDown = findContentInSourceFiles(file)?.joinToString("\n") ?: error("no such file $file") 134 | buf.append(markDown) 135 | buf.appendLine() 136 | buf.appendLine() 137 | } 138 | 139 | /** 140 | * Create a link to the source code of file that contains the [clazz] in your [SourceRepository]. 141 | * 142 | * The [title] defaults to the class name. 143 | */ 144 | fun mdLink(clazz: KClass<*>, title: String="`${clazz.simpleName!!}`"): String { 145 | return mdLink( 146 | title = title, 147 | target = sourceRepository.urlForFile(sourcePathForClass(clazz)) 148 | ) 149 | } 150 | 151 | /** 152 | * Create a link to a file in your [SourceRepository] with a [title]. 153 | * 154 | * The [relativeUrl] should be relative to your source repository root. 155 | */ 156 | fun mdLinkToRepoResource(title: String, relativeUrl: String) = 157 | mdLink(title, sourceRepository.repoUrl + relativeUrl) 158 | 159 | /** 160 | * Creates a link to the source file from which you are calling this. 161 | */ 162 | fun mdLinkToSelf(title: String = "Link to this source file"): String { 163 | val fn = this.sourceFileOfCaller() 164 | val path = sourceRepository.sourcePaths.map { File(it, fn) }.firstOrNull { it.exists() }?.path 165 | ?: throw IllegalStateException("file not found") 166 | return mdLink(title, "${sourceRepository.repoUrl}/blob/${sourceRepository.branch}/${path}") 167 | } 168 | 169 | @Deprecated("Use exampleFromSnippet", ReplaceWith("exampleFromSnippet(clazz, snippetId, allowLongLines, wrap, lineLength, type)")) 170 | fun snippetBlockFromClass( 171 | clazz: KClass<*>, 172 | snippetId: String, 173 | allowLongLines: Boolean = false, 174 | wrap: Boolean = false, 175 | lineLength: Int = 80, 176 | type: String = "kotlin" 177 | ) { 178 | exampleFromSnippet(clazz, snippetId, allowLongLines, wrap, lineLength, type) 179 | } 180 | 181 | /** 182 | * Creates a code block from code in the specified class. 183 | * 184 | * [snippetId] should be included in the source code in comments at the beginning and end of your example. 185 | * 186 | * Use [allowLongLines], [wrap], and [lineLength] to control the behavior for long lines (similar to [example]. 187 | * 188 | * The [type] defaults to kotlin. 189 | */ 190 | fun exampleFromSnippet( 191 | clazz: KClass<*>, 192 | snippetId: String, 193 | allowLongLines: Boolean = false, 194 | wrap: Boolean = false, 195 | lineLength: Int = 80, 196 | type: String = "kotlin" 197 | ) { 198 | val fileName = sourcePathForClass(clazz) 199 | exampleFromSnippet( 200 | sourceFileName = fileName, 201 | snippetId = snippetId, 202 | allowLongLines = allowLongLines, 203 | wrap = wrap, 204 | lineLength = lineLength, 205 | type = type 206 | ) 207 | } 208 | 209 | @Deprecated("Use exampleFromSnippet", ReplaceWith("exampleFromSnippet(fileName,snippetId, allowLongLines, wrap, lineLength, type)")) 210 | fun snippetFromSourceFile( 211 | fileName: String, 212 | snippetId: String, 213 | allowLongLines: Boolean = false, 214 | wrap: Boolean = false, 215 | lineLength: Int = 80, 216 | type: String = "kotlin" 217 | ) { 218 | exampleFromSnippet(fileName,snippetId, allowLongLines, wrap, lineLength, type) 219 | } 220 | 221 | /** 222 | * Creates a code block for a [sourceFile] in your [SourceRepository] 223 | * 224 | * [snippetId] should be included in the source code in comments at the beginning and end of your example. 225 | * 226 | * Use [allowLongLines], [wrap], and [lineLength] to control the behavior for long lines (similar to [example]. 227 | * 228 | * The [type] defaults to kotlin. 229 | */ 230 | fun exampleFromSnippet( 231 | sourceFileName: String, 232 | snippetId: String, 233 | allowLongLines: Boolean = false, 234 | wrap: Boolean = false, 235 | lineLength: Int = 80, 236 | type: String = "kotlin", 237 | reIndent: Boolean = true, 238 | reIndentSize: Int = 2 239 | ) { 240 | val snippetLines = mutableListOf() 241 | 242 | val lines = File(sourceRepository.sourcePaths.map { File(it, sourceFileName) }.firstOrNull { it.exists() }?.path 243 | ?: sourceFileName 244 | ).readLines() 245 | var inSnippet = false 246 | for (line in lines) { 247 | if (inSnippet && line.contains(snippetId)) { 248 | break // break out of the loop 249 | } 250 | if (inSnippet) { 251 | snippetLines.add(line) 252 | } 253 | 254 | if (!inSnippet && line.contains(snippetId)) { 255 | inSnippet = true 256 | } 257 | } 258 | if (snippetLines.size == 0) { 259 | throw IllegalArgumentException("Snippet $snippetId not found in $sourceFileName") 260 | } 261 | mdCodeBlock( 262 | snippetLines.joinToString("\n").trimIndent(), 263 | type = type, 264 | allowLongLines = allowLongLines, 265 | wrap = wrap, 266 | lineLength = lineLength, 267 | reIndent=reIndent, 268 | reIndentSize=reIndentSize, 269 | ) 270 | } 271 | 272 | @Deprecated("Use example, which now takes a suspending block by default", ReplaceWith("example(runExample, type, allowLongLines, wrap, lineLength, block)")) 273 | fun suspendingExample( 274 | runExample: Boolean = true, 275 | type: String = "kotlin", 276 | allowLongLines: Boolean = false, 277 | wrap: Boolean = false, 278 | lineLength: Int = 80, 279 | block: suspend BlockOutputCapture.() -> T 280 | ): ExampleOutput { 281 | return example( 282 | runExample = runExample, 283 | type = type, 284 | allowLongLines = allowLongLines, 285 | wrap = wrap, 286 | lineLength = lineLength, 287 | block = block 288 | ) 289 | } 290 | 291 | /** 292 | * Create a markdown code block for the code in the [block]. 293 | * 294 | * The [block] takes a [BlockOutputCapture] as the parameter. You can use this to make 295 | * calls to print and println. The output is returned as part of [ExampleOutput] along 296 | * with the return value of your block. 297 | * 298 | * Use [runExample] to turn off execution of the block. This is useful if you want to show 299 | * code with undesirable side effects or that is slow to run. Defaults to true so it will 300 | * run your code unless change this. 301 | * 302 | * The [type] defaults to kotlin. 303 | * 304 | * Use [allowLongLines] turn off the check for lines longer than [lineLength] 305 | * 306 | * Use [wrap] to wrap your code. Note, all it does is add a new line at the 80th character 307 | */ 308 | fun example( 309 | runExample: Boolean = true, 310 | type: String = "kotlin", 311 | allowLongLines: Boolean = false, 312 | wrap: Boolean = false, 313 | lineLength: Int = 80, 314 | reIndent: Boolean = true, 315 | reIndentSize: Int = 2, 316 | block: suspend BlockOutputCapture.() -> T 317 | ): ExampleOutput { 318 | val state = BlockOutputCapture() 319 | val returnVal = try { 320 | if (runExample) { 321 | runBlocking { 322 | Result.success(block.invoke(state)) 323 | } 324 | } else { 325 | Result.success(null) 326 | } 327 | } catch (e: Exception) { 328 | Result.failure(e) 329 | } 330 | val callerSourceBlock = 331 | getCallerSourceBlock() ?: throw IllegalStateException("source block could not be extracted") 332 | mdCodeBlock( 333 | code = callerSourceBlock, 334 | allowLongLines = allowLongLines, 335 | wrap = wrap, 336 | lineLength = lineLength, 337 | type = type, 338 | reIndent = reIndent, 339 | reIndentSize = reIndentSize, 340 | ) 341 | 342 | return ExampleOutput(returnVal, state.output().trimIndent()) 343 | } 344 | 345 | /** 346 | * Add the output of your example to the markdown. 347 | * 348 | * Note. a future version of this library will use context receivers for this. 349 | * If you can opt in to this in your code base, it's easy to add this yourself: 350 | * 351 | * ``` 352 | * context(Kotlin4Example) 353 | * fun ExampleOutput<*>.printStdOut() { 354 | * +""" 355 | * This prints: 356 | * """.trimIndent() 357 | * 358 | * mdCodeBlock(stdOut, type = "text", wrap = true) 359 | * } 360 | * ``` 361 | */ 362 | fun renderExampleOutput( 363 | exampleOutput: ExampleOutput, 364 | stdOutOnly: Boolean = true, 365 | allowLongLines: Boolean = false, 366 | wrap: Boolean = false, 367 | lineLength: Int = 80, 368 | reIndent: Boolean = true, 369 | reIndentSize: Int = 2 370 | ) { 371 | if(!stdOutOnly) { 372 | exampleOutput.result.let { r -> 373 | r.getOrNull()?.let { returnValue -> 374 | if(returnValue !is Unit) { 375 | mdCodeBlock( 376 | returnValue.toString(), 377 | allowLongLines = allowLongLines, 378 | wrap = wrap, 379 | lineLength = lineLength, 380 | type = "text", 381 | reIndent = reIndent, 382 | reIndentSize = reIndentSize, 383 | ) 384 | } 385 | } 386 | } 387 | } 388 | exampleOutput.stdOut.takeIf { it.isNotBlank() }?.let { 389 | mdCodeBlock( 390 | it, 391 | allowLongLines = allowLongLines, 392 | wrap = wrap, 393 | lineLength = lineLength, 394 | type = "text", 395 | reIndent = reIndent, 396 | reIndentSize = reIndentSize, 397 | ) 398 | } 399 | } 400 | 401 | @Deprecated("Use the new example function", ReplaceWith("""renderExampleOutput(example(runBlock,type,allowLongLines,wrap,lineLength,block),!captureBlockReturnValue,allowLongLines,wrap,lineLength)""")) 402 | fun block( 403 | runBlock: Boolean = true, 404 | type: String = "kotlin", 405 | allowLongLines: Boolean = false, 406 | wrap: Boolean = false, 407 | printStdOut: Boolean = true, 408 | captureBlockReturnValue: Boolean = true, 409 | stdOutPrefix: String = "Captured Output:", 410 | returnValuePrefix: String = "->", 411 | lineLength: Int = 80, 412 | block: BlockOutputCapture.() -> T 413 | ) { 414 | val state = BlockOutputCapture() 415 | @Suppress("DEPRECATION") 416 | block( 417 | allowLongLines = allowLongLines, 418 | type = type, 419 | wrap = wrap, 420 | lineLength = lineLength, 421 | runBlock = runBlock, 422 | block = block, 423 | blockCapture = state, 424 | returnValuePrefix = returnValuePrefix, 425 | printStdOut = printStdOut, 426 | captureBlockReturnValue = captureBlockReturnValue, 427 | stdOutPrefix = stdOutPrefix 428 | ) 429 | } 430 | 431 | @Deprecated("Use the new example function", ReplaceWith("""renderExampleOutput(example(runBlock,type,allowLongLines,wrap,lineLength,block),!captureBlockReturnValue,allowLongLines,wrap,lineLength)""")) 432 | fun block( 433 | runBlock: Boolean = true, 434 | type: String = "kotlin", 435 | allowLongLines: Boolean = false, 436 | wrap: Boolean = false, 437 | printStdOut: Boolean = true, 438 | stdOutPrefix: String = "Captured Output:", 439 | returnValuePrefix: String = "->", 440 | lineLength: Int = 80, 441 | captureBlockReturnValue: Boolean = true, 442 | blockCapture: BlockOutputCapture, 443 | block: BlockOutputCapture.() -> T 444 | ) { 445 | val callerSourceBlock = 446 | getCallerSourceBlock() ?: throw IllegalStateException("source block could not be extracted") 447 | mdCodeBlock( 448 | code = callerSourceBlock, 449 | allowLongLines = allowLongLines, 450 | type = type, 451 | wrap = wrap, 452 | lineLength = lineLength 453 | ) 454 | 455 | if (runBlock) { 456 | val response = block.invoke(blockCapture) 457 | if (response !is Unit) { 458 | if (captureBlockReturnValue) { 459 | buf.appendLine("$returnValuePrefix\n") 460 | mdCodeBlock(response.toString(), type = "") 461 | } 462 | } 463 | } 464 | 465 | // if you have runBlock == false, no output can be produced 466 | if (printStdOut && runBlock) { 467 | val output = blockCapture.output() 468 | blockCapture.reset() 469 | if (output.isNotBlank()) { 470 | buf.appendLine("$stdOutPrefix\n") 471 | mdCodeBlock( 472 | code = output, 473 | allowLongLines = allowLongLines, 474 | wrap = wrap, 475 | lineLength = lineLength, 476 | type = "" 477 | ) 478 | } 479 | } 480 | } 481 | 482 | @Deprecated("Use the new suspendingExample function", ReplaceWith("""renderExampleOutput(suspendingExample(runBlock,type,allowLongLines,wrap,lineLength,block),!captureBlockReturnValue,allowLongLines,wrap,lineLength)""")) 483 | fun suspendingBlock( 484 | runBlock: Boolean = true, 485 | type: String = "kotlin", 486 | allowLongLines: Boolean = false, 487 | wrap: Boolean = false, 488 | printStdOut: Boolean = true, 489 | stdOutPrefix: String = "Captured Output:", 490 | returnValuePrefix: String = "->", 491 | lineLength: Int = 80, 492 | captureBlockReturnValue: Boolean = true, 493 | block: suspend BlockOutputCapture.() -> T 494 | ) { 495 | val state = BlockOutputCapture() 496 | @Suppress("DEPRECATION") 497 | suspendingBlock( 498 | allowLongLines = allowLongLines, 499 | type = type, 500 | wrap = wrap, 501 | lineLength = lineLength, 502 | runBlock = runBlock, 503 | block = block, 504 | blockCapture = state, 505 | returnValuePrefix = returnValuePrefix, 506 | printStdOut = printStdOut, 507 | captureBlockReturnValue = captureBlockReturnValue, 508 | stdOutPrefix = stdOutPrefix 509 | ) 510 | } 511 | 512 | @Deprecated("Use the new suspendingExample function", ReplaceWith("""renderExampleOutput(suspendingExample(runBlock,type,allowLongLines,wrap,lineLength,block),!captureBlockReturnValue,allowLongLines,wrap,lineLength)""")) 513 | fun suspendingBlock( 514 | runBlock: Boolean = true, 515 | type: String = "kotlin", 516 | allowLongLines: Boolean = false, 517 | wrap: Boolean = false, 518 | printStdOut: Boolean = true, 519 | stdOutPrefix: String = "Captured Output:", 520 | returnValuePrefix: String = "->", 521 | lineLength: Int = 80, 522 | blockCapture: BlockOutputCapture, 523 | captureBlockReturnValue: Boolean = true, 524 | block: suspend BlockOutputCapture.() -> T 525 | ) { 526 | val callerSourceBlock = 527 | getCallerSourceBlock() ?: throw IllegalStateException("source block could not be extracted") 528 | mdCodeBlock( 529 | code = callerSourceBlock, 530 | allowLongLines = allowLongLines, 531 | type = type, 532 | wrap = wrap, 533 | lineLength = lineLength 534 | ) 535 | 536 | if (runBlock) { 537 | val response = runBlocking { 538 | block.invoke(blockCapture) 539 | } 540 | 541 | if (response !is Unit && captureBlockReturnValue) { 542 | buf.appendLine("$returnValuePrefix\n") 543 | mdCodeBlock(response.toString(), type = "") 544 | } 545 | } 546 | 547 | // if you have runBlock == false, no output can be produced 548 | if (printStdOut && runBlock) { 549 | val output = blockCapture.output() 550 | blockCapture.reset() 551 | if (output.isNotBlank()) { 552 | buf.appendLine("$stdOutPrefix\n") 553 | mdCodeBlock( 554 | code = output, 555 | allowLongLines = allowLongLines, 556 | wrap = wrap, 557 | lineLength = lineLength, 558 | type = "" 559 | ) 560 | } 561 | } 562 | } 563 | 564 | private fun findContentInSourceFiles(sourceFile: String) = 565 | sourceRepository.sourcePaths.map { File(it, sourceFile).absolutePath } 566 | // the calculated fileName for the .class file does not match the source file for inner classes 567 | // so try to fix it by stripping the Kt postfix 568 | .flatMap { listOf(it, it.replace("Kt.kt", ".kt")) } 569 | .map { File(it) } 570 | .firstOrNull { it.exists() }?.readLines() 571 | 572 | 573 | private fun getCallerSourceBlock(): String? { 574 | val sourceFile = sourceFileOfCaller() 575 | 576 | val ste = getCallerStackTraceElement() 577 | val line = ste.lineNumber 578 | 579 | val lines = findContentInSourceFiles(sourceFile) 580 | 581 | return if (lines != null && line > 0) { 582 | // Off by one error. Line numbers start at 1; list numbers start at 0 583 | val source = lines.subList(line - 1, lines.size).joinToString("\n") 584 | 585 | val allBlocks = patternForBlock.findAll(source) 586 | // FIXME this sometimes fails in a non reproducible way? 587 | val match = allBlocks.first() 588 | val start = match.range.last 589 | var openCount = 1 590 | var index = start + 1 591 | while (openCount > 0 && index < source.length) { 592 | when (source[index++]) { 593 | '{' -> openCount++ 594 | '}' -> openCount-- 595 | } 596 | } 597 | if (index > start + 1 && index < source.length) { 598 | source.substring(start + 1, index - 1).trimIndent() 599 | } else { 600 | logger.warn { "no block found $start $index ${source.length} $openCount" } 601 | null 602 | } 603 | } else { 604 | logger.warn("no suitable file found for ${ste.fileName} ${ste.lineNumber}") 605 | null 606 | } 607 | } 608 | 609 | private fun sourceFileOfCaller(): String { 610 | val ste = getCallerStackTraceElement() 611 | val pathElements = ste.className.split('.') 612 | val relativeDir = pathElements.subList(0, pathElements.size - 1).joinToString("${File.separatorChar}") 613 | return "$relativeDir${File.separatorChar}${ste.fileName}" 614 | } 615 | 616 | private fun sourceDirOfCaller(): String { 617 | val ste = getCallerStackTraceElement() 618 | val pathElements = ste.className.split('.') 619 | return pathElements.subList(0, pathElements.size - 1).joinToString("${File.separatorChar}") 620 | } 621 | 622 | /** 623 | * Figure out the source file name for the class, so we can grab code from it. Looks in the source paths. 624 | */ 625 | private fun sourcePathForClass(clazz: KClass<*>) = 626 | sourceRepository.sourcePaths.map { File(it, fileName(clazz)) }.firstOrNull { it.exists() }?.path 627 | ?: throw IllegalArgumentException("source not found for ${clazz.qualifiedName}") 628 | 629 | /** 630 | * Figure out the (likely) file name for the class or inner class. 631 | */ 632 | private fun fileName(clazz: KClass<*>) = 633 | clazz.qualifiedName!!.replace("\\$.*?$".toRegex(), "").replace('.', File.separatorChar) + ".kt" 634 | 635 | private fun getCallerStackTraceElement(): StackTraceElement { 636 | return Thread.currentThread() 637 | .stackTrace.first { 638 | !it.className.startsWith("java") && 639 | !it.className.startsWith("jdk.internal") && 640 | it.className != javaClass.name && 641 | it.className != "java.lang.Thread" && 642 | it.className != "io.inbot.eskotlinwrapper.manual.KotlinForExample" && 643 | it.className != "io.inbot.eskotlinwrapper.manual.KotlinForExample\$Companion" // edge case 644 | } 645 | } 646 | 647 | fun table(headers: List, rows: List>) { 648 | require(rows.all { it.size == headers.size }) { 649 | "All rows must have the same number of columns as headers" 650 | } 651 | 652 | val allRows = listOf(headers) + rows 653 | val columnWidths = headers.indices.map { col -> 654 | allRows.maxOf { it[col].length } 655 | } 656 | 657 | fun formatRow(row: List): String = 658 | row.mapIndexed { i, cell -> cell.padEnd(columnWidths[i]) } 659 | .joinToString(" | ", prefix = "| ", postfix = " |") 660 | 661 | val separator = columnWidths.joinToString(" | ", prefix = "| ", postfix = " |") { 662 | "-".repeat(it.coerceAtLeast(3)) // ensure minimum of 3 dashes for markdown 663 | } 664 | 665 | buf.appendLine(formatRow(headers)) 666 | buf.appendLine(separator) 667 | rows.forEach { buf.appendLine(formatRow(it)) } 668 | buf.appendLine() 669 | } 670 | 671 | fun blockquote(text: String) { 672 | buf.appendLine("> ${text.trimIndent().replace("\n", "\n> ")}") 673 | buf.appendLine() 674 | } 675 | 676 | 677 | fun unorderedList(vararg items: String) { 678 | unorderedList(items.toList()) 679 | } 680 | 681 | 682 | fun unorderedList(items: List) { 683 | items.forEach { buf.appendLine("- ${it.trim()}") } 684 | buf.appendLine() 685 | } 686 | 687 | fun orderedList(vararg items: String) { 688 | orderedList(items.toList()) 689 | } 690 | 691 | fun orderedList(items: List) { 692 | items.forEachIndexed { i, item -> buf.appendLine("${i + 1}. ${item.trim()}") } 693 | buf.appendLine() 694 | } 695 | 696 | companion object { 697 | fun markdown( 698 | sourceRepository: SourceRepository, 699 | block: Kotlin4Example.() -> Unit 700 | ): String { 701 | val example = Kotlin4Example(sourceRepository) 702 | example.apply(block) 703 | return example.buf.toString() 704 | } 705 | } 706 | } 707 | 708 | fun String.reIndent(indent: Int=2): String { 709 | val spaceFinder = "^(\\s+)".toRegex() 710 | return this.lines().firstOrNull { 711 | val whiteSpace = spaceFinder.find(it)?.value 712 | whiteSpace?.let { 713 | whiteSpace.length >= indent 714 | } == true 715 | }?.let { 716 | val whiteSpace = spaceFinder.find(it)!!.groups[1]!!.value 717 | if(whiteSpace.length != indent ) { 718 | val originalIndent = " ".repeat(whiteSpace.length) 719 | val newIndent = " ".repeat(indent) 720 | this.replace(originalIndent, newIndent) 721 | } else { 722 | this 723 | } 724 | }?:this 725 | } 726 | -------------------------------------------------------------------------------- /src/main/kotlin/com/jillesvangurp/kotlin4example/Page.kt: -------------------------------------------------------------------------------- 1 | package com.jillesvangurp.kotlin4example 2 | 3 | import java.io.File 4 | 5 | /** 6 | * Simple representation of a documentation page. 7 | */ 8 | data class Page( 9 | val title: String, 10 | val outputDir: String = ".", 11 | val fileName: String = "${title.lowercase().replace("""\s+""", "-")}.md" 12 | ) { 13 | val file = File(outputDir, fileName) 14 | 15 | fun write(markdown: String) { 16 | file.writeText(markdownContent(markdown)) 17 | } 18 | 19 | fun markdownContent(markdown: String) = """ 20 | # $title 21 | 22 | 23 | """.trimIndent() + markdown 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/com/jillesvangurp/kotlin4example/SourceRepository.kt: -------------------------------------------------------------------------------- 1 | package com.jillesvangurp.kotlin4example 2 | 3 | /** 4 | * This is used to tell Kotlin4Example where to look for code in your project and to create 5 | * markdown links to files in your repository. 6 | */ 7 | data class SourceRepository( 8 | /** The link to your Github repository. */ 9 | val repoUrl: String, 10 | /** The name of your primary branch. Defaults to main. */ 11 | val branch: String = "main", 12 | /** Relative paths to all folders where Kotlin4Example should look for source files.*/ 13 | val sourcePaths: Set = setOf("src/main/kotlin", "src/test/kotlin")) { 14 | 15 | /** 16 | * Quick way to create markdown for your repository 17 | */ 18 | fun md(block: Kotlin4Example.() -> Unit) = lazyOf( 19 | Kotlin4Example.markdown(this, block) 20 | ) 21 | 22 | /** 23 | * Construct a url to a given path in your repository. 24 | */ 25 | fun urlForFile(path: String) = "${repoUrl}/blob/${branch}/$path" 26 | } 27 | -------------------------------------------------------------------------------- /src/test/kotlin/com/jillesvangurp/kotlin4example/DocGenTest.kt: -------------------------------------------------------------------------------- 1 | package com.jillesvangurp.kotlin4example 2 | 3 | import com.jillesvangurp.kotlin4example.docs.readmeMarkdown 4 | import org.junit.jupiter.api.Test 5 | 6 | // READMEWRITEBEGIN 7 | /** 8 | * The readme is generated when the tests run. 9 | */ 10 | class DocGenTest { 11 | @Test 12 | fun `generate readme for this project`() { 13 | val readmePage = Page( 14 | title = "Kotlin4Example", 15 | fileName = "README.md" 16 | ) 17 | // readmeMarkdown is a lazy of the markdown content 18 | readmePage.write(markdown = readmeMarkdown) 19 | } 20 | } 21 | // READMEWRITEEND 22 | -------------------------------------------------------------------------------- /src/test/kotlin/com/jillesvangurp/kotlin4example/KotlinForExampleTest.kt: -------------------------------------------------------------------------------- 1 | package com.jillesvangurp.kotlin4example 2 | 3 | import io.kotest.matchers.ints.shouldBeLessThanOrEqual 4 | import io.kotest.matchers.shouldBe 5 | import io.kotest.matchers.shouldNotBe 6 | import io.kotest.matchers.string.shouldContain 7 | import io.kotest.matchers.string.shouldNotContain 8 | import org.junit.jupiter.api.Test 9 | import org.junit.jupiter.api.assertThrows 10 | 11 | val repo = SourceRepository("https://github.com/jillesvangurp/kotlin4example", branch = "master") 12 | 13 | val testDocOutsideClass by repo.md { 14 | // should not contain FooBar because this comment is outside the block 15 | // should contain BarFoo from this comment 16 | renderExampleOutput( 17 | exampleOutput = example(true, "kotlin", false, false, 80) { 18 | // should contain BarFoo from this comment 19 | println("Hello" + " World!") 20 | }, 21 | stdOutOnly = false 22 | ) 23 | +mdLinkToSelf() 24 | // and the output of the println 25 | } 26 | 27 | class KotlinForExampleTest { 28 | @Test 29 | fun `should render markdown with code block outside class`() { 30 | testDocOutsideClass shouldNotContain "FooBar" // because in a comment outside the block 31 | testDocOutsideClass shouldContain "BarFoo" // from the comment 32 | testDocOutsideClass shouldContain "Hello World!" // it should have captured the output of println this 33 | } 34 | 35 | @Test 36 | fun `link to self should be correct`() { 37 | testDocOutsideClass shouldContain "https://github.com/jillesvangurp/kotlin4example/blob/master/src/test/kotlin/com/jillesvangurp/kotlin4example/KotlinForExampleTest.kt" 38 | } 39 | 40 | @Test 41 | fun `do not allow long source lines`() { 42 | assertThrows { 43 | repo.md { 44 | // too long 45 | renderExampleOutput( 46 | example(true, "kotlin", false, false, 80) { 47 | // too long 48 | println("****************************************************************************************************") 49 | }, 50 | false 51 | ) 52 | 53 | }.value // make sure to access the value 54 | } 55 | } 56 | 57 | @Test 58 | fun `wrap long source lines`() { 59 | 60 | repo.md { 61 | // too long but will be wrapped 62 | renderExampleOutput( 63 | example(wrap = true, block = { 64 | // too long but will be wrapped 65 | println("****************************************************************************************************") 66 | }), 67 | false, 68 | wrap = true 69 | ) 70 | 71 | }.value.lines().forEach { 72 | it.length shouldBeLessThanOrEqual 80 73 | } // make sure to access the value 74 | } 75 | 76 | @Test 77 | fun `capture return value`() { 78 | repo.md { 79 | renderExampleOutput( 80 | example(true, "kotlin", false, false, 80) { 81 | 1 + 1 82 | }, 83 | false 84 | ) 85 | }.value shouldContain "2" 86 | } 87 | 88 | @Test 89 | fun `capture return value in suspendingBlock`() { 90 | repo.md { 91 | renderExampleOutput( 92 | example { 93 | 1 + 1 94 | }, 95 | false 96 | ) 97 | }.value shouldContain "2" 98 | } 99 | 100 | @Test 101 | fun `capture output from multiple blocks`() { 102 | val out1 = repo.md { 103 | example(true, "kotlin", false, false, 80 104 | ) { 105 | print("hel") 106 | print("lo") 107 | } 108 | }.value 109 | // if we disable printing nothing gets printed 110 | out1 shouldNotContain "hello" 111 | 112 | val out2 = repo.md { 113 | renderExampleOutput( 114 | example(true, "kotlin", false, false, 80) { 115 | print("hel") 116 | print("lo") 117 | }, 118 | true 119 | ) 120 | renderExampleOutput( 121 | example(true, "kotlin", false, false, 80) { 122 | println("world") 123 | }, 124 | false 125 | ) 126 | }.value 127 | // but we can reuse the same block capture and print at the end 128 | out2 shouldContain "hello" 129 | out2 shouldContain "world" 130 | } 131 | 132 | @Test 133 | fun `reindent correctly`() { 134 | val json = """ 135 | { 136 | "foo": { 137 | "bar": "foo" 138 | } 139 | } 140 | """.trimIndent() 141 | val reindented = json.reIndent(2) 142 | reindented.lines().firstOrNull { 143 | // should have cleaned up the double indent 144 | it.startsWith(" ") 145 | } shouldBe null 146 | reindented.lines().firstOrNull { 147 | // should have replaced the double indent with four spaces 148 | it.startsWith(" ") 149 | } shouldNotBe null 150 | reindented shouldBe """ 151 | { 152 | "foo": { 153 | "bar": "foo" 154 | } 155 | } 156 | """.trimIndent() 157 | } 158 | 159 | @Test 160 | fun `should not reindent already indented`() { 161 | val json=""" 162 | { 163 | "foo": { 164 | "bar": "foo" 165 | } 166 | } 167 | """.trimIndent() 168 | json.reIndent() shouldBe json 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/test/kotlin/com/jillesvangurp/kotlin4example/docs/intro.md: -------------------------------------------------------------------------------- 1 | [![](https://jitpack.io/v/jillesvangurp/kotlin4example.svg)](https://jitpack.io/#jillesvangurp/kotlin4example) 2 | [![Actions Status](https://github.com/jillesvangurp/kotlin4example/workflows/CI-gradle-build/badge.svg)](https://github.com/jillesvangurp/kotlin4example/actions) 3 | 4 | This project implements [literate programming](https://en.wikipedia.org/wiki/Literate_programming) in Kotlin. Literate programming is useful 5 | for documenting projects. Having working code in your documentation, ensures that the examples you include are correct 6 | and always up to date. And making it easy to include examples with your code lowers the barrier for writing good documentation. 7 | 8 | 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. 9 | 10 | Write your documentation using a kotlin markdown DSL. Use simple `example { // code goes here }` blocks to provide examples. Kotlin4example will generate nice markdown with the code inside that block added as code blocks. See below for a detailed introduction. 11 | 12 | This README is of course generated with kotlin4example. 13 | 14 | ## Gradle 15 | 16 | Add the dependency to your project and start writing some documentation. See below for some examples. 17 | I tend to put my documentation code in my tests so running the tests produces the documentation as a side effect. 18 | 19 | ```kotlin 20 | implementation("com.github.jillesvangurp:kotlin4example:") 21 | ``` 22 | 23 | You will also need to add the Jitpack repository: 24 | 25 | ```kotlin 26 | repositories { 27 | mavenCentral() 28 | maven { url = uri("https://jitpack.io") } 29 | } 30 | ``` -------------------------------------------------------------------------------- /src/test/kotlin/com/jillesvangurp/kotlin4example/docs/outro.md: -------------------------------------------------------------------------------- 1 | For more elaborate examples of using this library, checkout my 2 | [kt-search](https://github.com/jillesvangurp/kt-search) project. That 3 | 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. 4 | 5 | ## Why another documentation tool? 6 | 7 | When I started writing documentation for my [Kotlin Client for Elasticsearch](https://githubcom/jillesvangurp/es-kotlin-wrapper-client), I quickly discovered that keeping the 8 | examples in the documentation working was a challenge. I'd refactor or rename something which then would invalidate 9 | all my examples. Staying on top of that is a lot of work. 10 | 11 | Instead of just using one of the many documentation tools out there that can grab chunks of source code based on 12 | 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. 13 | 14 | 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. 15 | 16 | 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. 17 | 18 | ## Projects that use kotlin4example 19 | 20 | - [kt-search](https://github.com/jillesvangurp/kt-search) 21 | - [kotlin-opencage-client](https://github.com/jillesvangurp/kotlin-opencage-client) 22 | - [json-dsl](https://github.com/jillesvangurp/json-dsl) 23 | 24 | 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. 25 | -------------------------------------------------------------------------------- /src/test/kotlin/com/jillesvangurp/kotlin4example/docs/readme.kt: -------------------------------------------------------------------------------- 1 | package com.jillesvangurp.kotlin4example.docs 2 | 3 | import com.jillesvangurp.kotlin4example.DocGenTest 4 | import com.jillesvangurp.kotlin4example.Page 5 | import com.jillesvangurp.kotlin4example.SourceRepository 6 | import com.jillesvangurp.kotlin4example.mdPageLink 7 | 8 | // BEGIN_REPO_DEFINITION 9 | val k4ERepo = SourceRepository( 10 | // used to construct markdown links to files in your repository 11 | repoUrl = "https://github.com/jillesvangurp/kotlin4example", 12 | // default is main 13 | branch = "master", 14 | // this is the default 15 | sourcePaths = setOf( 16 | "src/main/kotlin", 17 | "src/test/kotlin" 18 | ) 19 | ) 20 | // END_REPO_DEFINITION 21 | 22 | val readmeMarkdown by k4ERepo.md { 23 | // for larger bits of text, it's nice to just load them from a markdown file 24 | includeMdFile("intro.md") 25 | 26 | // structure your markdown sections with kotlin nested blocks 27 | section("Getting Started") { 28 | +""" 29 | After adding this library to your (test) dependencies, you can start adding code 30 | to generate markdown. 31 | """.trimIndent() 32 | 33 | subSection("Creating a SourceRepository") { 34 | 35 | +""" 36 | The first thing you need is a `SourceRepository` definition. This is needed to tell 37 | kotlin4example about your repository, where to link, and where your code is. 38 | 39 | Some of the functions in kotlin4example construct links to files in your github repository, 40 | or lookup code from files in your source code. 41 | """.trimIndent() 42 | 43 | // this is how you can include code snippets from existing kotlin files 44 | exampleFromSnippet("com/jillesvangurp/kotlin4example/docs/readme.kt", "REPO_DEFINITION") 45 | } 46 | subSection("Creating markdown") { 47 | 48 | """ 49 | Once you have a repository, you can use it to create some Markdown via an extension function: 50 | """.trimIndent() 51 | 52 | example { 53 | val myMarkdown = k4ERepo.md { 54 | section("Introduction") 55 | +""" 56 | Hello world! 57 | """.trimIndent() 58 | } 59 | println(myMarkdown) 60 | }.let { 61 | +""" 62 | This will generate some markdown that looks as follows. 63 | """.trimIndent() 64 | mdCodeBlock(code = it.stdOut, type = "markdown") 65 | } 66 | } 67 | subSection("Using your Markdown to create a page") { 68 | +""" 69 | Kotlin4example has a simple page abstraction that you 70 | can use to organize your markdown content into pages and files 71 | """.trimIndent() 72 | 73 | val myMarkdown= "ignore" 74 | example(runExample = false) { 75 | val page = Page(title = "Hello!", fileName = "hello.md") 76 | // creates hello.md 77 | page.write(myMarkdown) 78 | } 79 | } 80 | 81 | subSection("This README is generated") { 82 | +""" 83 | This README.md is of course created from kotlin code that 84 | runs as part of the test suite. You can look at the kotlin 85 | source code that generates this markdown ${mdLinkToSelf("here")}. 86 | 87 | The code that writes the `README.md file` is as follows: 88 | """.trimIndent() 89 | exampleFromSnippet(DocGenTest::class, "READMEWRITE") 90 | +""" 91 | Here's a link to the source code on Github: ${mdLink(DocGenTest::class)}. 92 | 93 | The code that constructs the markdown is a bit longer, you can find it 94 | ${mdLinkToRepoResource("here", "com/jillesvangurp/kotlin4example/docs/readme.kt")}. 95 | """.trimIndent() 96 | } 97 | } 98 | section("Example blocks") { 99 | +""" 100 | With Kotlin4Example you can mix examples and markdown easily. 101 | An example is a Kotlin code block. Because it is a code block, 102 | you are forced to ensure it is syntactically correct and that it compiles. 103 | 104 | By executing the block (you can disable this), you can further guarantee it does what it 105 | is supposed to and you can intercept output and integrate that into your 106 | documentation as well 107 | 108 | For example: 109 | """.trimIndent() 110 | 111 | // a bit of kotlin4example inception here, but it works 112 | example(runExample = false) { 113 | // out is an ExampleOutput instance 114 | // with both stdout and the return 115 | // value as a Result. Any exceptions 116 | // are captured as well. 117 | val out = example { 118 | print("Hello World") 119 | } 120 | // this is how you can append arbitrary markdown 121 | +""" 122 | This example prints **${out.stdOut}** when it executes. 123 | """.trimIndent() 124 | } 125 | +""" 126 | The block you pass to example can be a suspending block; so you can create examples for 127 | your co-routine libraries too. Kotlin4example uses `runBlocking` to run your examples. 128 | 129 | When you include the above in your Markdown it will render as follows: 130 | """.trimIndent() 131 | 132 | example { 133 | print("Hello World") 134 | }.let { out -> 135 | // this is how you can append arbitrary markdown 136 | +""" 137 | This example prints **${out.stdOut}** when it executes. 138 | """.trimIndent() 139 | } 140 | 141 | subSection("Configuring examples") { 142 | 143 | +""" 144 | Sometimes you just want to show but not run the code. You can control this with the 145 | `runExample` parameter. 146 | """.trimIndent() 147 | example(runExample = false) { 148 | // 149 | example( 150 | runExample = false, 151 | ) { 152 | // your code goes here 153 | // but it won't run 154 | } 155 | } 156 | +""" 157 | The library imposes a default line length of 80 characters on your examples. The 158 | reason is that code blocks with long lines look ugly on web pages. E.g. Github will give 159 | you a horizontal scrollbar. 160 | 161 | You can of course turn this off or turn on the built in wrapping (wraps at the 80th character) 162 | 163 | """.trimIndent() 164 | example(runExample = false) { 165 | 166 | // making sure the example fits in a web page 167 | // long lines tend to look ugly in documentation 168 | example( 169 | // use longer line length 170 | // default is 80 171 | lineLength = 120, 172 | // wrap lines that are too long 173 | // default is false 174 | wrap = true, 175 | // don't fail on lines that are too long 176 | // default is false 177 | allowLongLines = true, 178 | 179 | ) { 180 | // your code goes here 181 | } 182 | } 183 | } 184 | 185 | subSection("Code snippets") { 186 | +""" 187 | While it is nice to have executable blocks as examples, 188 | sometimes you just want to grab 189 | code directly from some Kotlin file. You can do that with snippets. 190 | """.trimIndent() 191 | 192 | example { 193 | // BEGIN_MY_CODE_SNIPPET 194 | println("Example code that shows in a snippet") 195 | // END_MY_CODE_SNIPPET 196 | } 197 | // little hack to avoid picking up this line ;-) 198 | exampleFromSnippet("com/jillesvangurp/kotlin4example/docs/readme.kt","MY_" + "CODE_SNIPPET") 199 | +""" 200 | The `BEGIN_` and `END_` prefix are optional but it helps readability. 201 | 202 | You include the code in your markdown as follows: 203 | """.trimIndent() 204 | 205 | example(runExample = false) { 206 | exampleFromSnippet( 207 | // relative path to your source file 208 | // setup your source modules in the repository 209 | sourceFileName = "com/jillesvangurp/kotlin4example/docs/readme.kt", 210 | snippetId = "MY_CODE_SNIPPET" 211 | ) 212 | } 213 | } 214 | subSection("Misc Markdown") { 215 | //some more features to help you write markdown 216 | 217 | subSubSection("Simple markdown") { 218 | +""" 219 | This is how you can use the markdown dsl to generate markdown. 220 | """.trimIndent() 221 | example(runExample = true) { 222 | section("Section") { 223 | subSection("Sub Section") { 224 | +""" 225 | You can use string literals, templates ${1 + 1}, 226 | and [links](https://github.com/jillesvangurp/kotlin4example) 227 | or other **markdown** formatting. 228 | """.trimIndent() 229 | 230 | 231 | subSubSection("Sub sub section") { 232 | +""" 233 | There's more 234 | """.trimIndent() 235 | 236 | unorderedList("bullets","**bold**","*italic") 237 | 238 | orderedList("one","two","three") 239 | 240 | blockquote(""" 241 | The difference between code and poetry ... 242 | 243 | ... poetry doesn’t need to compile. 244 | """.trimIndent() 245 | ) 246 | 247 | } 248 | } 249 | } 250 | } 251 | } 252 | subSubSection("Links") { 253 | +""" 254 | Linking to different things in your repository. 255 | """.trimIndent() 256 | example(runExample = false) { 257 | 258 | // you can also just include markdown files 259 | // useful if you have a lot of markdown 260 | // content without code examples 261 | includeMdFile("intro.md") 262 | 263 | // link to things in your git repository 264 | mdLink(DocGenTest::class) 265 | 266 | // link to things in one of your source directories 267 | // you can customize where it looks in SourceRepository 268 | mdLinkToRepoResource( 269 | title = "A file", 270 | relativeUrl = "com/jillesvangurp/kotlin4example/Kotlin4Example.kt" 271 | ) 272 | 273 | val anotherPage = Page("Page 2", "page2.md") 274 | // link to another page in your manual 275 | mdPageLink(anotherPage) 276 | 277 | // and of course you can link to your self 278 | mdLinkToSelf("This class") 279 | } 280 | } 281 | 282 | subSubSection("Tables") { 283 | +""" 284 | Including tables is easy if you don't want to manually format them. 285 | """.trimIndent() 286 | 287 | example(runExample = true) { 288 | table(listOf("Function","Explanation"),listOf( 289 | listOf("mdLink","Add a link to a class or file in your repository"), 290 | listOf("mdLinkToRepoResource","Add a file in your repository"), 291 | listOf("includeMdFile","include a markdown file"), 292 | listOf("example","Example code block"), 293 | )) 294 | } 295 | } 296 | } 297 | subSection("Source code blocks") { 298 | +""" 299 | You can add your own source code blocks as well. 300 | """.trimIndent() 301 | example(runExample = false) { 302 | mdCodeBlock( 303 | code = """ 304 | Useful if you have some **non kotlin code** that you want to show 305 | """.trimIndent(), 306 | type = "markdown" 307 | ) 308 | } 309 | } 310 | } 311 | 312 | includeMdFile("outro.md") 313 | } 314 | 315 | 316 | 317 | -------------------------------------------------------------------------------- /versions.properties: -------------------------------------------------------------------------------- 1 | #### Dependencies and Plugin versions with their available updates. 2 | #### Generated by `./gradlew refreshVersions` version 0.60.5 3 | #### 4 | #### Don't manually edit or split the comments that start with four hashtags (####), 5 | #### they will be overwritten by refreshVersions. 6 | #### 7 | #### suppress inspection "SpellCheckingInspection" for whole file 8 | #### suppress inspection "UnusedProperty" for whole file 9 | 10 | plugin.org.jetbrains.dokka=2.0.0 11 | 12 | version.kotlinx.coroutines=1.10.2 13 | 14 | version.org.junit.platform..junit-platform-launcher=1.12.2 15 | ## # available=1.13.0-M1 16 | ## # available=1.13.0-M2 17 | ## # available=1.13.0-M3 18 | 19 | version.org.slf4j..slf4j-api=2.0.17 20 | ## # available=2.1.0-alpha0 21 | ## # available=2.1.0-alpha1 22 | 23 | version.org.slf4j..log4j-over-slf4j=2.0.17 24 | ## # available=2.1.0-alpha0 25 | ## # available=2.1.0-alpha1 26 | 27 | version.org.slf4j..jul-to-slf4j=2.0.17 28 | ## # available=2.1.0-alpha0 29 | ## # available=2.1.0-alpha1 30 | 31 | version.org.slf4j..jcl-over-slf4j=2.0.17 32 | ## # available=2.1.0-alpha0 33 | ## # available=2.1.0-alpha1 34 | 35 | version.org.apache.logging.log4j..log4j-to-slf4j=2.24.3 36 | ## # available=3.0.0-alpha1 37 | ## # available=3.0.0-beta1 38 | ## # available=3.0.0-beta2 39 | 40 | version.kotlin=2.1.21 41 | ## # available=2.2.0-Beta1 42 | ## # available=2.2.0-Beta2 43 | ## # available=2.2.0-RC 44 | 45 | version.kotest=5.9.1 46 | ## # available=6.0.0.M1 47 | ## # available=6.0.0.M2 48 | ## # available=6.0.0.M3 49 | ## # available=6.0.0.M4 50 | 51 | version.junit.jupiter=5.12.2 52 | ## # available=5.13.0-M1 53 | ## # available=5.13.0-M2 54 | ## # available=5.13.0-M3 55 | 56 | version.io.github.microutils..kotlin-logging=3.0.5 57 | ## # available=4.0.0-beta-1 58 | ## # available=4.0.0-beta-2 59 | 60 | version.ch.qos.logback..logback-classic=1.5.18 61 | --------------------------------------------------------------------------------