@@ -13,6 +13,13 @@ 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
+
20
+ const val KNIT_PATTERN_PROP = " knit.pattern"
21
+ const val KNIT_DIR_PROP = " knit.dir"
22
+
16
23
// --- markdown syntax
17
24
18
25
const val DIRECTIVE_START = " <!--- "
@@ -25,10 +32,9 @@ const val CLEAR_DIRECTIVE = "CLEAR"
25
32
const val TEST_DIRECTIVE = " TEST"
26
33
27
34
const val KNIT_AUTONUMBER_PLACEHOLDER = ' #'
28
- const val KNIT_AUTONUMBER_REGEX = " [0-9a-z]+"
35
+ const val KNIT_AUTONUMBER_REGEX = " ( [0-9a-z]+) "
29
36
30
37
const val TEST_NAME_DIRECTIVE = " TEST_NAME"
31
- const val TEST_NAME_PROP = " test.name"
32
38
33
39
const val MODULE_DIRECTIVE = " MODULE"
34
40
const val INDEX_DIRECTIVE = " INDEX"
@@ -44,7 +50,6 @@ const val TEST_END = "```"
44
50
45
51
const val SECTION_START = " ##"
46
52
47
- const val PACKAGE_PREFIX = " package "
48
53
const val STARTS_WITH_PREDICATE = " STARTS_WITH"
49
54
const val ARBITRARY_TIME_PREDICATE = " ARBITRARY_TIME"
50
55
const val FLEXIBLE_TIME_PREDICATE = " FLEXIBLE_TIME"
@@ -75,70 +80,65 @@ fun main(args: Array<String>) {
75
80
class KnitConfig (
76
81
val path : String ,
77
82
val regex : Regex ,
78
- val autonumberGroup : Int ,
79
83
val autonumberDigits : Int
80
84
)
81
85
82
86
fun 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 )
85
89
val i = pattern.indexOf(KNIT_AUTONUMBER_PLACEHOLDER )
86
- var autonumberGroup = 0
87
90
var autonumberDigits = 0
88
91
if (i >= 0 ) {
89
92
val j = pattern.lastIndexOf(KNIT_AUTONUMBER_PLACEHOLDER )
90
93
autonumberDigits = j - i + 1
91
94
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"
93
96
}
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"
100
99
}
101
- pattern = pattern.substring(0 , i) + replacementRegex + pattern.substring(j + 1 )
100
+ pattern = pattern.substring(0 , i) + KNIT_AUTONUMBER_REGEX + pattern.substring(j + 1 )
102
101
}
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)
105
104
}
106
105
107
106
class KnitIncludeEnv (
108
107
val file : File ,
109
- props : KnitProps
108
+ props : KnitProps ,
109
+ knitName : String
110
110
) {
111
- val knit = props.getMap(" knit" )
111
+ val knit = props.getMap(" knit" ) + mapOf ( " name " to knitName)
112
112
}
113
113
114
- fun KnitConfig.loadMainInclude (file : File , props : KnitProps ): Include {
114
+ fun KnitConfig.loadMainInclude (file : File , props : KnitProps , knitName : String ): Include {
115
115
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 ))
117
117
include.lines + = " "
118
118
return include
119
119
}
120
120
121
+ data class KnitInfo (val pkg : String , val name : String )
122
+
121
123
fun knit (markdownFile : File ): Boolean {
122
124
println (" *** Reading $markdownFile " )
123
125
val props = markdownFile.findProps()
124
126
val knit = props.knitConfig()
125
- var knitAutonumberIndex = HashMap <String , Int >()
127
+ val knitAutonumberIndex = HashMap <String , Int >()
126
128
val tocLines = arrayListOf<String >()
127
129
val includes = arrayListOf<Include >()
128
130
val codeLines = arrayListOf<String >()
129
131
val testLines = arrayListOf<String >()
130
132
var testName: String? = props[TEST_NAME_PROP ]
131
133
val testOutLines = arrayListOf<String >()
132
- var lastPgk : String ? = null
134
+ var lastKnit : KnitInfo ? = null
133
135
val files = mutableSetOf<File >()
134
136
val allApiRefs = arrayListOf<ApiRef >()
135
137
val remainingApiRefNames = mutableSetOf<String >()
136
138
var moduleName: String by Delegates .notNull()
137
139
var docsRoot: String by Delegates .notNull()
138
140
var retryKnitLater = false
139
141
val tocRefs = ArrayList <TocRef >().also { tocRefMap[markdownFile] = it }
140
- // load main includes (if defined)
141
- knit?.loadMainInclude(markdownFile, props)?.let { includes + = it }
142
142
// read markdown file
143
143
val markdown = markdownFile.withMarkdownTextReader {
144
144
mainLoop@ while (true ) {
@@ -200,7 +200,7 @@ fun knit(markdownFile: File): Boolean {
200
200
testName = directive.param
201
201
}
202
202
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 " }
204
204
require(testName != null ) { " Neither $TEST_NAME_DIRECTIVE directive nor '$TEST_NAME_PROP 'property was specified" }
205
205
val predicate = directive.param
206
206
if (testLines.isEmpty()) {
@@ -211,7 +211,7 @@ fun knit(markdownFile: File): Boolean {
211
211
} else {
212
212
requireSingleLine(directive)
213
213
}
214
- makeTest(testOutLines, lastPgk !! , testLines, predicate)
214
+ makeTest(testOutLines, lastKnit !! , testLines, predicate)
215
215
testLines.clear()
216
216
}
217
217
MODULE_DIRECTIVE -> {
@@ -261,12 +261,13 @@ fun knit(markdownFile: File): Boolean {
261
261
}
262
262
}
263
263
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"
265
267
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 )
270
271
val index = knitAutonumberIndex.getOrElse(key) { 1 }
271
272
val num = index.toString().padStart(knit.autonumberDigits, ' 0' )
272
273
if (numGroup.value != num) { // update and retry with this line if a different number
@@ -277,25 +278,23 @@ fun knit(markdownFile: File): Boolean {
277
278
}
278
279
knitAutonumberIndex[key] = index + 1
279
280
}
280
- val file = File (markdownFile.parentFile, fileName )
281
+ val file = File (markdownFile.parentFile, path )
281
282
require(files.add(file)) { " Duplicate file: $file " }
282
283
println (" Knitting $file ..." )
283
284
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
293
291
if (outLines.last().isNotBlank()) outLines + = " "
294
292
for (code in codeLines) {
295
293
outLines + = code.replace(" System.currentTimeMillis()" , " currentTimeMillis()" )
296
294
}
297
295
codeLines.clear()
298
296
writeLinesIfNeeded(file, outLines)
297
+ lastKnit = KnitInfo (props.getValue(" knit.package" ), knitName)
299
298
}
300
299
}
301
300
} ? : return false // false when failed
@@ -326,24 +325,17 @@ fun knit(markdownFile: File): Boolean {
326
325
return true
327
326
}
328
327
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
+
329
331
data class TocRef (val levelPrefix : String , val name : String , val ref : String )
330
332
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()
343
335
testOutLines + = " "
344
336
testOutLines + = " @Test"
345
337
testOutLines + = " fun test$funName () {"
346
- val prefix = " test(\" $funName \" ) { $pgk .main() }"
338
+ val prefix = " test(\" $funName \" ) { ${knit.pkg} . ${knit.name} .main() }"
347
339
when (predicate) {
348
340
" " -> makeTestLines(testOutLines, prefix, " verifyLines" , test)
349
341
STARTS_WITH_PREDICATE -> makeTestLines(testOutLines, prefix, " verifyLinesStartWith" , test)
@@ -362,6 +354,18 @@ fun makeTest(testOutLines: MutableList<String>, pgk: String, test: List<String>,
362
354
testOutLines + = " }"
363
355
}
364
356
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
+
365
369
private fun makeTestLines (testOutLines : MutableList <String >, prefix : String , method : String , test : List <String >) {
366
370
testOutLines + = " $prefix .$method ("
367
371
for ((index, testLine) in test.withIndex()) {
@@ -372,15 +376,6 @@ private fun makeTestLines(testOutLines: MutableList<String>, prefix: String, met
372
376
testOutLines + = " )"
373
377
}
374
378
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
-
384
379
class TestTemplateEnv (
385
380
val file : File ,
386
381
props : KnitProps ,
0 commit comments