blob: 71a9cc26e4e1b6b829225ddb52b0b70da1b267ff [file] [log] [blame]
/*
* Copyright 2016-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.io.File
import java.io.IOException
import java.io.LineNumberReader
import java.io.Reader
val DIRECTIVE_START = "<!--- "
val DIRECTIVE_END = "-->"
val TOC_DIRECTIVE = "TOC"
val KNIT_DIRECTIVE = "KNIT"
val INCLUDE_DIRECTIVE = "INCLUDE"
val CLEAR_DIRECTIVE = "CLEAR"
val CODE_START = "```kotlin"
val CODE_END = "```"
val SECTION_START = "##"
fun main(args: Array<String>) {
if(args.size != 1) {
println("Usage: Knit <markdown-file>")
return
}
val markdownFile = File(args[0])
val toc = arrayListOf<String>()
var knitRegex: Regex? = null
val includes = arrayListOf<Include>()
val code = arrayListOf<String>()
val files = mutableSetOf<String>()
// read markdown file
val markdown = markdownFile.withMarkdownTextReader {
mainLoop@ while (true) {
val inLine = readLine() ?: break
val directive = directive(inLine)
if (directive != null && markdownPart == MarkdownPart.TOC) {
markdownPart = MarkdownPart.POST_TOC
postTocText += inLine
}
when (directive?.name) {
TOC_DIRECTIVE -> {
require(directive.singleLine) { "TOC directive must end on the same line with '$DIRECTIVE_END'" }
require(directive.param.isEmpty()) { "TOC directive must not have parameters" }
require(markdownPart == MarkdownPart.PRE_TOC) { "Only one TOC directive is supported" }
markdownPart = MarkdownPart.TOC
}
KNIT_DIRECTIVE -> {
require(directive.singleLine) { "KNIT directive must end on the same line with '$DIRECTIVE_END'" }
require(!directive.param.isEmpty()) { "KNIT directive must include regex parameter" }
require(knitRegex == null) { "Only one KNIT directive is supported"}
knitRegex = Regex("\\((" + directive.param + ")\\)")
continue@mainLoop
}
INCLUDE_DIRECTIVE -> {
require(!directive.param.isEmpty()) { "INCLUDE directive must include regex parameter" }
val include = Include(Regex(directive.param))
if (directive.singleLine) {
include.lines += code
code.clear()
} else {
while (true) {
val includeLine = readLine() ?: break
if (includeLine.startsWith(DIRECTIVE_END)) break
include.lines += includeLine
}
}
includes += include
continue@mainLoop
}
CLEAR_DIRECTIVE -> {
require(directive.singleLine) { "CLEAR directive must end on the same line with '$DIRECTIVE_END'" }
require(directive.param.isEmpty()) { "CLEAR directive must not have parameters" }
code.clear()
continue@mainLoop
}
}
if (inLine.startsWith(CODE_START)) {
code += ""
while (true) {
val codeLine = readLine() ?: break
if (codeLine.startsWith(CODE_END)) break
code += codeLine
}
continue@mainLoop
}
if (inLine.startsWith(SECTION_START) && markdownPart == MarkdownPart.POST_TOC) {
val i = inLine.indexOf(' ')
require(i >= 2) { "Invalid section start" }
val name = inLine.substring(i + 1).trim()
toc += " ".repeat(i - 2) + "* [$name](#${makeSectionRef(name)})"
continue@mainLoop
}
knitRegex?.find(inLine)?.let { knitMatch ->
val fileName = knitMatch.groups[1]!!.value
require(files.add(fileName)) { "Duplicate file name: $fileName"}
println("Knitting $fileName ...")
val outLines = arrayListOf<String>()
for (include in includes) {
val includeMatch = include.regex.matchEntire(fileName) ?: continue
include.lines.forEach { includeLine ->
var toOutLine = includeLine
for ((id, group) in includeMatch.groups.withIndex()) {
if (group != null)
toOutLine = toOutLine.replace("\$\$$id", group.value)
}
outLines += toOutLine
}
}
outLines += code
code.clear()
val file = File(fileName)
val oldLines = try { file.readLines() } catch (e: IOException) { emptyList<String>() }
if (outLines != oldLines) writeLines(file, outLines)
}
}
}
// update markdown file with toc
val newLines = markdown.preTocText + "" + toc + "" + markdown.postTocText
if (newLines != markdown.allText) writeLines(markdownFile, newLines)
}
fun makeSectionRef(name: String): String = name.replace(' ', '-').replace(".", "").toLowerCase()
class Include(val regex: Regex, val lines: MutableList<String> = arrayListOf())
class Directive(
val name: String,
val param: String,
val singleLine: Boolean
)
fun directive(line: String): Directive? {
if (!line.startsWith(DIRECTIVE_START)) return null
var s = line.substring(DIRECTIVE_START.length).trim()
val singleLine = s.endsWith(DIRECTIVE_END)
if (singleLine) s = s.substring(0, s.length - DIRECTIVE_END.length)
val i = s.indexOf(' ')
val name = if (i < 0) s else s.substring(0, i)
val param = if (i < 0) "" else s.substring(i).trim()
return Directive(name, param, singleLine)
}
enum class MarkdownPart { PRE_TOC, TOC, POST_TOC }
class MarkdownTextReader(r: Reader) : LineNumberReader(r) {
val allText = arrayListOf<String>()
val preTocText = arrayListOf<String>()
val postTocText = arrayListOf<String>()
var markdownPart: MarkdownPart = MarkdownPart.PRE_TOC
override fun readLine(): String? {
val line = super.readLine() ?: return null
allText += line
when (markdownPart) {
MarkdownPart.PRE_TOC -> preTocText += line
MarkdownPart.POST_TOC -> postTocText += line
MarkdownPart.TOC -> {} // do nothing
}
return line
}
}
fun File.withMarkdownTextReader(block: MarkdownTextReader.() -> Unit): MarkdownTextReader {
MarkdownTextReader(reader()).use {
try {
it.block()
} catch (e: IllegalArgumentException) {
println("ERROR: $this: ${it.lineNumber}: ${e.message}")
}
return it
}
}
private fun writeLines(file: File, lines: List<String>) {
println(" Writing $file ...")
file.parentFile?.mkdirs()
file.printWriter().use { out ->
lines.forEach { out.println(it) }
}
}