summaryrefslogtreecommitdiff
path: root/src/main/kotlin/moe
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-05-02 23:32:11 +0200
committerLinnea Gräf <nea@nea.moe>2024-05-02 23:42:15 +0200
commit7e24b934bbd424669a1c935894dd8799d4db0348 (patch)
treeea17a94215768ed6dea8a306a0bd58dfd936fb5f /src/main/kotlin/moe
parentebe41fbbd8b537b3008c5c52d2c9a496694ea281 (diff)
downloadblog-infra-7e24b934bbd424669a1c935894dd8799d4db0348.tar.gz
blog-infra-7e24b934bbd424669a1c935894dd8799d4db0348.tar.bz2
blog-infra-7e24b934bbd424669a1c935894dd8799d4db0348.zip
Add note blocks and an integration test
Diffstat (limited to 'src/main/kotlin/moe')
-rw-r--r--src/main/kotlin/moe/nea/blog/gen/HtmlFragmentGenerator.kt10
-rw-r--r--src/main/kotlin/moe/nea/blog/gen/MD2HtmlGenerator.kt10
-rw-r--r--src/main/kotlin/moe/nea/blog/md/MarkdownParser.kt374
-rw-r--r--src/main/kotlin/moe/nea/blog/md/ext/NoteBlocks.kt57
4 files changed, 267 insertions, 184 deletions
diff --git a/src/main/kotlin/moe/nea/blog/gen/HtmlFragmentGenerator.kt b/src/main/kotlin/moe/nea/blog/gen/HtmlFragmentGenerator.kt
index 3ba4f79..7702b75 100644
--- a/src/main/kotlin/moe/nea/blog/gen/HtmlFragmentGenerator.kt
+++ b/src/main/kotlin/moe/nea/blog/gen/HtmlFragmentGenerator.kt
@@ -4,4 +4,12 @@ import moe.nea.blog.md.MarkdownElement
fun interface HtmlFragmentGenerator<T : MarkdownElement> {
fun generateHtml(htmlGenerator: MD2HtmlGenerator, node: T): HtmlFragment
-} \ No newline at end of file
+}
+
+abstract class DefHtmlFragmentGenerator<T : MarkdownElement> : HtmlFragmentGenerator<T> {
+ override fun generateHtml(htmlGenerator: MD2HtmlGenerator, node: T): HtmlFragment {
+ return HtmlDsl().apply { makeHtml(htmlGenerator, node) }.intoFragment()
+ }
+
+ abstract fun HtmlDsl.makeHtml(generator: MD2HtmlGenerator, node: T)
+}
diff --git a/src/main/kotlin/moe/nea/blog/gen/MD2HtmlGenerator.kt b/src/main/kotlin/moe/nea/blog/gen/MD2HtmlGenerator.kt
index 0077469..9c9299e 100644
--- a/src/main/kotlin/moe/nea/blog/gen/MD2HtmlGenerator.kt
+++ b/src/main/kotlin/moe/nea/blog/gen/MD2HtmlGenerator.kt
@@ -8,6 +8,7 @@ import moe.nea.blog.md.FormatSequence
import moe.nea.blog.md.Header
import moe.nea.blog.md.Italics
import moe.nea.blog.md.Link
+import moe.nea.blog.md.MDList
import moe.nea.blog.md.MarkdownElement
import moe.nea.blog.md.Paragraph
import moe.nea.blog.md.Whitespace
@@ -49,6 +50,15 @@ class MD2HtmlGenerator {
registerFragmentGenerator<Italics> { generator, node ->
element("em", mapOf(), generator.generateHtml(node.inner))
}
+ registerFragmentGenerator<MDList> { generator, node ->
+ element("ul", mapOf()) {
+ for (item in node.elements) {
+ element("li", mapOf()) {
+ +generator.generateHtml(item)
+ }
+ }
+ }
+ }
registerFragmentGenerator<Link> { generator, node ->
element("a", mapOf("href" to node.target), generator.generateHtml(node.label ?: Begin()))
}
diff --git a/src/main/kotlin/moe/nea/blog/md/MarkdownParser.kt b/src/main/kotlin/moe/nea/blog/md/MarkdownParser.kt
index 11aff14..673a0ed 100644
--- a/src/main/kotlin/moe/nea/blog/md/MarkdownParser.kt
+++ b/src/main/kotlin/moe/nea/blog/md/MarkdownParser.kt
@@ -1,191 +1,199 @@
package moe.nea.blog.md
import moe.nea.blog.util.indentSize
-import java.util.*
+import java.util.Stack
class MarkdownParser(source: String) {
- private val lines = source.lines()
- private var lineIndex = 0
- private var blockIndents = 0
- private val indentStack = Stack<Int>()
-
- private val blockParsers = mutableListOf<BlockParser>()
- private val inlineParsers = mutableListOf<InlineParser>()
- private val preprocessors = Stack<LinePreProcessor>()
- private var peekedLine: String? = null
-
- fun findParserFor(line: String): BlockParser? {
- return blockParsers.filter { it.detect(line) }
- .maxByOrNull { it.prio }
- }
-
- fun readChildBlock(): MarkdownBlock? {
- val peek = peekLine() ?: return null
- val blockParser = findParserFor(peek) ?: ParagraphParser
- return blockParser.parse(this)
- }
-
- fun pushPreProcessor(preProcessor: LinePreProcessor) {
- peekedLine = null
- preprocessors.push(preProcessor)
- }
-
- fun popPreProcessor() {
- peekedLine = null
- preprocessors.pop()
- }
-
- fun preProcessLine(string: String): String? {
- var acc = string
- for (processor in preprocessors) {
- acc = processor.preprocess(lineIndex, acc) ?: return null
- }
- return acc
- }
-
- fun pushIndent(newIndent: Int) {
- indentStack.push(blockIndents)
- blockIndents += newIndent
- peekedLine = null
- }
-
- fun popIndent() {
- blockIndents = indentStack.pop()
- peekedLine = null
- }
-
- fun unpeekLine() {
- peekedLine = null
- }
-
- fun consumeLine(): String? {
- val line = peekLine()
- if (line != null) {
- peekedLine = null
- lineIndex++
- }
- return line
- }
-
- fun peekLine(): String? {
- if (lineIndex !in lines.indices) return null
- val line = peekedLine ?: preProcessLine(lines[lineIndex]) ?: return null
- peekedLine = line
- val indent = line.indentSize()
- if (indent != null && indent < blockIndents) {
- peekedLine = null
- return null
- }
- return line.drop(blockIndents)
- }
-
- fun parseInlineTextUntil(
- text: String,
- initialLookback: MarkdownFormat,
- breakout: (lookback: MarkdownFormat, remaining: String) -> Boolean
- ): Pair<List<MarkdownFormat>, String> {
- val seq = mutableListOf<MarkdownFormat>()
- var remaining = text
- var lastToken: MarkdownFormat = initialLookback
- while (remaining.isNotEmpty()) {
- if (breakout(lastToken, remaining))
- break
- val (tok, next) = parseInlineTextOnce(lastToken, remaining)
- seq.add(tok)
- lastToken = tok
- remaining = next
- }
- return seq to remaining
- }
-
- fun parseInlineTextOnce(lookback: MarkdownFormat, text: String): Pair<MarkdownFormat, String> {
- require(text.isNotEmpty()) // TODO handle empty string
- val parser = inlineParsers.find { it.detect(lookback, text) }
- if (parser != null) {
- return parser.parse(this, text)
- }
- if (text[0] == ' ') {
- return Pair(Whitespace(), text.substring(1))
- }
- val nextSpecial = text.indexOfFirst { it in inlineParsers.flatMap { it.specialSyntax } || it == ' ' }
- if (nextSpecial == 0) {
- return Pair(Word(text.substring(0, 1)), text.substring(1))
- }
- if (nextSpecial == -1) {
- return Pair(Word(text), "")
- }
- return Pair(Word(text.substring(0, nextSpecial)), text.substring(nextSpecial))
- }
-
- fun parseInlineText(text: String): MarkdownFormat {
- val (tokens, rest) = parseInlineTextUntil(text, Begin()) { _, _ -> false }
- require(rest.isEmpty())
- return collapseInlineFormat(tokens, true)
- }
-
- private fun expandMarkdownFormats(sequence: List<MarkdownFormat>): List<MarkdownFormat> {
- val elongated = mutableListOf<MarkdownFormat>()
- for (markdownFormat in sequence) {
- if (markdownFormat is FormatSequence) {
- elongated.addAll(expandMarkdownFormats(markdownFormat.list))
- } else {
- elongated.add(markdownFormat)
- }
- }
- return elongated
- }
-
- private fun collapseMarkdownFormats(
- sequence: List<MarkdownFormat>,
- trimWhitespace: Boolean
- ): MutableList<MarkdownFormat> {
- val shortened = mutableListOf<MarkdownFormat>()
- var last: MarkdownFormat = if (trimWhitespace) Whitespace() else Begin()
- for (format in sequence) {
- if (format is Whitespace && last is Whitespace) {
- continue
- }
- last = format
- shortened.add(format)
- }
- return shortened
- }
-
- fun collapseInlineFormat(sequence: List<MarkdownFormat>, trimWhitespace: Boolean): MarkdownFormat {
- val formats = collapseMarkdownFormats(expandMarkdownFormats(sequence), trimWhitespace)
- return formats.singleOrNull() ?: FormatSequence(formats)
- }
-
- fun readDocument(): Document {
- val list = mutableListOf<MarkdownBlock>()
- while (true) {
- val block = readChildBlock() ?: break
- list.add(block)
- }
- return Document(list)
- }
-
- fun addDefaultParsers() {
- blockParsers.add(CodeBlockParser)
- blockParsers.add(HeaderParser)
- blockParsers.add(ListParser)
- blockParsers.add(BlockQuoteParser)
- inlineParsers.add(ItalicsParser)
- inlineParsers.add(LinkParser)
- inlineParsers.add(ImageParser)
- }
-
- fun getLineIndex(): Int {
- return lineIndex
- }
-
- fun mergeBlocks(elements: List<MarkdownBlock>): MarkdownBlock {
- return elements.singleOrNull() ?: BlockList(elements)
- }
-
- fun getIndent(): Int {
- return blockIndents
- }
+ private val lines = source.lines()
+ private var lineIndex = 0
+ private var blockIndents = 0
+ private val indentStack = Stack<Int>()
+
+ private val blockParsers = mutableListOf<BlockParser>()
+ private val inlineParsers = mutableListOf<InlineParser>()
+ private val preprocessors = Stack<LinePreProcessor>()
+ private var peekedLine: String? = null
+
+ fun findParserFor(line: String): BlockParser? {
+ return blockParsers.filter { it.detect(line) }
+ .maxByOrNull { it.prio }
+ }
+
+ fun readChildBlock(): MarkdownBlock? {
+ val peek = peekLine() ?: return null
+ val blockParser = findParserFor(peek) ?: ParagraphParser
+ return blockParser.parse(this)
+ }
+
+ fun pushPreProcessor(preProcessor: LinePreProcessor) {
+ peekedLine = null
+ preprocessors.push(preProcessor)
+ }
+
+ fun popPreProcessor() {
+ peekedLine = null
+ preprocessors.pop()
+ }
+
+ fun preProcessLine(string: String): String? {
+ var acc = string
+ for (processor in preprocessors) {
+ acc = processor.preprocess(lineIndex, acc) ?: return null
+ }
+ return acc
+ }
+
+ fun pushIndent(newIndent: Int) {
+ indentStack.push(blockIndents)
+ blockIndents += newIndent
+ peekedLine = null
+ }
+
+ fun popIndent() {
+ blockIndents = indentStack.pop()
+ peekedLine = null
+ }
+
+ fun unpeekLine() {
+ peekedLine = null
+ }
+
+ fun consumeLine(): String? {
+ val line = peekLine()
+ if (line != null) {
+ peekedLine = null
+ lineIndex++
+ }
+ return line
+ }
+
+ fun peekLine(): String? {
+ if (lineIndex !in lines.indices) return null
+ val line = peekedLine ?: preProcessLine(lines[lineIndex]) ?: return null
+ peekedLine = line
+ val indent = line.indentSize()
+ if (indent != null && indent < blockIndents) {
+ peekedLine = null
+ return null
+ }
+ return line.drop(blockIndents)
+ }
+
+ fun parseInlineTextUntil(
+ text: String,
+ initialLookback: MarkdownFormat,
+ breakout: (lookback: MarkdownFormat, remaining: String) -> Boolean
+ ): Pair<List<MarkdownFormat>, String> {
+ val seq = mutableListOf<MarkdownFormat>()
+ var remaining = text
+ var lastToken: MarkdownFormat = initialLookback
+ while (remaining.isNotEmpty()) {
+ if (breakout(lastToken, remaining))
+ break
+ val (tok, next) = parseInlineTextOnce(lastToken, remaining)
+ seq.add(tok)
+ lastToken = tok
+ remaining = next
+ }
+ return seq to remaining
+ }
+
+ fun parseInlineTextOnce(lookback: MarkdownFormat, text: String): Pair<MarkdownFormat, String> {
+ require(text.isNotEmpty()) // TODO handle empty string
+ val parser = inlineParsers.find { it.detect(lookback, text) }
+ if (parser != null) {
+ return parser.parse(this, text)
+ }
+ if (text[0] == ' ') {
+ return Pair(Whitespace(), text.substring(1))
+ }
+ val nextSpecial = text.indexOfFirst { it in inlineParsers.flatMap { it.specialSyntax } || it == ' ' }
+ if (nextSpecial == 0) {
+ return Pair(Word(text.substring(0, 1)), text.substring(1))
+ }
+ if (nextSpecial == -1) {
+ return Pair(Word(text), "")
+ }
+ return Pair(Word(text.substring(0, nextSpecial)), text.substring(nextSpecial))
+ }
+
+ fun parseInlineText(text: String): MarkdownFormat {
+ val (tokens, rest) = parseInlineTextUntil(text, Begin()) { _, _ -> false }
+ require(rest.isEmpty())
+ return collapseInlineFormat(tokens, true)
+ }
+
+ private fun expandMarkdownFormats(sequence: List<MarkdownFormat>): List<MarkdownFormat> {
+ val elongated = mutableListOf<MarkdownFormat>()
+ for (markdownFormat in sequence) {
+ if (markdownFormat is FormatSequence) {
+ elongated.addAll(expandMarkdownFormats(markdownFormat.list))
+ } else {
+ elongated.add(markdownFormat)
+ }
+ }
+ return elongated
+ }
+
+ private fun collapseMarkdownFormats(
+ sequence: List<MarkdownFormat>,
+ trimWhitespace: Boolean
+ ): MutableList<MarkdownFormat> {
+ val shortened = mutableListOf<MarkdownFormat>()
+ var last: MarkdownFormat = if (trimWhitespace) Whitespace() else Begin()
+ for (format in sequence) {
+ if (format is Whitespace && last is Whitespace) {
+ continue
+ }
+ last = format
+ shortened.add(format)
+ }
+ return shortened
+ }
+
+ fun collapseInlineFormat(sequence: List<MarkdownFormat>, trimWhitespace: Boolean): MarkdownFormat {
+ val formats = collapseMarkdownFormats(expandMarkdownFormats(sequence), trimWhitespace)
+ return formats.singleOrNull() ?: FormatSequence(formats)
+ }
+
+ fun readDocument(): Document {
+ val list = mutableListOf<MarkdownBlock>()
+ while (true) {
+ val block = readChildBlock() ?: break
+ list.add(block)
+ }
+ return Document(list)
+ }
+
+ fun addDefaultParsers() {
+ blockParsers.add(CodeBlockParser)
+ blockParsers.add(HeaderParser)
+ blockParsers.add(ListParser)
+ blockParsers.add(BlockQuoteParser)
+ inlineParsers.add(ItalicsParser)
+ inlineParsers.add(LinkParser)
+ inlineParsers.add(ImageParser)
+ }
+
+ fun getLineIndex(): Int {
+ return lineIndex
+ }
+
+ fun mergeBlocks(elements: List<MarkdownBlock>): MarkdownBlock {
+ return elements.singleOrNull() ?: BlockList(elements)
+ }
+
+ fun getIndent(): Int {
+ return blockIndents
+ }
+
+ fun addParser(blockParser: BlockParser) {
+ this.blockParsers.add(blockParser)
+ }
+
+ fun addParser(formatParser: InlineParser) {
+ this.inlineParsers.add(formatParser)
+ }
}
diff --git a/src/main/kotlin/moe/nea/blog/md/ext/NoteBlocks.kt b/src/main/kotlin/moe/nea/blog/md/ext/NoteBlocks.kt
new file mode 100644
index 0000000..a1f977f
--- /dev/null
+++ b/src/main/kotlin/moe/nea/blog/md/ext/NoteBlocks.kt
@@ -0,0 +1,57 @@
+package moe.nea.blog.md.ext
+
+import moe.nea.blog.gen.DefHtmlFragmentGenerator
+import moe.nea.blog.gen.HtmlDsl
+import moe.nea.blog.gen.MD2HtmlGenerator
+import moe.nea.blog.md.BlockParser
+import moe.nea.blog.md.MarkdownBlock
+import moe.nea.blog.md.MarkdownParser
+import moe.nea.blog.util.indent
+import moe.nea.blog.util.indentSize
+import java.io.PrintStream
+
+data class NoteBlock(val noteType: String, val child: MarkdownBlock) : MarkdownBlock {
+ override fun debugFormat(indent: Int, printStream: PrintStream) {
+ printStream.indent(indent)
+ printStream.println("<note type=\"$noteType\">")
+
+ child.debugFormat(indent + 2, printStream)
+
+ printStream.indent(indent)
+ printStream.println("</note>")
+ }
+}
+
+object NoteBlockGenerator : DefHtmlFragmentGenerator<NoteBlock>() {
+ override fun HtmlDsl.makeHtml(generator: MD2HtmlGenerator, node: NoteBlock) {
+ element("div", mapOf("class" to "note-${node.noteType}"), generator.generateHtml(node.child))
+ }
+}
+
+object NoteBlockParser : BlockParser {
+ override fun detect(line: String): Boolean {
+ return line.startsWith("!!!")
+ }
+
+ override fun parse(parser: MarkdownParser): MarkdownBlock {
+ val line = parser.consumeLine()!!
+ val noteType = line.substring(3).trim()
+ while ((parser.peekLine() ?: error("Unterminated !!! note")).isBlank()) {
+ parser.consumeLine()
+ }
+ val indent = parser.peekLine()!!.indentSize()!!
+ require(indent > 0) { "!!! note not indented" }
+ parser.pushIndent(indent)
+ val list = mutableListOf<MarkdownBlock>()
+ while (true) {
+ val block = parser.readChildBlock() ?: break
+ list.add(block)
+ }
+ parser.popIndent()
+ return NoteBlock(noteType, parser.mergeBlocks(list))
+ }
+
+ override val prio: Int
+ get() = 10
+
+}