├── .gitignore ├── LICENSE.txt ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── imports.txt ├── prefixes.txt ├── samples-out ├── VerifyAllSamples.kt ├── file │ └── 1_Point.kt └── sample │ └── _SomeSampleChapter.kt ├── samples ├── codesnippet.adoc ├── file.adoc ├── func.adoc ├── helloworld.adoc └── sample.adoc ├── settings.gradle └── src ├── main └── kotlin │ └── com │ └── hadihariri │ └── markcode │ ├── Chapter.kt │ ├── CodeExample.kt │ ├── DocMetadata.kt │ ├── ExampleLanguage.kt │ ├── ExampleMetadata.kt │ ├── ExampleOutput.kt │ ├── JavaLanguage.kt │ ├── KotlinLanguage.kt │ ├── Main.kt │ ├── OutputVerifier.kt │ ├── SectionCounter.kt │ ├── SourceMetadata.kt │ ├── Synthetics.kt │ └── codegen.kt └── test └── kotlin └── SyntheticsTests.kt /.gitignore: -------------------------------------------------------------------------------- 1 | out/* 2 | ~$* 3 | .idea/workspace.xml 4 | .DS_Store/* 5 | .DS_Store 6 | lib/kotlin-runtime.jar 7 | build/ 8 | classes/ 9 | .gradle/ 10 | gradle.properties 11 | slack.log 12 | 13 | lib/ 14 | .idea/ 15 | **.iml 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Hadi Hariri and Contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Mark-Code 2 | 3 | Mark-Code is a command line application that can parse AsciiDoc (Markdown support coming) files for source code, extract the code, compile it and make sure everything works. Its main purpose is for those that 4 | write documentation for API's et al and it in some way helps make sure that the code in documentation is current (or at least compiles). 5 | 6 | Currently it mainly supports Kotlin with some features available for Java. For now `mark-code` only supports [AsciiDoc](http://asciidoctor.org) and in general supports all the syntax available for [code snippets for AsciiDoc](http://asciidoctor.org/docs/asciidoc-writers-guide/#listing-and-source-code-blocks). 7 | 8 | ## What does mark-code do? 9 | 10 | It does two things: 11 | 12 | * Extract all source code from your documentation. 13 | * Compiles and optionally runs the verifier to make sure the output matches the input. 14 | 15 | ## Creating Code Snippets in your Documentation 16 | 17 | ### Output Structure 18 | 19 | When `mark-code` processes your input files (*.adoc), it creates a folder for each `chapter`, which is defined with `==` in AsciiDoc. Inside the folder 20 | a Kotlin file for each individual snippet. 21 | 22 | 23 | ### Source code 24 | 25 | Any source code you write will automatically generate a corresponding valid Kotlin file which can then be checked for compilation errors 26 | 27 | 28 | ```asciidoc 29 | [source,kotlin] 30 | ---- 31 | fun sum(x: Int, y: Int) = x + y 32 | sum(2,3) 33 | ---- 34 | ``` 35 | 36 | would generate 37 | 38 | ```kotlin 39 | fun sum(x: Int, y: Int) = x + y 40 | ``` 41 | 42 | and 43 | 44 | ```asciidoc 45 | [source,kotlin] 46 | ---- 47 | package org.kotlin.byexample 48 | 49 | fun main(args: Array) { 50 | println("Hello, World!") 51 | } 52 | ---- 53 | ``` 54 | 55 | would generate 56 | 57 | ```kotlin 58 | package org.kotlin.byexample 59 | 60 | fun main(args: Array) { 61 | println("Hello, World!") 62 | } 63 | ``` 64 | 65 | Like all AsciiDoc source, you can annotate your code 66 | 67 | ```asciidoc 68 | [source,kotlin] 69 | ---- 70 | package org.kotlin.byexample <1> 71 | 72 | fun main(args: Array) { <2> 73 | println("Hello, World!") <3> 74 | } 75 | ---- 76 | <1> Kotlin code is usually defined in packages. If you don't define one, the default package will be used 77 | <2> The main entry point to a Kotlin application is a function called *main* 78 | <3> `println` writes to standard output and is implicitly imported 79 | ``` 80 | 81 | ### Code snippet 82 | 83 | If you want `mark-code` to ignore a Kotlin code snippet, yet still have it in your document, use `kotlin-snippet` 84 | 85 | ```asciidoc 86 | [source,kotlin-snippet] 87 | ---- 88 | fun sum(x: Int, y: Int) = x + y 89 | ---- 90 | ``` 91 | ### Referencing Code Snippets 92 | 93 | You can give code snippets names that you can later reference 94 | 95 | ```asciidoc 96 | [source,kotlin,Point.kt] 97 | ---- 98 | data class Point(val x: Int, val y: Int) 99 | ---- 100 | ``` 101 | 102 | and include it in another snippet 103 | 104 | ```asciidoc 105 | [source,kotlin,prepend=Point.kt] 106 | ---- 107 | val point = Point(20, 30) 108 | ---- 109 | ``` 110 | 111 | ### Define main function 112 | 113 | You can define a main function for a code snippet, different to the default `main` 114 | 115 | ```asciidoc 116 | [source,kotlin,main=ExecuteFile.kt] 117 | ---- 118 | val point = Point(20, 30) 119 | ---- 120 | ``` 121 | 122 | ### Verify output 123 | 124 | You can execute a code snippet and validate the output (currently works for standard output) 125 | 126 | ```asciidoc 127 | [source,kotlin] 128 | ---- 129 | fun sum(x: Int, y: Int) = x + y 130 | 131 | >>> println(sum(3, 6)) 132 | 9 133 | ``` 134 | 135 | What you want executed you prefix with `>>>` and on the next line you put the result. The `>>>` will automatically generate a `main` function. 136 | 137 | 138 | ## How to use it 139 | 140 | The easiest way is to run the Gradle task `distZip`. This create a zip file in the `distributions` folder which contains a few files and folders. 141 | 142 | Unzip the contents to your project folder, where you'd call the tool located in the `bin` folder `mark-code` from. In order for this to work, you need 143 | to make sure that the following files are located in your project folder: 144 | 145 | * `imports.txt` and `prefixes.txt`: should be placed in the root folder of your project. Copy them from the contents of the `zip` file and modify at will. 146 | * `OutputVerifier.kt`: copy it to the source folder which is usually where the `VerifyAllSamples.kt` will be located 147 | 148 | To invoke the tool, use the following command: 149 | 150 | ```bash 151 | mark-code -o 152 | ``` 153 | 154 | where `` is where your *.adoc (*.md not supported yet) are located, and `` is where you want the code to be verified to be placed. The -o is optional and generates the 155 | `VerifyAllSamples.kt` which actually executes the code to see if the output is as specified. 156 | 157 | ## TODO 158 | 159 | * Add unit tests as adding/refactoring 160 | * Continue code reorganisation and cleanup 161 | * Make it independent of concept of Chapters 162 | * Add markdown support 163 | 164 | Contributions welcome. 165 | 166 | 167 | ## To run 168 | 169 | Run the Gradle `distZip` task. Copy the output to your project folder (usually `tools`). Copy the 170 | imports.txt and prefixes.txt samples to your root folder and modify as needed. 171 | Copy the src/com/hadihariri/markcode/OutputVerifier.kt to your -o folder. 172 | 173 | ## Credits 174 | 175 | Credits to [Dimitry Jemerov](https://twitter.com/intelliyole) for his work on this. Dmitry wrote the original code for the [Kotlin In Action](https://www.manning.com/books/kotlin-in-action) book. 176 | 177 | 178 | ## License 179 | 180 | Project is Licensed under MIT (c) 2017 Hadi Hariri and Contributors 181 | 182 | 183 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | group 'hadihariri.com' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.1.1' 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | dependencies { 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | } 13 | } 14 | 15 | apply plugin: 'kotlin' 16 | apply plugin: 'application' 17 | 18 | distZip { 19 | into(project.name+"-"+project.version) { 20 | from '.' 21 | include 'imports.txt' 22 | include 'prefixes.txt' 23 | include 'src/main/kotlin/com/hadihariri/markcode/OutputVerifier.kt' 24 | } 25 | } 26 | 27 | 28 | mainClassName = 'com.hadihariri.markcode.MainKt' 29 | 30 | repositories { 31 | mavenCentral() 32 | maven { url "http://dl.bintray.com/jetbrains/spek" } 33 | } 34 | 35 | dependencies { 36 | compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" 37 | testCompile 'org.jetbrains.kotlin:kotlin-test:1.1.0' 38 | testCompile 'org.jetbrains.spek:spek-api:1.1.0' 39 | testRuntime 'org.jetbrains.spek:spek-junit-platform-engine:1.1.0' 40 | } 41 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhariri/mark-code/881b662e239f4fb0f960387f3d45e5c7d713c952/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Apr 14 20:10:36 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /imports.txt: -------------------------------------------------------------------------------- 1 | Comparator=java.util.Comparator 2 | TreeMap=java.util.TreeMap 3 | ArrayList=java.util.ArrayList 4 | BufferedReader=java.io.BufferedReader 5 | FileReader=java.io.FileReader 6 | StringReader=java.io.StringReader 7 | InputStreamReader=java.io.InputStreamReader 8 | File=java.io.File 9 | HashSet=java.util.HashSet 10 | Date=java.util.Date 11 | Collections=java.util.Collections 12 | BigDecimal=java.math.BigDecimal 13 | Serializable=java.io.Serializable 14 | PropertyChangeSupport=java.beans.PropertyChangeSupport 15 | PropertyChangeListener=java.beans.PropertyChangeListener 16 | Delegates=kotlin.properties.Delegates 17 | KClass=kotlin.reflect.KClass 18 | KProperty=kotlin.reflect.KProperty 19 | memberProperties=kotlin.reflect.memberProperties 20 | compareValuesBy=kotlin.comparisons.compareValuesBy 21 | @Before=org.junit.Before 22 | @Test=org.junit.Test 23 | Assert=org.junit.Assert 24 | Period=java.time.Period 25 | LocalDate=java.time.LocalDate 26 | -------------------------------------------------------------------------------- /prefixes.txt: -------------------------------------------------------------------------------- 1 | getFacebookName=fun getFacebookName(accountId: Int) = "fb:$accountId" 2 | -------------------------------------------------------------------------------- /samples-out/VerifyAllSamples.kt: -------------------------------------------------------------------------------- 1 | import com.hadihariri.markcode.OutputVerifier 2 | import helloworld.exkt.main as helloworld_exkt 3 | 4 | 5 | fun main(args: Array) { 6 | val verifier = OutputVerifier() 7 | verifier.verifySample(::helloworld_exkt, "samples-out/helloworld/1_HelloWorld.txt", "helloworld.adoc:8") 8 | verifier.report() 9 | } 10 | -------------------------------------------------------------------------------- /samples-out/file/1_Point.kt: -------------------------------------------------------------------------------- 1 | package mypackge 2 | 3 | data class Point(val x: Int, val y: Int) 4 | -------------------------------------------------------------------------------- /samples-out/sample/_SomeSampleChapter.kt: -------------------------------------------------------------------------------- 1 | package sample._SomeSampleChapter 2 | 3 | import java.util.ArrayList 4 | 5 | data class Person(val name: String, 6 | val age: Int? = null) 7 | 8 | fun main(args: Array) { 9 | val otherPersons = ArrayList() 10 | 11 | val persons = listOf(Person("Alice"), 12 | Person("Bob", age = 29)) 13 | 14 | val oldest = persons.maxBy { it.age ?: 0 } 15 | println("The oldest is: $oldest") 16 | } 17 | 18 | // The oldest is: Person(name=Bob, age=29) 19 | -------------------------------------------------------------------------------- /samples/codesnippet.adoc: -------------------------------------------------------------------------------- 1 | == Code Snippet 2 | 3 | [source,kotlin-snippet] 4 | ---- 5 | fun sum(x: Int, y: Int) = x + y 6 | ---- 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/file.adoc: -------------------------------------------------------------------------------- 1 | == File 2 | 3 | [source,kotlin,Point.kt] 4 | ---- 5 | package mypackge 6 | 7 | data class Point(val x: Int, val y: Int) 8 | ---- 9 | -------------------------------------------------------------------------------- /samples/func.adoc: -------------------------------------------------------------------------------- 1 | [source,kotlin] 2 | ---- 3 | fun sum(x: Int, y: Int) = x + y 4 | ---- 5 | -------------------------------------------------------------------------------- /samples/helloworld.adoc: -------------------------------------------------------------------------------- 1 | == Hello World 2 | 3 | 4 | [source,kotlin] 5 | ---- 6 | fun sum(x: Int, y: Int) = x + y <1> 7 | >>> println(sum(2,3)) 8 | 5 9 | ---- 10 | <1> Single-expression functions do not need explicit return type defined 11 | 12 | -------------------------------------------------------------------------------- /samples/sample.adoc: -------------------------------------------------------------------------------- 1 | = Some Sample Chapter 2 | 3 | [source,kotlin] 4 | .A Taste of Kotlin 5 | ---- 6 | data class Person(val name: String, <1> 7 | val age: Int? = null) <2> 8 | 9 | 10 | fun main(args: Array) { <3> 11 | val otherPersons = ArrayList() 12 | 13 | val persons = listOf(Person("Alice"), 14 | Person("Bob", age = 29)) <4> 15 | 16 | 17 | val oldest = persons.maxBy { it.age ?: 0 } <5> 18 | println("The oldest is: $oldest") <6> 19 | } 20 | 21 | // The oldest is: Person(name=Bob, age=29) <7> 22 | ---- 23 | <1> "data" class 24 | <2> Nullable type (Int?); the default value for the argument 25 | <3> Top-level function 26 | <4> Named argument 27 | <5> Lambda expression; Elvis operator 28 | <6> String template 29 | <7> Autogenerated toString -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'mark-code' 2 | 3 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hadihariri/markcode/Chapter.kt: -------------------------------------------------------------------------------- 1 | package com.hadihariri.markcode 2 | 3 | 4 | import java.io.File 5 | import java.util.* 6 | 7 | class Chapter(val chapterFile: File, val chapterCodeDir: File) { 8 | val examples = LinkedHashMap() 9 | val examplesBySection = mutableMapOf>() 10 | val sectionCounter = SectionCounter(extractChapterNumber(chapterFile.name)) 11 | 12 | fun process(writeExpectedOutput: Boolean) { 13 | var currentCaption = chapterFile.nameWithoutExtension 14 | var currentExample: CodeExample? = null 15 | var lastExample: CodeExample? = null 16 | var exampleIndex = 1 17 | var inBlockComment = false 18 | 19 | var currentCaptionIndex = 0 20 | var sourceMetadataLine: String? = null 21 | var expectedExampleLineNumber = -1 22 | var prevLine: String? = null 23 | 24 | for ((lineNumber, line) in chapterFile.readLines().withIndex()) { 25 | if (currentExample != null) { 26 | if (line.startsWith("----")) { 27 | lastExample = currentExample 28 | currentExample = null 29 | } 30 | else { 31 | currentExample.addLine(lineNumber + 1, line) 32 | } 33 | } 34 | else { 35 | if (line.startsWith("////")) { 36 | inBlockComment = !inBlockComment 37 | } 38 | else if (!inBlockComment) { 39 | if (line.startsWith("=")) { 40 | val caption = parseCaption(lineNumber, line, prevLine) 41 | if (!caption.isEmpty()) { 42 | currentCaption = caption 43 | currentCaptionIndex = 0 44 | } 45 | } 46 | else if (line.startsWith("[source,")) { 47 | sourceMetadataLine = line 48 | expectedExampleLineNumber = lineNumber + 1 49 | } 50 | else if (line.startsWith(".") && lineNumber == expectedExampleLineNumber) { 51 | expectedExampleLineNumber++ 52 | } 53 | else if (line.startsWith("----")) { 54 | val sourceMetadata = if (expectedExampleLineNumber == lineNumber) 55 | DocMetadata().parse(sourceMetadataLine!!) 56 | else 57 | null 58 | 59 | if (sourceMetadata?.append == true) { 60 | currentExample = lastExample 61 | } 62 | else { 63 | val filename = sourceMetadata?.filename ?: (captionToFilename(currentCaption, currentCaptionIndex) + ".kt") 64 | 65 | val prependExamples = sourceMetadata?.prependFilenames?.map { 66 | examples [it] ?: throw IllegalStateException("Can't find example to prepend: $it") 67 | } ?: emptyList() 68 | val mainExample = sourceMetadata?.mainFilename?.let { 69 | examples[it] ?: throw IllegalStateException("Can't find example to take 'main' from: $it") 70 | } 71 | 72 | val skip = if (sourceMetadata?.language == "kotlin") 73 | sourceMetadata.skip 74 | else if (sourceMetadata?.language == "java") 75 | sourceMetadata.filename == null 76 | else 77 | true 78 | 79 | val sectionNumber = sectionCounter.currentSectionNumber() 80 | 81 | currentExample = CodeExample(this, filename, 82 | if (sourceMetadata?.language == "java") JavaLanguage else KotlinLanguage, 83 | prependExamples, mainExample, skip, sourceMetadata ?: EMPTY_SOURCE_METADATA) 84 | if (examples[filename] != null) { 85 | throw IllegalStateException("Duplicate filename $filename") 86 | } 87 | if (sourceMetadata?.filename != null || currentExample.willWriteOutputFile()) { 88 | examples.put(filename, currentExample) 89 | } 90 | if (currentExample.willWriteOutputFile()) { 91 | examplesBySection.getOrPut(sectionNumber, { ArrayList() }).add(currentExample) 92 | exampleIndex++ 93 | currentCaptionIndex++ 94 | } 95 | } 96 | } 97 | } 98 | } 99 | prevLine = line 100 | } 101 | 102 | for ((sectionNumber, examples) in examplesBySection) { 103 | if (examples.size == 1) { 104 | examples[0].updateOutputFileName("${sectionNumber}_${examples[0].filename}") 105 | } 106 | else { 107 | for ((i, example) in examples.withIndex()) { 108 | example.updateOutputFileName("${sectionNumber}_${i+1}_${example.filename}") 109 | } 110 | } 111 | } 112 | 113 | examples.values.forEach { 114 | it.process(chapterCodeDir, writeExpectedOutput) 115 | } 116 | } 117 | 118 | private fun parseCaption(lineNumber: Int, line: String, prevLine: String?): String { 119 | val level = line.takeWhile { it == '=' }.length 120 | if (level > 4) { 121 | System.err.println("${chapterFile.name}:$lineNumber: Heading level too large") 122 | } 123 | else { 124 | sectionCounter.increment(level) 125 | } 126 | if (prevLine?.startsWith("[exampleprefix=") == true) { 127 | return prevLine.substringAfter('=').removeSuffix("]") 128 | } 129 | return line.trimStart('=').trimStart() 130 | } 131 | 132 | val hasErrors: Boolean get() = examples.values.any { it.hasErrors } 133 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/hadihariri/markcode/CodeExample.kt: -------------------------------------------------------------------------------- 1 | package com.hadihariri.markcode 2 | 3 | import com.hadihariri.markcode.Synthetics 4 | import com.hadihariri.markcode.annotationRegex 5 | import com.hadihariri.markcode.Chapter 6 | import com.hadihariri.markcode.ExampleLanguage 7 | import com.hadihariri.markcode.KotlinLanguage 8 | import com.hadihariri.markcode.SourceMetadata 9 | import java.io.File 10 | import java.util.* 11 | 12 | class CodeExample(val chapter: Chapter, 13 | var filename: String, 14 | val language: ExampleLanguage, 15 | val prependExamples: List, 16 | val mainExample: CodeExample?, 17 | val skip: Boolean, 18 | val sourceMetadata: SourceMetadata) { 19 | 20 | private val rawLines = TreeMap() 21 | private val text = StringBuilder() 22 | private val textWithoutMain = StringBuilder() 23 | private val textOfMain = StringBuilder() 24 | val expectedOutput = StringBuilder() 25 | var hasErrors: Boolean = false 26 | private set 27 | var packageName: String? = null 28 | var expectedOutputStartLine: Int? = null 29 | 30 | fun updateOutputFileName(filename: String) { 31 | if (language == KotlinLanguage) { 32 | this.filename = filename 33 | } 34 | } 35 | 36 | fun addLine(lineNumber: Int, line: String) { 37 | rawLines.put(lineNumber, line) 38 | } 39 | 40 | fun processLines() { 41 | text.setLength(0) 42 | val everythingInsideMain = (!skip && mainExample == null && rawLines.values.none { it.startsWith(">>>") } && 43 | !language.hasAnyDeclarations(rawLines.values)) 44 | 45 | var insideMain = false 46 | for ((lineNumber, line) in rawLines) { 47 | if (line.startsWith(">>>") || line.startsWith("...") || everythingInsideMain) { 48 | if (!insideMain) { 49 | appendProcessedLine("\n${language.formatMainFunction(filename)}", true) 50 | insideMain = true 51 | } 52 | val lineContent = line.removePrefix(">>> ").removePrefix("... ") 53 | appendSourceLine(language.formatMainFunctionIndent() + lineContent, lineNumber, insideMain) 54 | } 55 | else if (!insideMain) { 56 | appendSourceLine(line, lineNumber, insideMain) 57 | } 58 | else if (line.isNotEmpty()) { 59 | if (expectedOutputStartLine == null) { 60 | expectedOutputStartLine = lineNumber 61 | } 62 | expectedOutput.append(checkRemoveAnnotation(line, lineNumber)).append("\n") 63 | } 64 | } 65 | 66 | if (insideMain) { 67 | appendProcessedLine(language.formatMainFunctionEnd(), true) 68 | } 69 | } 70 | 71 | private fun appendSourceLine(line: String, lineNumber: Int, insideMain: Boolean) { 72 | appendProcessedLine(checkRemoveAnnotation(line, lineNumber), insideMain) 73 | } 74 | 75 | private fun checkRemoveAnnotation(line: String, lineNumber: Int): String { 76 | val matchResult = annotationRegex.find(line) 77 | val processedLine: String 78 | if (matchResult != null) { 79 | val lineBeforeAnno = line.substring(0, matchResult.range.start).trimEnd() 80 | if (lineBeforeAnno.length > 150) { 81 | reportLineError(lineNumber, "Code line before annotation is too long (55 characters max)") 82 | } 83 | processedLine = lineBeforeAnno 84 | } else { 85 | if (line.trimEnd().length > 175) { 86 | reportLineError(lineNumber, "Code line is too long (76 characters max)") 87 | } 88 | processedLine = line.trimEnd() 89 | } 90 | return processedLine 91 | } 92 | 93 | private fun appendProcessedLine(processedLine: String, insideMain: Boolean) { 94 | text.append(processedLine).append("\n") 95 | if (!insideMain) { 96 | textWithoutMain.append(processedLine).append("\n") 97 | } else { 98 | textOfMain.append(processedLine).append("\n") 99 | } 100 | } 101 | 102 | fun process(outputDir: File, writeExpectedOutput: Boolean) { 103 | processLines() 104 | 105 | if (!skip) { 106 | outputDir.mkdir() 107 | val outputFile = getOutputFile(outputDir) 108 | val result = generateOutputText(outputDir.name) 109 | outputFile.writeText(result.toString()) 110 | if ("fun main" !in result && outputFile.extension == "kt") { 111 | System.err.println("No 'main' function in ${outputFile.name}") 112 | } 113 | 114 | if (writeExpectedOutput && hasOutputToVerify) { 115 | val expectedOutputFile = getExpectedOutputFile(outputDir) 116 | if (expectedOutput.isNotEmpty()) { 117 | expectedOutputFile.writeText(expectedOutput.toString()) 118 | } 119 | else { 120 | mainExample?.expectedOutput?.let { 121 | expectedOutputFile.writeText(it.toString()) 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | val hasOutputToVerify: Boolean 129 | get() = (expectedOutput.isNotEmpty() || mainExample?.expectedOutput?.isNotEmpty() == true) && !sourceMetadata.noverify 130 | 131 | private fun generateOutputText(dirName: String): String { 132 | val result = buildString { 133 | if (sourceMetadata.jvmname) { 134 | append("@file:JvmName(\"${sourceMetadata.filename!!.substringBefore('.')}\")\n") 135 | } 136 | 137 | if (!rawLines.values.any { it.startsWith("package")}) { 138 | packageName = language.getPackageName(dirName, sourceMetadata.filename ?: filename) 139 | append(language.formatPackageStatement(packageName!!) + "\n\n") 140 | } 141 | 142 | var anyPrefixes = false 143 | for (prefix in collectSyntheticElements(Synthetics.imports)) { 144 | append(language.formatImportStatement(prefix)).append("\n") 145 | anyPrefixes = true 146 | } 147 | for (prefix in collectSyntheticElements(Synthetics.prefixes)) { 148 | append(prefix).append("\n") 149 | anyPrefixes = true 150 | } 151 | for (import in sourceMetadata.imports) { 152 | append(language.formatImportStatement(import) + "\n") 153 | } 154 | if (anyPrefixes || sourceMetadata.imports.isNotEmpty()) append("\n") 155 | 156 | for (prependExample in prependExamples) { 157 | append(prependExample.textWithoutMain).append("\n") 158 | } 159 | append(text) 160 | if (mainExample != null) { 161 | append(mainExample.textOfMain) 162 | } 163 | } 164 | return result.replace("\n\n\n", "\n\n") 165 | } 166 | 167 | private fun collectSyntheticElements(prefixMap: Map): Collection { 168 | return prefixMap.filter { 169 | entry -> entry.key in text.toString() || 170 | prependExamples.any { entry.key in it.text.toString() } || 171 | (mainExample?.textOfMain?.contains(entry.key) ?: false) 172 | }.values 173 | } 174 | 175 | fun willWriteOutputFile() = !skip 176 | 177 | fun reportLineError(lineNumber: Int, message: String) { 178 | System.err.println("${chapter.chapterFile.name} line $lineNumber: $message") 179 | hasErrors = true 180 | } 181 | 182 | fun getOutputFile(outputDir: File): File = File(outputDir, filename) 183 | fun getExpectedOutputFile(outputDir: File) = File(outputDir, expectedOutputFileName) 184 | val expectedOutputFileName: String 185 | get() = filename.replaceAfterLast(".", "txt") 186 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/hadihariri/markcode/DocMetadata.kt: -------------------------------------------------------------------------------- 1 | package com.hadihariri.markcode 2 | 3 | class DocMetadata : ExampleMetadata { 4 | override fun parse(line: String): SourceMetadata { 5 | val parts = line.trimStart('[').trimEnd(']').split(',') 6 | val language = parts.getOrNull(1) 7 | var fileName: String? = null 8 | val prependFilenames = arrayListOf() 9 | var mainFilename: String? = null 10 | var append = false 11 | var skip = false 12 | var noverify = false 13 | var jvmname = false 14 | val imports = arrayListOf() 15 | for (part in parts.drop(2)) { 16 | if (part.startsWith("prepend=")) { 17 | prependFilenames.add(part.removePrefix("prepend=")) 18 | } else if (part.startsWith("main=")) { 19 | mainFilename = part.removePrefix("main=") 20 | } else if (part.startsWith("import=")) { 21 | imports.add(part.removePrefix("import=")) 22 | } else if (part == "append") { 23 | append = true 24 | } else if (part == "skip") { 25 | skip = true 26 | } else if (part == "noverify") { 27 | noverify = true 28 | } else if (part == "jvmname") { 29 | jvmname = true 30 | } else { 31 | fileName = part 32 | } 33 | } 34 | 35 | return SourceMetadata(language, fileName, prependFilenames, append, mainFilename, skip, noverify, jvmname, imports) 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/hadihariri/markcode/ExampleLanguage.kt: -------------------------------------------------------------------------------- 1 | package com.hadihariri.markcode 2 | 3 | interface ExampleLanguage { 4 | fun getPackageName(dirName: String, fileName: String): String 5 | fun formatPackageStatement(packageName: String): String 6 | fun formatImportStatement(importName: String): String 7 | fun formatMainFunction(fileName: String): String 8 | fun formatMainFunctionIndent(): String 9 | fun formatMainFunctionEnd(): String 10 | fun hasAnyDeclarations(lines: Collection): Boolean 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/hadihariri/markcode/ExampleMetadata.kt: -------------------------------------------------------------------------------- 1 | package com.hadihariri.markcode 2 | 3 | 4 | 5 | interface ExampleMetadata { 6 | fun parse(line: String): SourceMetadata 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/hadihariri/markcode/ExampleOutput.kt: -------------------------------------------------------------------------------- 1 | package com.hadihariri.markcode 2 | 3 | data class ExampleOutput(val functionName: String, val expectedOutputFile: String, val location: String) -------------------------------------------------------------------------------- /src/main/kotlin/com/hadihariri/markcode/JavaLanguage.kt: -------------------------------------------------------------------------------- 1 | package com.hadihariri.markcode 2 | 3 | import com.hadihariri.markcode.ExampleLanguage 4 | 5 | object JavaLanguage : ExampleLanguage { 6 | override fun getPackageName(dirName: String, fileName: String) = dirName 7 | override fun formatPackageStatement(packageName: String) = "package $packageName;" 8 | override fun formatImportStatement(importName: String) = "import $importName;" 9 | 10 | override fun formatMainFunction(fileName: String): String { 11 | val className = fileName.substringBefore('.') 12 | return "public class $className {\n public static void main(String[] args) {" 13 | } 14 | 15 | override fun formatMainFunctionIndent(): String = " " 16 | override fun formatMainFunctionEnd(): String = " }\n}" 17 | 18 | override fun hasAnyDeclarations(lines: Collection) = true 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/hadihariri/markcode/KotlinLanguage.kt: -------------------------------------------------------------------------------- 1 | package com.hadihariri.markcode 2 | 3 | import com.hadihariri.markcode.ExampleLanguage 4 | import com.hadihariri.markcode.startsWithAny 5 | 6 | object KotlinLanguage : ExampleLanguage { 7 | override fun getPackageName(dirName: String, fileName: String): String { 8 | if (fileName[0] in '0'..'9') 9 | return dirName + ".ex" + fileName.substringAfter('.').removeSuffix(".kt").replace('.', '_') 10 | return dirName + "." + fileName.removeSuffix(".kt") 11 | } 12 | 13 | override fun formatPackageStatement(packageName: String) = "package $packageName" 14 | 15 | override fun formatImportStatement(importName: String) = "import $importName" 16 | 17 | override fun formatMainFunction(fileName: String) = "fun main(args: Array) {" 18 | override fun formatMainFunctionIndent() = " " 19 | override fun formatMainFunctionEnd() = "}" 20 | 21 | override fun hasAnyDeclarations(lines: Collection) = lines.any { 22 | it.startsWithAny("fun", "inline fun", "operator fun", "class", "enum class", "interface", "open class", "data class", "abstract class") 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/hadihariri/markcode/Main.kt: -------------------------------------------------------------------------------- 1 | package com.hadihariri.markcode 2 | 3 | import java.io.File 4 | 5 | fun main(args: Array) { 6 | if (args.size < 2) { 7 | println("Usage: [-o]") 8 | return 9 | } 10 | 11 | val writeExpectedOutput = args.size > 2 && args[2] == "-o" 12 | val chapters = mutableListOf() 13 | File(args[0]).listFiles { _, name -> name.endsWith(".adoc") }.forEach { 14 | val chapterCodeDir = File(args[1], it.nameWithoutExtension ) 15 | val chapter = Chapter(it, chapterCodeDir) 16 | chapters.add(chapter) 17 | chapter.process(writeExpectedOutput) 18 | } 19 | if (writeExpectedOutput) { 20 | writeVerifyAllSamples(chapters, File(args[1])) 21 | } 22 | if (chapters.any { it.hasErrors }) { 23 | System.exit(1) 24 | } 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hadihariri/markcode/OutputVerifier.kt: -------------------------------------------------------------------------------- 1 | package com.hadihariri.markcode 2 | 3 | import java.io.ByteArrayOutputStream 4 | import java.io.File 5 | import java.io.PrintStream 6 | 7 | 8 | class OutputVerifier { 9 | private val originalOut = System.out 10 | private val errors = mutableListOf() 11 | 12 | fun verifySample(mainFunction: (Array) -> Unit, expectedOutputFile: String, location: String) { 13 | val actualOutputStream = ByteArrayOutputStream() 14 | System.setOut(PrintStream(actualOutputStream)) 15 | try { 16 | mainFunction(arrayOf()) 17 | } catch (e: Exception) { 18 | actualOutputStream.writer().use { writer -> 19 | writer.write(e.toString() + "\n") 20 | } 21 | } 22 | val actualOutput = String(actualOutputStream.toByteArray(), Charsets.UTF_8).trimEnd() 23 | val expectedOutput = File(expectedOutputFile).readText().trimEnd() 24 | if (actualOutput != expectedOutput) { 25 | errors.add("Unexpected output for $expectedOutputFile at $location: $actualOutput") 26 | } 27 | } 28 | 29 | fun report() { 30 | System.setOut(originalOut) 31 | for (error in errors) { 32 | println(error) 33 | } 34 | if (errors.isNotEmpty()) { 35 | System.exit(1) 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/hadihariri/markcode/SectionCounter.kt: -------------------------------------------------------------------------------- 1 | package com.hadihariri.markcode 2 | 3 | class SectionCounter(chapterNumber: Int) { 4 | private val sectionNumber = intArrayOf(chapterNumber, 0, 0, 0) 5 | 6 | fun increment(level: Int) { 7 | if (level > 1) { 8 | sectionNumber[level - 1]++ 9 | for (i in level..3) { 10 | sectionNumber[i] = 0 11 | } 12 | } 13 | } 14 | 15 | fun currentSectionNumber(): String { 16 | return sectionNumber.filter { it != 0 }.joinToString(separator = ".") 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/hadihariri/markcode/SourceMetadata.kt: -------------------------------------------------------------------------------- 1 | package com.hadihariri.markcode 2 | 3 | data class SourceMetadata(val language: String?, 4 | val filename: String?, 5 | val prependFilenames: List, 6 | val append: Boolean, 7 | val mainFilename: String?, 8 | val skip: Boolean, 9 | val noverify: Boolean, 10 | val jvmname: Boolean, 11 | val imports: List) -------------------------------------------------------------------------------- /src/main/kotlin/com/hadihariri/markcode/Synthetics.kt: -------------------------------------------------------------------------------- 1 | package com.hadihariri.markcode 2 | 3 | import java.io.File 4 | 5 | 6 | object Synthetics { 7 | val imports = File("./imports.txt").readLines().map { 8 | Pair(it.substringBefore("="), it.substringAfter("=")) 9 | }.toMap() 10 | val prefixes = File("./prefixes.txt").readLines().map { 11 | Pair(it.substringBefore("="), it.substringAfter("=")) 12 | }.toMap() 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/kotlin/com/hadihariri/markcode/codegen.kt: -------------------------------------------------------------------------------- 1 | package com.hadihariri.markcode 2 | 3 | import java.io.BufferedWriter 4 | import java.io.File 5 | import java.io.FileWriter 6 | 7 | val EMPTY_SOURCE_METADATA = SourceMetadata(null, null, emptyList(), false, null, false, false, false, emptyList()) 8 | 9 | fun captionToFilename(caption: String, captionIndex: Int): String { 10 | val name = StringBuilder() 11 | var capitalize = true 12 | caption.forEach { 13 | if (it == ' ') { 14 | capitalize = true 15 | } else if (it.isLetterOrDigit()) { 16 | name.append(if (capitalize) it.toUpperCase() else it) 17 | capitalize = false 18 | } 19 | } 20 | return name.toString() + (if (captionIndex == 0) "" else "$captionIndex") 21 | } 22 | 23 | 24 | 25 | val annotationRegex = Regex("<(\\d+)>") 26 | 27 | fun String.startsWithAny(vararg prefixes: String) = prefixes.any { startsWith(it) } 28 | 29 | fun extractChapterNumber(filename: String): Int { 30 | if (filename.startsWith("ch")) { 31 | return Integer.parseInt(filename.removePrefix("ch").substringBeforeLast(".")) 32 | } 33 | return 0 34 | } 35 | 36 | fun writeVerifyAllSamples(chapters: List, outputDir: File) { 37 | BufferedWriter(FileWriter(File(outputDir, "VerifyAllSamples.kt"))).use { outputFile -> 38 | outputFile.write("import com.hadihariri.markcode.OutputVerifier\n") 39 | 40 | val examples = mutableListOf() 41 | 42 | for (chapter in chapters) { 43 | for (example in chapter.examples.values.filter { !it.skip && it.language is KotlinLanguage && it.hasOutputToVerify }) { 44 | val fqName = example.packageName ?: continue 45 | val import = fqName.replace('.', '_') 46 | outputFile.write("import $fqName.main as $import\n") 47 | examples.add(ExampleOutput( 48 | import, 49 | "${chapter.chapterCodeDir.name}/${example.expectedOutputFileName}", 50 | "${example.chapter.chapterFile.name}:${example.expectedOutputStartLine ?: example.mainExample?.expectedOutputStartLine}")) 51 | } 52 | } 53 | 54 | // TODO - make this actually read the file so we don't have to keep in sync 55 | // (distZip right now needs proper path for this so that's why hacked right now) 56 | outputFile.write("\n\nfun main(args: Array) {\n") 57 | outputFile.write(" val verifier = OutputVerifier()\n") 58 | for ((function, expectedOutput, location) in examples) { 59 | outputFile.write(" verifier.verifySample(::$function, \"$outputDir/$expectedOutput\", \"$location\")\n") 60 | } 61 | outputFile.write(" verifier.report()\n}\n") 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/test/kotlin/SyntheticsTests.kt: -------------------------------------------------------------------------------- 1 | import com.hadihariri.markcode.Synthetics 2 | import org.jetbrains.spek.api.Spek 3 | import org.jetbrains.spek.api.dsl.given 4 | import org.jetbrains.spek.api.dsl.it 5 | import org.jetbrains.spek.api.dsl.on 6 | import kotlin.test.assertEquals 7 | import kotlin.test.assertNotEquals 8 | 9 | class SyntheticsTests : Spek({ 10 | given("a list of imports defined in imports.txt") { 11 | on("accessing imports") { 12 | val values = Synthetics.imports 13 | it("should contain a map of key/value pairs") { 14 | assertNotEquals(0, values.count()) 15 | assertEquals("java.util.Comparator", values["Comparator"]) 16 | } 17 | } 18 | } 19 | given("a list of prefixes defined in prefixes.txt") { 20 | on("accessing prefixes") { 21 | val values = Synthetics.prefixes 22 | it("should contain a map of key/value pairs") { 23 | assertNotEquals(0, values.count()) 24 | assertEquals("fun getFacebookName(accountId: Int) = \"fb:\$accountId\"", values["getFacebookName"]) 25 | } 26 | } 27 | } 28 | }) --------------------------------------------------------------------------------