Skip to content

Commit

Permalink
Add quick reference generation task
Browse files Browse the repository at this point in the history
  • Loading branch information
alllex committed Jul 27, 2023
1 parent fe27c7f commit e2e825d
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 0 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ There are, however, no pros without cons. Parsus relies heavily on coroutines ma
performance and memory overhead as compared to other techniques such as generating parsers at compile-time from special
grammar formats.

## Quick Reference

| Description | Procedural Grammar | Combinator Grammar | Parsing |
| ----------- | ------------------ | ------------------ | ------- |
| Parsing token and getting its text | <pre> val ab by regexToken("a[bB]")<br/> override val root by parser {<br/> val abMatch = ab()<br/> abMatch.text<br/> }</pre> | <pre> val ab by regexToken("a[bB]")<br/> override val root by ab map { it.text }</pre> | `ab` => (`ab`)<br/>`aB` => (`aB`) |
| Parsing two tokens sequentially | <pre> val a by literalToken("a")<br/> val b by regexToken("[bB]")<br/> override val root by parser {<br/> val aMatch = a()<br/> val bMatch = b()<br/> aMatch.text to bMatch.text<br/> }</pre> | <pre> val a by literalToken("a")<br/> val b by regexToken("[bB]")<br/> override val root by a and b map<br/> { (aM, bM) -> aM.text to bM.text }</pre> | `ab` => (`a`, `b`)<br/>`aB` => (`a`, `B`) |
| Parsing one of two tokens | <pre> val a by literalToken("a")<br/> val b by regexToken("[bB]")<br/> override val root by parser {<br/> val abMatch = choose(a, b)<br/> abMatch.text<br/> }</pre> | <pre> val a by literalToken("a")<br/> val b by regexToken("[bB]")<br/> override val root by a or b map { it.text }</pre> | `a` => (`a`)<br/>`b` => (`b`)<br/>`B` => (`B`) |
| Parsing an optional token | <pre> val a by literalToken("a")<br/> val b by regexToken("[bB]")<br/> override val root by parser {<br/> val aMatch = poll(a)<br/> val bMatch = b()<br/> aMatch?.text to bMatch.text<br/> }</pre> | <pre> val a by literalToken("a")<br/> val b by regexToken("[bB]")<br/> override val root by optional(a) and b map<br/> { (aM, bM) -> aM?.text to bM.text }</pre> | `ab` => (`a`, `b`)<br/>`aB` => (`a`, `B`)<br/>`b` => (null, `b`)<br/>`B` => (null, `B`) |
| Parsing a token and ignoring its value | <pre> val a by literalToken("a")<br/> val b by regexToken("[bB]")<br/> override val root by parser {<br/> skip(a) // or just a() without using the value<br/> val bMatch = b()<br/> bMatch.text<br/> }</pre> | <pre> val a by literalToken("a")<br/> val b by regexToken("[bB]")<br/> override val root by -a * b map { it.text }</pre> | `ab` => (`b`)<br/>`aB` => (`B`) |

## Introduction

The goal of a grammar is to define rules by which to turn an input string of characters into a structured value. This
Expand Down
5 changes: 5 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,8 @@ nexusPublishing {
}
}
}

tasks.register<GenerateQuickReferenceMarkdown>("generateQuickRef") {
kotlinTestSource = project.layout.projectDirectory.file("src/commonTest/kotlin/me/alllex/parsus/ReadmeTests.kt")
markdownOutput = project.layout.buildDirectory.file("quickref.md")
}
115 changes: 115 additions & 0 deletions buildSrc/src/main/kotlin/GenerateQuickReferenceMarkdown.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.*

@CacheableTask
abstract class GenerateQuickReferenceMarkdown : DefaultTask() {

@get:[InputFile PathSensitive(PathSensitivity.NONE)]
abstract val kotlinTestSource: RegularFileProperty

@get:OutputFile
abstract val markdownOutput: RegularFileProperty

@TaskAction
fun generate() {
val input = kotlinTestSource.get().asFile.readText()
val output = markdownOutput.get().asFile
output.writeText(generateMarkdown(input))
}

private fun generateMarkdown(input: String): String {
val entries = parseSource(input)
return buildString {
appendLine("| Description | Procedural Grammar | Combinator Grammar | Parsing |")
appendLine("| ----------- | ------------------ | ------------------ | ------- |")
for (entry in entries) {
append("| ")
append(entry.description)
append(" | ")
append(entry.proceduralGrammar.toMultilineMarkdownCodeBlock())
append(" | ")
append(entry.combinatorGrammar.toMultilineMarkdownCodeBlock())
append(" | ")
append(entry.testCases.joinToString("<br/>"))
appendLine(" |")
}
}
}

private fun List<String>.toMultilineMarkdownCodeBlock(): String {
// TODO: syntax highlight
return joinToString("<br/>", prefix = "<pre>", postfix = "</pre>")
}

private data class QuickRefEntry(
val description: String,
val proceduralGrammar: List<String>,
val combinatorGrammar: List<String>,
val testCases: List<String>,
)

private fun <T> Iterator<T>.skipUntil(predicate: (T) -> Boolean): T? {
while (hasNext()) {
val v = next()
if (predicate(v)) return v
}

return null
}

private fun <T> Iterator<T>.collectUntil(predicate: (T) -> Boolean): List<T> {
val result = mutableListOf<T>()
while (hasNext()) {
val v = next()
if (predicate(v)) break
result += v
}

return result
}

private fun parseSource(input: String): List<QuickRefEntry> {
val entries = mutableListOf<QuickRefEntry>()
val linesIter = input.lines().iterator()
while (linesIter.hasNext()) {
// Find the next test function whose name starts with `quickRef`
linesIter.skipUntil { it.trim().startsWith("fun quickRef") } ?: break

// Extract the first line-comment from the function body
val descriptionLine = linesIter.next().trim()
check(descriptionLine.startsWith("//")) { "Expected a // comment with description, got: $descriptionLine" }
val description = descriptionLine.removePrefix("//").trim()

// Find the `testCases` and extract them
linesIter.skipUntil { it.trim().startsWith("val testCases") } ?: break

val testCaseLines = linesIter.collectUntil { it.trim().startsWith(")") }

val testCases = testCaseLines.asSequence()
.map { it.trim().removeSuffix(",") }
.map { it.replace("\"", "`") }
.map { it.replaceFirst(" to ", " => ") }
.map { it.replace(" to ", ", ") }

// Find the procedural grammar and extract it
linesIter.skipUntil { it.trim().startsWith("val proc") } ?: break
val proceduralGrammarLines = linesIter.collectUntil { it.startsWith(" }") }

// Find the combinator grammar and extract it
linesIter.skipUntil { it.trim().startsWith("val comb") } ?: break
val combinatorGrammarLines = linesIter.collectUntil { it.startsWith(" }") }

// Add the entry
entries += QuickRefEntry(
description = description,
proceduralGrammar = proceduralGrammarLines,
combinatorGrammar = combinatorGrammarLines,
testCases = testCases.toList(),
)
}

return entries
}

}

0 comments on commit e2e825d

Please sign in to comment.