@@ -13,6 +13,17 @@ val moduleRoots = globalProperties.getValue("module.roots").split(" ")
1313val moduleMarker = globalProperties.getValue(" module.marker" )
1414val moduleDocs = globalProperties.getValue(" module.docs" )
1515
16+ // --- other props
17+
18+ const val TEST_NAME_PROP = " test.name"
19+ const val TEST_DIR_PROP = " test.dir"
20+ const val TEST_INCLUDE_PROP = " test.include"
21+
22+ const val KNIT_PACKAGE_PROP = " knit.package"
23+ const val KNIT_PATTERN_PROP = " knit.pattern"
24+ const val KNIT_DIR_PROP = " knit.dir"
25+ const val KNIT_INCLUDE_PROP = " knit.include"
26+
1627// --- markdown syntax
1728
1829const val DIRECTIVE_START = " <!--- "
@@ -25,10 +36,9 @@ const val CLEAR_DIRECTIVE = "CLEAR"
2536const val TEST_DIRECTIVE = " TEST"
2637
2738const val KNIT_AUTONUMBER_PLACEHOLDER = ' #'
28- const val KNIT_AUTONUMBER_REGEX = " [0-9a-z]+"
39+ const val KNIT_AUTONUMBER_REGEX = " ( [0-9a-z]+) "
2940
3041const val TEST_NAME_DIRECTIVE = " TEST_NAME"
31- const val TEST_NAME_PROP = " test.name"
3242
3343const val MODULE_DIRECTIVE = " MODULE"
3444const val INDEX_DIRECTIVE = " INDEX"
@@ -44,7 +54,6 @@ const val TEST_END = "```"
4454
4555const val SECTION_START = " ##"
4656
47- const val PACKAGE_PREFIX = " package "
4857const val STARTS_WITH_PREDICATE = " STARTS_WITH"
4958const val ARBITRARY_TIME_PREDICATE = " ARBITRARY_TIME"
5059const val FLEXIBLE_TIME_PREDICATE = " FLEXIBLE_TIME"
@@ -75,70 +84,69 @@ fun main(args: Array<String>) {
7584class KnitConfig (
7685 val path : String ,
7786 val regex : Regex ,
78- val autonumberGroup : Int ,
7987 val autonumberDigits : Int
8088)
8189
8290fun KnitProps.knitConfig (): KnitConfig ? {
83- val dir = this [" knit.dir " ] ? : return null
84- var pattern = getValue(" knit.pattern " )
91+ val dir = this [KNIT_DIR_PROP ] ? : return null
92+ var pattern = getValue(KNIT_PATTERN_PROP )
8593 val i = pattern.indexOf(KNIT_AUTONUMBER_PLACEHOLDER )
86- var autonumberGroup = 0
8794 var autonumberDigits = 0
8895 if (i >= 0 ) {
8996 val j = pattern.lastIndexOf(KNIT_AUTONUMBER_PLACEHOLDER )
9097 autonumberDigits = j - i + 1
9198 require(pattern.substring(i, j + 1 ) == KNIT_AUTONUMBER_PLACEHOLDER .toString().repeat(autonumberDigits)) {
92- " knit.pattern can only use a contiguous range of '$KNIT_AUTONUMBER_PLACEHOLDER ' for auto-numbering"
99+ " $KNIT_PATTERN_PROP property can only use a contiguous range of '$KNIT_AUTONUMBER_PLACEHOLDER ' for auto-numbering"
93100 }
94- autonumberGroup = pattern.substring(0 , i).count { it == ' (' } + 1 // note: it does not understand escaped open braces
95- var replacementRegex = KNIT_AUTONUMBER_REGEX
96- if (pattern.getOrNull(i - 1 ) != ' (' || pattern.getOrNull(j + 1 ) != ' )' ) {
97- // needs its own group to extract number
98- autonumberGroup++
99- replacementRegex = " ($replacementRegex )"
101+ require(' (' !in pattern && ' )' !in pattern) {
102+ " $KNIT_PATTERN_PROP property cannot have match groups"
100103 }
101- pattern = pattern.substring(0 , i) + replacementRegex + pattern.substring(j + 1 )
104+ pattern = pattern.substring(0 , i) + KNIT_AUTONUMBER_REGEX + pattern.substring(j + 1 )
102105 }
103- val path = " $dir$pattern "
104- return KnitConfig (path, Regex (" \\ (($path )\\ )" ), autonumberGroup, autonumberDigits)
106+ val path = " $dir ( $pattern ) "
107+ return KnitConfig (path, Regex (" \\ (($path )\\ )" ), autonumberDigits)
105108}
106109
110+ @Suppress(" unused" ) // This class is passed to freemarker template
107111class KnitIncludeEnv (
108112 val file : File ,
109- props : KnitProps
113+ props : KnitProps ,
114+ knitName : String
110115) {
111- val knit = props.getMap(" knit" )
116+ val knit = props.getMap(" knit" ) + mapOf ( " name " to knitName)
112117}
113118
114- fun KnitConfig.loadMainInclude (file : File , props : KnitProps ): Include {
119+ fun KnitConfig.loadMainInclude (file : File , props : KnitProps , knitName : String ): Include {
115120 val include = Include (Regex (path))
116- include.lines + = props.loadTemplateLines(" knit.include " , KnitIncludeEnv (file, props))
121+ include.lines + = props.loadTemplateLines(KNIT_INCLUDE_PROP , KnitIncludeEnv (file, props, knitName ))
117122 include.lines + = " "
118123 return include
119124}
120125
126+ // Reference to knitted example's full package (pkg.name)
127+ class KnitRef (val pkg : String , val name : String ) {
128+ override fun toString (): String = " $pkg .$name "
129+ }
130+
121131fun knit (markdownFile : File ): Boolean {
122132 println (" *** Reading $markdownFile " )
123133 val props = markdownFile.findProps()
124134 val knit = props.knitConfig()
125- var knitAutonumberIndex = HashMap <String , Int >()
135+ val knitAutonumberIndex = HashMap <String , Int >()
126136 val tocLines = arrayListOf<String >()
127137 val includes = arrayListOf<Include >()
128138 val codeLines = arrayListOf<String >()
129139 val testLines = arrayListOf<String >()
130140 var testName: String? = props[TEST_NAME_PROP ]
131141 val testOutLines = arrayListOf<String >()
132- var lastPgk : String ? = null
142+ var lastKnit : KnitRef ? = null
133143 val files = mutableSetOf<File >()
134144 val allApiRefs = arrayListOf<ApiRef >()
135145 val remainingApiRefNames = mutableSetOf<String >()
136146 var moduleName: String by Delegates .notNull()
137147 var docsRoot: String by Delegates .notNull()
138148 var retryKnitLater = false
139149 val tocRefs = ArrayList <TocRef >().also { tocRefMap[markdownFile] = it }
140- // load main includes (if defined)
141- knit?.loadMainInclude(markdownFile, props)?.let { includes + = it }
142150 // read markdown file
143151 val markdown = markdownFile.withMarkdownTextReader {
144152 mainLoop@ while (true ) {
@@ -200,18 +208,18 @@ fun knit(markdownFile: File): Boolean {
200208 testName = directive.param
201209 }
202210 TEST_DIRECTIVE -> {
203- require(lastPgk != null ) { " ' $PACKAGE_PREFIX ' prefix was not found in emitted code " }
211+ require(lastKnit != null ) { " $TEST_DIRECTIVE must be preceded by knitted file " }
204212 require(testName != null ) { " Neither $TEST_NAME_DIRECTIVE directive nor '$TEST_NAME_PROP 'property was specified" }
205213 val predicate = directive.param
206214 if (testLines.isEmpty()) {
207215 if (directive.singleLine) {
208- require(! predicate.isEmpty ()) { " $TEST_DIRECTIVE must be preceded by $TEST_START block or contain test predicate" }
216+ require(predicate.isNotEmpty ()) { " $TEST_DIRECTIVE must be preceded by $TEST_START block or contain test predicate" }
209217 } else
210218 testLines + = readUntil(DIRECTIVE_END )
211219 } else {
212220 requireSingleLine(directive)
213221 }
214- makeTest(testOutLines, lastPgk !! , testLines, predicate)
222+ makeTest(testOutLines, lastKnit !! , testLines, predicate)
215223 testLines.clear()
216224 }
217225 MODULE_DIRECTIVE -> {
@@ -261,12 +269,13 @@ fun knit(markdownFile: File): Boolean {
261269 }
262270 }
263271 knit?.regex?.find(inLine)?.let knitRegexMatch@{ knitMatch ->
264- val fileName = knitMatch.groups[1 ]!! .value
272+ val path = knitMatch.groups[1 ]!! .value // full matched knit path dir dir & file name
273+ val fileGroup = knitMatch.groups[2 ]!!
274+ val fileName = fileGroup.value // knitted file name like "example-basic-01.kt"
265275 if (knit.autonumberDigits != 0 ) {
266- val numGroup = knitMatch.groups[knit.autonumberGroup]!!
267- val key = knitMatch.groupValues.withIndex()
268- .filter { it.index > 1 && it.index != knit.autonumberGroup }
269- .joinToString(" -" ) { it.value }
276+ val numGroup = knitMatch.groups[3 ]!! // file number part like "01"
277+ val key = inLine.substring(fileGroup.range.first, numGroup.range.first) +
278+ inLine.substring(numGroup.range.last + 1 , fileGroup.range.last + 1 )
270279 val index = knitAutonumberIndex.getOrElse(key) { 1 }
271280 val num = index.toString().padStart(knit.autonumberDigits, ' 0' )
272281 if (numGroup.value != num) { // update and retry with this line if a different number
@@ -277,25 +286,23 @@ fun knit(markdownFile: File): Boolean {
277286 }
278287 knitAutonumberIndex[key] = index + 1
279288 }
280- val file = File (markdownFile.parentFile, fileName )
289+ val file = File (markdownFile.parentFile, path )
281290 require(files.add(file)) { " Duplicate file: $file " }
282291 println (" Knitting $file ..." )
283292 val outLines = arrayListOf<String >()
284- for (include in includes) {
285- val includeMatch = include.regex.matchEntire(fileName) ? : continue
286- include.lines.forEach { includeLine ->
287- val line = makeReplacements(includeLine, includeMatch)
288- if (line.startsWith(PACKAGE_PREFIX ))
289- lastPgk = line.substring(PACKAGE_PREFIX .length).trim()
290- outLines + = line
291- }
292- }
293+ val fileIncludes = arrayListOf<Include >()
294+ // load & process template of the main include
295+ val knitName = fileName.toKnitName()
296+ fileIncludes + = knit.loadMainInclude(markdownFile, props, knitName)
297+ fileIncludes + = includes.filter { it.regex.matches(path) }
298+ for (include in fileIncludes) outLines + = include.lines
293299 if (outLines.last().isNotBlank()) outLines + = " "
294300 for (code in codeLines) {
295301 outLines + = code.replace(" System.currentTimeMillis()" , " currentTimeMillis()" )
296302 }
297303 codeLines.clear()
298304 writeLinesIfNeeded(file, outLines)
305+ lastKnit = KnitRef (props.getValue(KNIT_PACKAGE_PROP ), knitName)
299306 }
300307 }
301308 } ? : return false // false when failed
@@ -326,24 +333,27 @@ fun knit(markdownFile: File): Boolean {
326333 return true
327334}
328335
329- data class TocRef (val levelPrefix : String , val name : String , val ref : String )
336+ // Converts file name like "example-basic-01.kt" to unique knit.name for package like "exampleBasic01"
337+ private fun String.toKnitName (): String = substringBefore(' .' ).capitalizeAfter(' -' )
330338
331- fun makeTest (testOutLines : MutableList <String >, pgk : String , test : List <String >, predicate : String ) {
332- val funName = buildString {
333- var cap = true
334- for (c in pgk) {
335- cap = if (c == ' .' ) {
336- true
337- } else {
338- append(if (cap) c.toUpperCase() else c)
339- false
340- }
339+ private fun String.capitalizeAfter (char : Char ): String = buildString {
340+ var cap = false
341+ for (c in this @capitalizeAfter) {
342+ cap = if (c == char) true else {
343+ append(if (cap) c.toUpperCase() else c)
344+ false
341345 }
342346 }
347+ }
348+
349+ data class TocRef (val levelPrefix : String , val name : String , val ref : String )
350+
351+ fun makeTest (testOutLines : MutableList <String >, knit : KnitRef , test : List <String >, predicate : String ) {
352+ val funName = knit.name.capitalize()
343353 testOutLines + = " "
344354 testOutLines + = " @Test"
345355 testOutLines + = " fun test$funName () {"
346- val prefix = " test(\" $funName \" ) { $pgk .main() }"
356+ val prefix = " test(\" $funName \" ) { $knit .main() }"
347357 when (predicate) {
348358 " " -> makeTestLines(testOutLines, prefix, " verifyLines" , test)
349359 STARTS_WITH_PREDICATE -> makeTestLines(testOutLines, prefix, " verifyLinesStartWith" , test)
@@ -372,15 +382,7 @@ private fun makeTestLines(testOutLines: MutableList<String>, prefix: String, met
372382 testOutLines + = " )"
373383}
374384
375- private fun makeReplacements (line : String , match : MatchResult ): String {
376- var result = line
377- for ((id, group) in match.groups.withIndex()) {
378- if (group != null )
379- result = result.replace(" \$\$ $id " , group.value)
380- }
381- return result
382- }
383-
385+ @Suppress(" unused" ) // This class is passed to freemarker template
384386class TestTemplateEnv (
385387 val file : File ,
386388 props : KnitProps ,
@@ -393,10 +395,10 @@ private fun flushTestOut(file: File, props: KnitProps, testName: String?, testOu
393395 if (testOutLines.isEmpty()) return
394396 if (testName == null ) return
395397 val lines = arrayListOf<String >()
396- lines + = props.loadTemplateLines(" test.include " , TestTemplateEnv (file, props, testName))
398+ lines + = props.loadTemplateLines(TEST_INCLUDE_PROP , TestTemplateEnv (file, props, testName))
397399 lines + = testOutLines
398400 lines + = " }"
399- val testFile = File (props.getFile(" test.dir " ), " $testName .kt" )
401+ val testFile = File (props.getFile(TEST_DIR_PROP ), " $testName .kt" )
400402 println (" Checking $testFile " )
401403 writeLinesIfNeeded(testFile, lines)
402404 testOutLines.clear()
0 commit comments