@@ -13,6 +13,13 @@ 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+
20+ const val KNIT_PATTERN_PROP = " knit.pattern"
21+ const val KNIT_DIR_PROP = " knit.dir"
22+
1623// --- markdown syntax
1724
1825const val DIRECTIVE_START = " <!--- "
@@ -25,10 +32,9 @@ const val CLEAR_DIRECTIVE = "CLEAR"
2532const val TEST_DIRECTIVE = " TEST"
2633
2734const val KNIT_AUTONUMBER_PLACEHOLDER = ' #'
28- const val KNIT_AUTONUMBER_REGEX = " [0-9a-z]+"
35+ const val KNIT_AUTONUMBER_REGEX = " ( [0-9a-z]+) "
2936
3037const val TEST_NAME_DIRECTIVE = " TEST_NAME"
31- const val TEST_NAME_PROP = " test.name"
3238
3339const val MODULE_DIRECTIVE = " MODULE"
3440const val INDEX_DIRECTIVE = " INDEX"
@@ -44,7 +50,6 @@ const val TEST_END = "```"
4450
4551const val SECTION_START = " ##"
4652
47- const val PACKAGE_PREFIX = " package "
4853const val STARTS_WITH_PREDICATE = " STARTS_WITH"
4954const val ARBITRARY_TIME_PREDICATE = " ARBITRARY_TIME"
5055const val FLEXIBLE_TIME_PREDICATE = " FLEXIBLE_TIME"
@@ -75,70 +80,65 @@ fun main(args: Array<String>) {
7580class KnitConfig (
7681 val path : String ,
7782 val regex : Regex ,
78- val autonumberGroup : Int ,
7983 val autonumberDigits : Int
8084)
8185
8286fun KnitProps.knitConfig (): KnitConfig ? {
83- val dir = this [" knit.dir " ] ? : return null
84- var pattern = getValue(" knit.pattern " )
87+ val dir = this [KNIT_DIR_PROP ] ? : return null
88+ var pattern = getValue(KNIT_PATTERN_PROP )
8589 val i = pattern.indexOf(KNIT_AUTONUMBER_PLACEHOLDER )
86- var autonumberGroup = 0
8790 var autonumberDigits = 0
8891 if (i >= 0 ) {
8992 val j = pattern.lastIndexOf(KNIT_AUTONUMBER_PLACEHOLDER )
9093 autonumberDigits = j - i + 1
9194 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"
95+ " $KNIT_PATTERN_PROP property can only use a contiguous range of '$KNIT_AUTONUMBER_PLACEHOLDER ' for auto-numbering"
9396 }
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 )"
97+ require(' (' !in pattern && ' )' !in pattern) {
98+ " $KNIT_PATTERN_PROP property cannot have match groups"
10099 }
101- pattern = pattern.substring(0 , i) + replacementRegex + pattern.substring(j + 1 )
100+ pattern = pattern.substring(0 , i) + KNIT_AUTONUMBER_REGEX + pattern.substring(j + 1 )
102101 }
103- val path = " $dir$pattern "
104- return KnitConfig (path, Regex (" \\ (($path )\\ )" ), autonumberGroup, autonumberDigits)
102+ val path = " $dir ( $pattern ) "
103+ return KnitConfig (path, Regex (" \\ (($path )\\ )" ), autonumberDigits)
105104}
106105
107106class KnitIncludeEnv (
108107 val file : File ,
109- props : KnitProps
108+ props : KnitProps ,
109+ knitName : String
110110) {
111- val knit = props.getMap(" knit" )
111+ val knit = props.getMap(" knit" ) + mapOf ( " name " to knitName)
112112}
113113
114- fun KnitConfig.loadMainInclude (file : File , props : KnitProps ): Include {
114+ fun KnitConfig.loadMainInclude (file : File , props : KnitProps , knitName : String ): Include {
115115 val include = Include (Regex (path))
116- include.lines + = props.loadTemplateLines(" knit.include" , KnitIncludeEnv (file, props))
116+ include.lines + = props.loadTemplateLines(" knit.include" , KnitIncludeEnv (file, props, knitName ))
117117 include.lines + = " "
118118 return include
119119}
120120
121+ data class KnitInfo (val pkg : String , val name : String )
122+
121123fun knit (markdownFile : File ): Boolean {
122124 println (" *** Reading $markdownFile " )
123125 val props = markdownFile.findProps()
124126 val knit = props.knitConfig()
125- var knitAutonumberIndex = HashMap <String , Int >()
127+ val knitAutonumberIndex = HashMap <String , Int >()
126128 val tocLines = arrayListOf<String >()
127129 val includes = arrayListOf<Include >()
128130 val codeLines = arrayListOf<String >()
129131 val testLines = arrayListOf<String >()
130132 var testName: String? = props[TEST_NAME_PROP ]
131133 val testOutLines = arrayListOf<String >()
132- var lastPgk : String ? = null
134+ var lastKnit : KnitInfo ? = null
133135 val files = mutableSetOf<File >()
134136 val allApiRefs = arrayListOf<ApiRef >()
135137 val remainingApiRefNames = mutableSetOf<String >()
136138 var moduleName: String by Delegates .notNull()
137139 var docsRoot: String by Delegates .notNull()
138140 var retryKnitLater = false
139141 val tocRefs = ArrayList <TocRef >().also { tocRefMap[markdownFile] = it }
140- // load main includes (if defined)
141- knit?.loadMainInclude(markdownFile, props)?.let { includes + = it }
142142 // read markdown file
143143 val markdown = markdownFile.withMarkdownTextReader {
144144 mainLoop@ while (true ) {
@@ -200,7 +200,7 @@ fun knit(markdownFile: File): Boolean {
200200 testName = directive.param
201201 }
202202 TEST_DIRECTIVE -> {
203- require(lastPgk != null ) { " ' $PACKAGE_PREFIX ' prefix was not found in emitted code " }
203+ require(lastKnit != null ) { " $TEST_DIRECTIVE must be preceeded by knitted file " }
204204 require(testName != null ) { " Neither $TEST_NAME_DIRECTIVE directive nor '$TEST_NAME_PROP 'property was specified" }
205205 val predicate = directive.param
206206 if (testLines.isEmpty()) {
@@ -211,7 +211,7 @@ fun knit(markdownFile: File): Boolean {
211211 } else {
212212 requireSingleLine(directive)
213213 }
214- makeTest(testOutLines, lastPgk !! , testLines, predicate)
214+ makeTest(testOutLines, lastKnit !! , testLines, predicate)
215215 testLines.clear()
216216 }
217217 MODULE_DIRECTIVE -> {
@@ -261,12 +261,13 @@ fun knit(markdownFile: File): Boolean {
261261 }
262262 }
263263 knit?.regex?.find(inLine)?.let knitRegexMatch@{ knitMatch ->
264- val fileName = knitMatch.groups[1 ]!! .value
264+ val path = knitMatch.groups[1 ]!! .value // full matched knit path dir dir & file name
265+ val fileGroup = knitMatch.groups[2 ]!!
266+ val fileName = fileGroup.value // knitted file name like "example-basic-01.kt"
265267 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 }
268+ val numGroup = knitMatch.groups[3 ]!! // file number part like "01"
269+ val key = inLine.substring(fileGroup.range.first, numGroup.range.first) +
270+ inLine.substring(numGroup.range.last + 1 , fileGroup.range.last + 1 )
270271 val index = knitAutonumberIndex.getOrElse(key) { 1 }
271272 val num = index.toString().padStart(knit.autonumberDigits, ' 0' )
272273 if (numGroup.value != num) { // update and retry with this line if a different number
@@ -277,25 +278,23 @@ fun knit(markdownFile: File): Boolean {
277278 }
278279 knitAutonumberIndex[key] = index + 1
279280 }
280- val file = File (markdownFile.parentFile, fileName )
281+ val file = File (markdownFile.parentFile, path )
281282 require(files.add(file)) { " Duplicate file: $file " }
282283 println (" Knitting $file ..." )
283284 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- }
285+ val fileIncludes = arrayListOf<Include >()
286+ // load & process template of the main include
287+ val knitName = fileName.toKnitName()
288+ fileIncludes + = knit.loadMainInclude(markdownFile, props, knitName)
289+ fileIncludes + = includes.filter { it.regex.matches(path) }
290+ for (include in fileIncludes) outLines + = include.lines
293291 if (outLines.last().isNotBlank()) outLines + = " "
294292 for (code in codeLines) {
295293 outLines + = code.replace(" System.currentTimeMillis()" , " currentTimeMillis()" )
296294 }
297295 codeLines.clear()
298296 writeLinesIfNeeded(file, outLines)
297+ lastKnit = KnitInfo (props.getValue(" knit.package" ), knitName)
299298 }
300299 }
301300 } ? : return false // false when failed
@@ -326,24 +325,17 @@ fun knit(markdownFile: File): Boolean {
326325 return true
327326}
328327
328+ // Converts file name like "example-basic-01.kt" to unique knit.name for package like "exampleBasic01"
329+ fun String.toKnitName (): String = substringBefore(' .' ).capitalizeAfter(' -' )
330+
329331data class TocRef (val levelPrefix : String , val name : String , val ref : String )
330332
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- }
341- }
342- }
333+ fun makeTest (testOutLines : MutableList <String >, knit : KnitInfo , test : List <String >, predicate : String ) {
334+ val funName = knit.name.capitalize()
343335 testOutLines + = " "
344336 testOutLines + = " @Test"
345337 testOutLines + = " fun test$funName () {"
346- val prefix = " test(\" $funName \" ) { $pgk .main() }"
338+ val prefix = " test(\" $funName \" ) { ${knit.pkg} . ${knit.name} .main() }"
347339 when (predicate) {
348340 " " -> makeTestLines(testOutLines, prefix, " verifyLines" , test)
349341 STARTS_WITH_PREDICATE -> makeTestLines(testOutLines, prefix, " verifyLinesStartWith" , test)
@@ -362,6 +354,18 @@ fun makeTest(testOutLines: MutableList<String>, pgk: String, test: List<String>,
362354 testOutLines + = " }"
363355}
364356
357+ private fun String.capitalizeAfter (char : Char ): String = buildString {
358+ var cap = false
359+ for (c in this @capitalizeAfter) {
360+ cap = if (c == char) {
361+ true
362+ } else {
363+ append(if (cap) c.toUpperCase() else c)
364+ false
365+ }
366+ }
367+ }
368+
365369private fun makeTestLines (testOutLines : MutableList <String >, prefix : String , method : String , test : List <String >) {
366370 testOutLines + = " $prefix .$method ("
367371 for ((index, testLine) in test.withIndex()) {
@@ -372,15 +376,6 @@ private fun makeTestLines(testOutLines: MutableList<String>, prefix: String, met
372376 testOutLines + = " )"
373377}
374378
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-
384379class TestTemplateEnv (
385380 val file : File ,
386381 props : KnitProps ,
0 commit comments