@@ -13,6 +13,17 @@ val moduleRoots = globalProperties.getValue("module.roots").split(" ")
13
13
val moduleMarker = globalProperties.getValue(" module.marker" )
14
14
val moduleDocs = globalProperties.getValue(" module.docs" )
15
15
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
+
16
27
// --- markdown syntax
17
28
18
29
const val DIRECTIVE_START = " <!--- "
@@ -25,10 +36,9 @@ const val CLEAR_DIRECTIVE = "CLEAR"
25
36
const val TEST_DIRECTIVE = " TEST"
26
37
27
38
const val KNIT_AUTONUMBER_PLACEHOLDER = ' #'
28
- const val KNIT_AUTONUMBER_REGEX = " [0-9a-z]+"
39
+ const val KNIT_AUTONUMBER_REGEX = " ( [0-9a-z]+) "
29
40
30
41
const val TEST_NAME_DIRECTIVE = " TEST_NAME"
31
- const val TEST_NAME_PROP = " test.name"
32
42
33
43
const val MODULE_DIRECTIVE = " MODULE"
34
44
const val INDEX_DIRECTIVE = " INDEX"
@@ -44,7 +54,6 @@ const val TEST_END = "```"
44
54
45
55
const val SECTION_START = " ##"
46
56
47
- const val PACKAGE_PREFIX = " package "
48
57
const val STARTS_WITH_PREDICATE = " STARTS_WITH"
49
58
const val ARBITRARY_TIME_PREDICATE = " ARBITRARY_TIME"
50
59
const val FLEXIBLE_TIME_PREDICATE = " FLEXIBLE_TIME"
@@ -75,70 +84,69 @@ fun main(args: Array<String>) {
75
84
class KnitConfig (
76
85
val path : String ,
77
86
val regex : Regex ,
78
- val autonumberGroup : Int ,
79
87
val autonumberDigits : Int
80
88
)
81
89
82
90
fun 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 )
85
93
val i = pattern.indexOf(KNIT_AUTONUMBER_PLACEHOLDER )
86
- var autonumberGroup = 0
87
94
var autonumberDigits = 0
88
95
if (i >= 0 ) {
89
96
val j = pattern.lastIndexOf(KNIT_AUTONUMBER_PLACEHOLDER )
90
97
autonumberDigits = j - i + 1
91
98
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"
93
100
}
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"
100
103
}
101
- pattern = pattern.substring(0 , i) + replacementRegex + pattern.substring(j + 1 )
104
+ pattern = pattern.substring(0 , i) + KNIT_AUTONUMBER_REGEX + pattern.substring(j + 1 )
102
105
}
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)
105
108
}
106
109
110
+ @Suppress(" unused" ) // This class is passed to freemarker template
107
111
class KnitIncludeEnv (
108
112
val file : File ,
109
- props : KnitProps
113
+ props : KnitProps ,
114
+ knitName : String
110
115
) {
111
- val knit = props.getMap(" knit" )
116
+ val knit = props.getMap(" knit" ) + mapOf ( " name " to knitName)
112
117
}
113
118
114
- fun KnitConfig.loadMainInclude (file : File , props : KnitProps ): Include {
119
+ fun KnitConfig.loadMainInclude (file : File , props : KnitProps , knitName : String ): Include {
115
120
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 ))
117
122
include.lines + = " "
118
123
return include
119
124
}
120
125
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
+
121
131
fun knit (markdownFile : File ): Boolean {
122
132
println (" *** Reading $markdownFile " )
123
133
val props = markdownFile.findProps()
124
134
val knit = props.knitConfig()
125
- var knitAutonumberIndex = HashMap <String , Int >()
135
+ val knitAutonumberIndex = HashMap <String , Int >()
126
136
val tocLines = arrayListOf<String >()
127
137
val includes = arrayListOf<Include >()
128
138
val codeLines = arrayListOf<String >()
129
139
val testLines = arrayListOf<String >()
130
140
var testName: String? = props[TEST_NAME_PROP ]
131
141
val testOutLines = arrayListOf<String >()
132
- var lastPgk : String ? = null
142
+ var lastKnit : KnitRef ? = null
133
143
val files = mutableSetOf<File >()
134
144
val allApiRefs = arrayListOf<ApiRef >()
135
145
val remainingApiRefNames = mutableSetOf<String >()
136
146
var moduleName: String by Delegates .notNull()
137
147
var docsRoot: String by Delegates .notNull()
138
148
var retryKnitLater = false
139
149
val tocRefs = ArrayList <TocRef >().also { tocRefMap[markdownFile] = it }
140
- // load main includes (if defined)
141
- knit?.loadMainInclude(markdownFile, props)?.let { includes + = it }
142
150
// read markdown file
143
151
val markdown = markdownFile.withMarkdownTextReader {
144
152
mainLoop@ while (true ) {
@@ -200,18 +208,18 @@ fun knit(markdownFile: File): Boolean {
200
208
testName = directive.param
201
209
}
202
210
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 " }
204
212
require(testName != null ) { " Neither $TEST_NAME_DIRECTIVE directive nor '$TEST_NAME_PROP 'property was specified" }
205
213
val predicate = directive.param
206
214
if (testLines.isEmpty()) {
207
215
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" }
209
217
} else
210
218
testLines + = readUntil(DIRECTIVE_END )
211
219
} else {
212
220
requireSingleLine(directive)
213
221
}
214
- makeTest(testOutLines, lastPgk !! , testLines, predicate)
222
+ makeTest(testOutLines, lastKnit !! , testLines, predicate)
215
223
testLines.clear()
216
224
}
217
225
MODULE_DIRECTIVE -> {
@@ -261,12 +269,13 @@ fun knit(markdownFile: File): Boolean {
261
269
}
262
270
}
263
271
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"
265
275
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 )
270
279
val index = knitAutonumberIndex.getOrElse(key) { 1 }
271
280
val num = index.toString().padStart(knit.autonumberDigits, ' 0' )
272
281
if (numGroup.value != num) { // update and retry with this line if a different number
@@ -277,25 +286,23 @@ fun knit(markdownFile: File): Boolean {
277
286
}
278
287
knitAutonumberIndex[key] = index + 1
279
288
}
280
- val file = File (markdownFile.parentFile, fileName )
289
+ val file = File (markdownFile.parentFile, path )
281
290
require(files.add(file)) { " Duplicate file: $file " }
282
291
println (" Knitting $file ..." )
283
292
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
293
299
if (outLines.last().isNotBlank()) outLines + = " "
294
300
for (code in codeLines) {
295
301
outLines + = code.replace(" System.currentTimeMillis()" , " currentTimeMillis()" )
296
302
}
297
303
codeLines.clear()
298
304
writeLinesIfNeeded(file, outLines)
305
+ lastKnit = KnitRef (props.getValue(KNIT_PACKAGE_PROP ), knitName)
299
306
}
300
307
}
301
308
} ? : return false // false when failed
@@ -326,24 +333,27 @@ fun knit(markdownFile: File): Boolean {
326
333
return true
327
334
}
328
335
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(' -' )
330
338
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
341
345
}
342
346
}
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()
343
353
testOutLines + = " "
344
354
testOutLines + = " @Test"
345
355
testOutLines + = " fun test$funName () {"
346
- val prefix = " test(\" $funName \" ) { $pgk .main() }"
356
+ val prefix = " test(\" $funName \" ) { $knit .main() }"
347
357
when (predicate) {
348
358
" " -> makeTestLines(testOutLines, prefix, " verifyLines" , test)
349
359
STARTS_WITH_PREDICATE -> makeTestLines(testOutLines, prefix, " verifyLinesStartWith" , test)
@@ -372,15 +382,7 @@ private fun makeTestLines(testOutLines: MutableList<String>, prefix: String, met
372
382
testOutLines + = " )"
373
383
}
374
384
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
384
386
class TestTemplateEnv (
385
387
val file : File ,
386
388
props : KnitProps ,
@@ -393,10 +395,10 @@ private fun flushTestOut(file: File, props: KnitProps, testName: String?, testOu
393
395
if (testOutLines.isEmpty()) return
394
396
if (testName == null ) return
395
397
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))
397
399
lines + = testOutLines
398
400
lines + = " }"
399
- val testFile = File (props.getFile(" test.dir " ), " $testName .kt" )
401
+ val testFile = File (props.getFile(TEST_DIR_PROP ), " $testName .kt" )
400
402
println (" Checking $testFile " )
401
403
writeLinesIfNeeded(testFile, lines)
402
404
testOutLines.clear()
0 commit comments