84 lines
2.9 KiB
Kotlin

package be.simplenotes.domain.usecases.markdown
import arrow.core.Either
import arrow.core.extensions.fx
import arrow.core.left
import arrow.core.right
import be.simplenotes.domain.model.NoteMetadata
import be.simplenotes.domain.validation.NoteValidations
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
import com.vladsch.flexmark.html.HtmlRenderer
import com.vladsch.flexmark.parser.Parser
import com.vladsch.flexmark.util.data.MutableDataSet
import io.konform.validation.ValidationErrors
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.parser.ParserException
import org.yaml.snakeyaml.scanner.ScannerException
sealed class MarkdownParsingError
object MissingMeta : MarkdownParsingError()
object InvalidMeta : MarkdownParsingError()
class ValidationError(val validationErrors: ValidationErrors) : MarkdownParsingError()
data class Document(val metadata: NoteMetadata, val html: String)
typealias MetaMdPair = Pair<String, String>
interface MarkdownConverter {
fun renderDocument(input: String): Either<MarkdownParsingError, Document>
}
internal class MarkdownConverterImpl : MarkdownConverter {
private val yamlBoundPattern = "-{3}".toRegex()
private fun splitMetaFromDocument(input: String): Either<MissingMeta, MetaMdPair> {
val split = input.split(yamlBoundPattern, 3)
if (split.size < 3) return MissingMeta.left()
return (split[1].trim() to split[2].trim()).right()
}
private val yaml = Yaml()
private fun parseMeta(input: String): Either<InvalidMeta, NoteMetadata> {
val load: Map<String, Any> = try {
yaml.load(input)
} catch (e: ParserException) {
return InvalidMeta.left()
} catch (e: ScannerException) {
return InvalidMeta.left()
}
val title = when (val titleNode = load["title"]) {
is String, is Number -> titleNode.toString()
else -> return InvalidMeta.left()
}
val tagsNode = load["tags"]
val tags = if (tagsNode !is List<*>)
emptyList()
else
tagsNode.map { it.toString() }
return NoteMetadata(title, tags).right()
}
private val parser: Parser
private val renderer: HtmlRenderer
init {
val options = MutableDataSet()
options.set(Parser.EXTENSIONS, listOf(TaskListExtension.create()))
options.set(HtmlRenderer.SOFT_BREAK, "<br>")
parser = Parser.builder(options).build()
renderer = HtmlRenderer.builder(options).build()
}
private fun renderMarkdown(markdown: String) = parser.parse(markdown).run(renderer::render)
override fun renderDocument(input: String) = Either.fx<MarkdownParsingError, Document> {
val (meta, md) = !splitMetaFromDocument(input)
val parsedMeta = !parseMeta(meta)
!NoteValidations.validateMetadata(parsedMeta).toEither { }.swap()
val html = renderMarkdown(md)
Document(parsedMeta, html)
}
}