From 5f0552a7ea251a52fb89f7886f6a8dd48ce566d5 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Mon, 29 Apr 2024 19:22:32 +0200 Subject: Add basic html generator --- data/test.md | 9 -- index.html | 7 + src/main/kotlin/moe/nea/blog/gen/HtmlGenerator.kt | 149 +++++++++++++++++++++ src/test/kotlin/moe/nea/blog/gen/HtmlTest.kt | 56 ++++++++ .../kotlin/moe/nea/blog/md/test/TestItalics.kt | 1 + 5 files changed, 213 insertions(+), 9 deletions(-) delete mode 100644 data/test.md create mode 100644 index.html create mode 100644 src/main/kotlin/moe/nea/blog/gen/HtmlGenerator.kt create mode 100644 src/test/kotlin/moe/nea/blog/gen/HtmlTest.kt diff --git a/data/test.md b/data/test.md deleted file mode 100644 index ccd4932..0000000 --- a/data/test.md +++ /dev/null @@ -1,9 +0,0 @@ -# Hello World - -***lol* hehe** - -```java -public class ObjectControllerFactoryManagerProvider> { - -} -``` \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..0f09ab1 --- /dev/null +++ b/index.html @@ -0,0 +1,7 @@ +

Hello World

lol hehe

public class ObjectControllerFactoryManagerProvider<T extends ObjectControllerFactoryManager<T>> {
+
+}
+ + + + diff --git a/src/main/kotlin/moe/nea/blog/gen/HtmlGenerator.kt b/src/main/kotlin/moe/nea/blog/gen/HtmlGenerator.kt new file mode 100644 index 0000000..ef0858a --- /dev/null +++ b/src/main/kotlin/moe/nea/blog/gen/HtmlGenerator.kt @@ -0,0 +1,149 @@ +package moe.nea.blog.gen + +import moe.nea.blog.md.* +import kotlin.reflect.KClass + +fun interface HtmlFragmentGenerator { + fun generateHtml(htmlGenerator: HtmlGenerator, node: T): HtmlFragment +} + + +class HtmlFragment private constructor(val text: String) { + companion object { + fun ofUnescaped(text: String) = HtmlFragment( + text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'") + ) + + fun ofPreEscaped(text: String) = HtmlFragment(text) + fun empty(): HtmlFragment { + return HtmlFragment("") + } + } +} + +annotation class HtmlDslMarker + +@HtmlDslMarker +class HtmlDsl { + val parts = mutableListOf() + + fun append(string: String) { + parts.add(HtmlFragment.ofUnescaped(string)) + } + + fun append(fragment: HtmlFragment) { + parts.add(fragment) + } + + fun appendPreEscaped(markup: String) { + append(HtmlFragment.ofPreEscaped(markup)) + } + + operator fun String.unaryPlus() = append(this) + operator fun HtmlFragment.unaryPlus() = append(this) + + fun element(name: String, attributes: Map, block: HtmlDsl.() -> Unit) { + element(name, attributes, HtmlDsl().also(block).intoFragment()) + } + + fun element(name: String, attributes: Map, fragment: HtmlFragment) { + appendPreEscaped(("<")) + append(name) + + for ((key, value) in attributes) { + appendPreEscaped((" ")) + append(key) + appendPreEscaped(("=\"")) + append(value) + appendPreEscaped(("\"")) + } + + appendPreEscaped((">")) + + append(fragment) + + appendPreEscaped(("")) + } + + fun intoFragment(): HtmlFragment { + return HtmlFragment.ofPreEscaped(parts.joinToString("") { it.text }) + } +} + + +class HtmlGenerator { + private val generators = mutableMapOf, HtmlFragmentGenerator>() + + fun getGeneratorFor(kClass: KClass) = generators[kClass] as HtmlFragmentGenerator? + + inline fun registerFragmentGenerator(noinline outputter: HtmlDsl.(generator: HtmlGenerator, node: T) -> Unit) { + registerGeneratorFor(T::class) { gen, node -> + HtmlDsl() + .apply { outputter.invoke(this, gen, node) } + .intoFragment() + } + } + + inline fun registerGeneratorFor(outputter: HtmlFragmentGenerator) { + registerGeneratorFor(T::class, outputter) + } + + fun registerGeneratorFor(kClass: KClass, outputter: HtmlFragmentGenerator) { + generators[kClass] = outputter as HtmlFragmentGenerator + } + + fun registerDefaultGenerators() { + registerGeneratorFor { _, _ -> HtmlFragment.empty() } + registerFragmentGenerator
{ gen, header -> + element("h${header.level}", mapOf()) { + +header.text + } + } + registerFragmentGenerator { gen, node -> + element("b", mapOf(), gen.generateHtml(node.inner)) + } + registerFragmentGenerator { generator, node -> + element("em", mapOf(), generator.generateHtml(node.inner)) + } + registerFragmentGenerator { generator, node -> + element("a", mapOf("href" to node.target), generator.generateHtml(node.label ?: Begin())) + } + registerFragmentGenerator { generator, node -> + element("p", mapOf(), generator.generateHtml(node.format)) + } + registerFragmentGenerator { generator, node -> + element("pre", mapOf()) { + element("code", mapOf("class" to "language-${node.language}")) { + append(node.lines.joinToString("\n")) + } + } + } + registerFragmentGenerator { generator, node -> + for (markdownFormat in node.list) { + append(generator.generateHtml(markdownFormat)) + } + } + registerFragmentGenerator { generator, node -> + append(node.text) + } + registerFragmentGenerator { generator, node -> + append(" ") + } + registerFragmentGenerator { generator, node -> + for (markdownBlock in node.list) { + append(generator.generateHtml(markdownBlock)) + } + } + } + + fun generateHtml(node: T): HtmlFragment { + val gen = getGeneratorFor(node::class) ?: error("Missing html generator for $node") + return gen.generateHtml(this, node) + } +} \ No newline at end of file diff --git a/src/test/kotlin/moe/nea/blog/gen/HtmlTest.kt b/src/test/kotlin/moe/nea/blog/gen/HtmlTest.kt new file mode 100644 index 0000000..07c5a24 --- /dev/null +++ b/src/test/kotlin/moe/nea/blog/gen/HtmlTest.kt @@ -0,0 +1,56 @@ +package moe.nea.blog.gen + +import moe.nea.blog.md.* +import kotlin.test.Test +import kotlin.test.assertEquals + +class HtmlTest { + + fun assertGenerator(generatedHtml: String, markdown: MarkdownElement) { + val generator = HtmlGenerator() + generator.registerDefaultGenerators() + assertEquals(generatedHtml, generator.generateHtml(markdown).text) + } + + fun assertGeneratorMD(generatedHtml: String, text: String) { + val generator = HtmlGenerator() + generator.registerDefaultGenerators() + assertEquals(generatedHtml, generator.generateHtml(MarkdownParser(text).also { it.addDefaultParsers() }.readDocument()).text) + } + + @Test + fun testBiggerFile() { + assertGeneratorMD( + """ +

Hello World

lol hehe

public class ObjectControllerFactoryManagerProvider<T extends ObjectControllerFactoryManager<T>> {
+                    
+                }
+ """.trimIndent(), + """ + # Hello World + + ***lol* hehe** + + ```java + public class ObjectControllerFactoryManagerProvider> { + + } + ``` + """.trimIndent() + ) + } + + @Test + fun testBold() { + assertGenerator("Hii", Bold(Word("Hii"))) + } + + @Test + fun testParagraphs() { + assertGenerator( + "

Test Whatever

", + Paragraph(FormatSequence(Word("Test"), Whitespace(), Bold(Word("Whatever")))) + ) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/moe/nea/blog/md/test/TestItalics.kt b/src/test/kotlin/moe/nea/blog/md/test/TestItalics.kt index 01bd49e..939ad16 100644 --- a/src/test/kotlin/moe/nea/blog/md/test/TestItalics.kt +++ b/src/test/kotlin/moe/nea/blog/md/test/TestItalics.kt @@ -64,6 +64,7 @@ class TestItalics : MarkdownTest() { @Test fun testFreestandingStarInItalics() { assertInlineFormat("left * right", "*left * right*") + assertInlineFormat("left * middle ** later", "left * middle ** later") } @Test -- cgit