From 32596228feb2e18a27feda2ef6442f3f67618a4e Mon Sep 17 00:00:00 2001 From: Lars Westermann Date: Mon, 27 May 2019 14:45:34 +0200 Subject: [PATCH] Add live preview for posts --- build.gradle | 2 - src/jsMain/kotlin/de/kif/frontend/main.kt | 4 + .../kif/frontend/repository/PostRepository.kt | 4 + .../frontend/views/overview/OverviewMain.kt | 23 ++ src/jsMain/resources/style/style.scss | 46 +++- .../kotlin/de/kif/backend/Application.kt | 2 - src/jvmMain/kotlin/de/kif/backend/Main.kt | 2 - .../kotlin/de/kif/backend/route/Overview.kt | 208 ++++++++++-------- .../kotlin/de/kif/backend/route/api/Post.kt | 20 +- .../de/kif/backend/util/ParseMarkdown.kt | 80 +++---- .../kotlin/de/kif/backend/util/PushService.kt | 2 +- 11 files changed, 241 insertions(+), 152 deletions(-) diff --git a/build.gradle b/build.gradle index 4fa2035..28332da 100644 --- a/build.gradle +++ b/build.gradle @@ -88,8 +88,6 @@ kotlin { implementation "de.westermann:KObserve-jvm:$observable_version" - //api 'org.jetbrains:markdown:0.1.28' - //implementation 'com.atlassian.commonmark:commonmark:0.12.1' implementation 'com.vladsch.flexmark:flexmark-all:0.42.10' api 'io.github.microutils:kotlin-logging:1.6.23' api 'ch.qos.logback:logback-classic:1.2.3' diff --git a/src/jsMain/kotlin/de/kif/frontend/main.kt b/src/jsMain/kotlin/de/kif/frontend/main.kt index cd77097..7642969 100644 --- a/src/jsMain/kotlin/de/kif/frontend/main.kt +++ b/src/jsMain/kotlin/de/kif/frontend/main.kt @@ -4,6 +4,7 @@ import de.kif.frontend.views.calendar.initCalendar import de.kif.frontend.views.table.initTableLayout import de.kif.frontend.views.initWorkGroupConstraints import de.kif.frontend.views.overview.initOverviewMain +import de.kif.frontend.views.overview.initPostEdit import de.kif.frontend.views.overview.initPosts import de.westermann.kwebview.components.init import kotlin.browser.document @@ -26,4 +27,7 @@ fun main() = init { if (document.getElementsByClassName("post").length > 0) { initPosts() } + if (document.getElementsByClassName("post-edit-right").length > 0) { + initPostEdit() + } } diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt index 0facc43..97e6292 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt @@ -45,6 +45,10 @@ object PostRepository : Repository { return repositoryRawGet("/api/p/$url") } + suspend fun render(data: String): String { + return repositoryPost("/api/render", data) + } + val handler = object : MessageHandler(RepositoryType.POST) { override fun onCreate(id: Long) = onCreate.emit(id) diff --git a/src/jsMain/kotlin/de/kif/frontend/views/overview/OverviewMain.kt b/src/jsMain/kotlin/de/kif/frontend/views/overview/OverviewMain.kt index be6ad5e..08f6f2f 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/overview/OverviewMain.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/overview/OverviewMain.kt @@ -1,8 +1,11 @@ package de.kif.frontend.views.overview import de.kif.frontend.iterator +import de.kif.frontend.launch import de.kif.frontend.repository.PostRepository import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLTextAreaElement +import org.w3c.dom.events.EventListener import org.w3c.dom.get import kotlin.browser.document @@ -38,4 +41,24 @@ fun initPosts() { for (post in postList) { PostView(post) } +} + +fun initPostEdit() { + val textArea = document.getElementById("content") as HTMLTextAreaElement + val preview = document.getElementsByClassName("post-edit-right")[0] as HTMLElement + + textArea.addEventListener("change", EventListener { + launch { + preview.innerHTML = PostRepository.render(textArea.value) + } + }) + textArea.addEventListener("keyup", EventListener { + launch { + preview.innerHTML = PostRepository.render(textArea.value) + } + }) + + launch { + preview.innerHTML = PostRepository.render(textArea.value) + } } \ No newline at end of file diff --git a/src/jsMain/resources/style/style.scss b/src/jsMain/resources/style/style.scss index d784d2f..b4d3922 100644 --- a/src/jsMain/resources/style/style.scss +++ b/src/jsMain/resources/style/style.scss @@ -1001,6 +1001,7 @@ form { color: $text-primary-color; } } + .post-edit { position: absolute; top: 0; @@ -1013,26 +1014,68 @@ form { margin: 0.7rem 0; padding: 0; } + h1 { font-size: 1rem; } + h2 { font-size: 1rem; } + h3 { font-size: 1rem; } + h4 { font-size: 1rem; } + h5 { font-size: 1rem; } + h6 { font-size: 1rem; } + + table { + border-collapse: collapse; + border-spacing: 0; + } + + td { + border-top: solid 1px rgba($text-primary-color, 0.1); + } + + th { + background-color: rgba($text-primary-color, 0.06); + } + + td, th { + padding: 0.4rem; + } } +.post-edit-container { + display: flex; + flex-direction: column; +} + +.post-edit-right { + margin-left: 0; +} + +@media (min-width: 768px) { + .post-edit-container { + flex-direction: row; + } + .post-edit-right { + margin-left: 2rem; + } +} + +/* .overview-post { max-height: 20rem; overflow: hidden; @@ -1047,4 +1090,5 @@ form { width: 100%; background: linear-gradient(0deg, $background-primary-color, transparent); } -} \ No newline at end of file +} + */ diff --git a/src/jvmMain/kotlin/de/kif/backend/Application.kt b/src/jvmMain/kotlin/de/kif/backend/Application.kt index 6ff2070..f90ad8f 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Application.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Application.kt @@ -12,9 +12,7 @@ import io.ktor.http.content.static import io.ktor.jackson.jackson import io.ktor.routing.routing import io.ktor.websocket.WebSockets -import kotlinx.serialization.ImplicitReflectionSerializer -@ImplicitReflectionSerializer fun Application.main() { install(DefaultHeaders) install(CallLogging) diff --git a/src/jvmMain/kotlin/de/kif/backend/Main.kt b/src/jvmMain/kotlin/de/kif/backend/Main.kt index e1db3fb..08abe04 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Main.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Main.kt @@ -8,10 +8,8 @@ import io.ktor.application.Application import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty import kotlinx.coroutines.runBlocking -import kotlinx.serialization.ImplicitReflectionSerializer object Main { - @ImplicitReflectionSerializer @Suppress("UnusedMainParameter") @JvmStatic fun main(args: Array) { diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Overview.kt b/src/jvmMain/kotlin/de/kif/backend/route/Overview.kt index b2b2ba7..47cafe7 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Overview.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Overview.kt @@ -136,63 +136,70 @@ fun Route.overview() { } content { h1 { +"Edit post" } - form(method = FormMethod.post) { - div("form-group") { - label { - htmlFor = "name" - +"Name" - } - input( - name = "name", - classes = "form-control" - ) { - id = "name" - placeholder = "Name" - value = editPost.name - } - } - div("form-group") { - label { - htmlFor = "url" - +"Url" - } - input( - name = "url", - classes = "form-control" - ) { - id = "places" - placeholder = "Places" - value = editPost.url - } - } + div("post-edit-container") { + div("post-edit-left") { + form(method = FormMethod.post) { + div("form-group") { + label { + htmlFor = "name" + +"Name" + } + input( + name = "name", + classes = "form-control" + ) { + id = "name" + placeholder = "Name" + value = editPost.name + } + } + div("form-group") { + label { + htmlFor = "url" + +"Url" + } + input( + name = "url", + classes = "form-control" + ) { + id = "places" + placeholder = "Places" + value = editPost.url + } + } - div("form-group") { - label { - htmlFor = "content" - +"Content" - } - textArea(rows = "10", classes = "form-control") { - name = "content" - id = "projector" + div("form-group") { + label { + htmlFor = "content" + +"Content" + } + textArea(rows = "10", classes = "form-control") { + name = "content" + id = "content" - +editPost.content - } - } + +editPost.content + } + } - div("form-group") { - a("/") { - button(classes = "form-btn") { - +"Cancel" + div("form-group") { + a("/") { + button(classes = "form-btn") { + +"Cancel" + } + } + button(type = ButtonType.submit, classes = "form-btn btn-primary") { + +"Save" + } } } - button(type = ButtonType.submit, classes = "form-btn btn-primary") { - +"Save" + a("/post/${editPost.id}/delete") { + button(classes = "form-btn btn-danger") { + +"Delete" + } } } - } - a("/post/${editPost.id}/delete") { - button(classes = "form-btn btn-danger") { - +"Delete" + div("post-edit-right post-content") { + } } } @@ -227,58 +234,65 @@ fun Route.overview() { } content { h1 { +"Create post" } - form(method = FormMethod.post) { - div("form-group") { - label { - htmlFor = "name" - +"Name" - } - input( - name = "name", - classes = "form-control" - ) { - id = "name" - placeholder = "Name" - value = "" - } - } - div("form-group") { - label { - htmlFor = "url" - +"Url" - } - input( - name = "url", - classes = "form-control" - ) { - id = "places" - placeholder = "Places" - value = Post.generateUrl() - } - } + div("post-edit-container") { + div("post-edit-left") { + form(method = FormMethod.post) { + div("form-group") { + label { + htmlFor = "name" + +"Name" + } + input( + name = "name", + classes = "form-control" + ) { + id = "name" + placeholder = "Name" + value = "" + } + } + div("form-group") { + label { + htmlFor = "url" + +"Url" + } + input( + name = "url", + classes = "form-control" + ) { + id = "places" + placeholder = "Places" + value = Post.generateUrl() + } + } - div("form-group") { - label { - htmlFor = "content" - +"Content" - } - textArea(rows = "10", classes = "form-control") { - name = "content" - id = "projector" + div("form-group") { + label { + htmlFor = "content" + +"Content" + } + textArea(rows = "10", classes = "form-control") { + name = "content" + id = "content" - +"" - } - } + +"" + } + } - div("form-group") { - a("/") { - button(classes = "form-btn") { - +"Cancel" + div("form-group") { + a("/") { + button(classes = "form-btn") { + +"Cancel" + } + } + button(type = ButtonType.submit, classes = "form-btn btn-primary") { + +"Create" + } } } - button(type = ButtonType.submit, classes = "form-btn btn-primary") { - +"Create" - } + } + div("post-edit-right post-content") { + } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/api/Post.kt b/src/jvmMain/kotlin/de/kif/backend/route/api/Post.kt index de200cf..626624b 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/api/Post.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/api/Post.kt @@ -9,7 +9,7 @@ import io.ktor.application.call import io.ktor.html.respondHtml import io.ktor.http.HttpStatusCode import io.ktor.request.receive -import io.ktor.response.respond +import io.ktor.request.receiveText import io.ktor.routing.Route import io.ktor.routing.get import io.ktor.routing.post @@ -58,7 +58,7 @@ fun Route.postApi() { post("/api/post/{id}") { try { - authenticate { + authenticate(Permission.POST) { val id = call.parameters["id"]?.toLongOrNull() val post = call.receive().copy(id = id) @@ -77,7 +77,7 @@ fun Route.postApi() { } post("/api/post/{id}/delete") { try { - authenticate { + authenticate(Permission.POST) { val id = call.parameters["id"]?.toLongOrNull() if (id != null) { @@ -112,4 +112,18 @@ fun Route.postApi() { call.error(HttpStatusCode.InternalServerError) } } + + post("/api/render") { + try { + authenticate(Permission.POST) { + val data = call.receiveText() + val html = markdownToHtml(data) + call.success(html) + } onFailure { + call.error(HttpStatusCode.Unauthorized) + } + } catch (_: Exception) { + call.error(HttpStatusCode.InternalServerError) + } + } } diff --git a/src/jvmMain/kotlin/de/kif/backend/util/ParseMarkdown.kt b/src/jvmMain/kotlin/de/kif/backend/util/ParseMarkdown.kt index 843060b..312dad4 100644 --- a/src/jvmMain/kotlin/de/kif/backend/util/ParseMarkdown.kt +++ b/src/jvmMain/kotlin/de/kif/backend/util/ParseMarkdown.kt @@ -2,59 +2,51 @@ package de.kif.backend.util import com.vladsch.flexmark.ext.autolink.AutolinkExtension import com.vladsch.flexmark.ext.emoji.EmojiExtension +import com.vladsch.flexmark.ext.emoji.EmojiImageType import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension +import com.vladsch.flexmark.ext.gitlab.GitLabExtension import com.vladsch.flexmark.ext.tables.TablesExtension -import com.vladsch.flexmark.util.ast.Node; +import com.vladsch.flexmark.ext.typographic.TypographicExtension +import com.vladsch.flexmark.ext.youtube.embedded.YouTubeLinkExtension import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.options.MutableDataSet; import java.util.* -/* -import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor -import org.intellij.markdown.html.HtmlGenerator -import org.intellij.markdown.parser.MarkdownParser +object ParseMarkdown { -fun markdownToHtml(content: String): String { - val flavour = CommonMarkFlavourDescriptor() - val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(content) - val html = HtmlGenerator(content, parsedTree, flavour).generateHtml() - return html -} - */ -/* -import org.commonmark.parser.Parser -import org.commonmark.renderer.html.HtmlRenderer + private val parser: Parser + private val renderer: HtmlRenderer -fun markdownToHtml(content: String): String { - val parser = Parser.builder().build() - val document = parser.parse(content) - val renderer = HtmlRenderer.builder().build() - return renderer.render(document) + fun parse(content: String): String { + val document = parser.parse(content) ?: return "" + return renderer.render(document) ?: "" + } + + init { + val options = MutableDataSet() + + options.set( + Parser.EXTENSIONS, Arrays.asList( + TablesExtension.create(), + StrikethroughExtension.create(), + TaskListExtension.create(), + EmojiExtension.create(), + AutolinkExtension.create(), + GitLabExtension.create(), + TypographicExtension.create(), + YouTubeLinkExtension.create() + ) + ) + + options.set(EmojiExtension.USE_IMAGE_TYPE, EmojiImageType.UNICODE_ONLY) + + //options.set(HtmlRenderer.SOFT_BREAK, "
\n"); + + parser = Parser.builder(options).build() + renderer = HtmlRenderer.builder(options).build() + } } - */ - -fun markdownToHtml(content: String): String { - val options = MutableDataSet() - - options.set(Parser.EXTENSIONS, Arrays.asList( - TablesExtension.create(), - StrikethroughExtension.create(), - TaskListExtension.create(), - EmojiExtension.create(), - AutolinkExtension.create() - )); - - //options.set(HtmlRenderer.SOFT_BREAK, "
\n"); - - val parser = Parser.builder(options).build() - val renderer = HtmlRenderer.builder(options).build() - - // You can re-use parser and renderer instances - val document = parser.parse(content) - val html = renderer.render(document) - - return html -} \ No newline at end of file +fun markdownToHtml(content: String): String = ParseMarkdown.parse(content) diff --git a/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt b/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt index 4a0c5d3..e45635d 100644 --- a/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt +++ b/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt @@ -51,4 +51,4 @@ fun Route.pushService() { UserRepository.registerPushService() WorkGroupRepository.registerPushService() PostRepository.registerPushService() -} \ No newline at end of file +}