diff --git a/gradle.properties b/gradle.properties index 6f55956..e2e0fd8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ kotlin.code.style=official -toolsVersion=0.11.5-kotlin-1.6.21 \ No newline at end of file +toolsVersion=0.11.7-kotlin-1.7.0 \ No newline at end of file diff --git a/src/main/kotlin/ru/mipt/spc/master.kt b/src/main/kotlin/ru/mipt/spc/master.kt index df846d7..af51e06 100644 --- a/src/main/kotlin/ru/mipt/spc/master.kt +++ b/src/main/kotlin/ru/mipt/spc/master.kt @@ -296,7 +296,7 @@ internal fun Application.spcMaster(context: Context, dataPath: Path, prefix: Str val snark = context.fetch(SnarkPlugin) - val magProgPageContext: PageContext = snark.parse(prefix, dataPath.resolve("content")) + val magProgPageContext: PageContext = snark.read(dataPath.resolve("content"), prefix) routing { route(prefix) { diff --git a/src/main/kotlin/ru/mipt/spc/spcHome.kt b/src/main/kotlin/ru/mipt/spc/spcHome.kt index 670078d..7aa103c 100644 --- a/src/main/kotlin/ru/mipt/spc/spcHome.kt +++ b/src/main/kotlin/ru/mipt/spc/spcHome.kt @@ -5,8 +5,6 @@ import io.ktor.server.application.Application import io.ktor.server.application.call import io.ktor.server.application.log import io.ktor.server.html.respondHtml -import io.ktor.server.http.content.files -import io.ktor.server.http.content.static import io.ktor.server.routing.* import kotlinx.html.* import space.kscience.dataforge.context.Context @@ -304,26 +302,17 @@ internal fun Application.spcHome(context: Context, rootPath: Path, prefix: Strin val snark = context.fetch(SnarkPlugin) - val homePageContext = snark.parse(prefix, rootPath.resolve("content")) + val homePageContext = snark.read(rootPath.resolve("content"), prefix) routing { route(prefix) { + snark(homePageContext) { + staticDirectory("assets", rootPath.resolve("assets")) + staticDirectory("images", rootPath.resolve("images")) + page { spcHome() } + } + with(homePageContext) { - static("assets") { - files(rootPath.resolve("assets").toFile()) - } - - static("images") { - files(rootPath.resolve("images").toFile()) - } - - get { - withRequest(call.request) { - call.respondHtml { - spcHome() - } - } - } spcDirectory("consulting") spcPage("ru/consulting") diff --git a/src/main/kotlin/space/kscience/snark/PageContext.kt b/src/main/kotlin/space/kscience/snark/PageContext.kt index 8239f7b..777d02e 100644 --- a/src/main/kotlin/space/kscience/snark/PageContext.kt +++ b/src/main/kotlin/space/kscience/snark/PageContext.kt @@ -63,12 +63,15 @@ internal val Data<*>.published: Boolean get() = meta["published"].string != "fal fun PageContext(rootUrl: String, data: DataSet<*>): PageContext = PageContext(rootUrl, data.meta, data) -fun SnarkPlugin.parse(rootUrl: String, path: Path): PageContext { +fun SnarkPlugin.read(path: Path, rootUrl: String = "/"): PageContext { val parsedData: DataSet = readDirectory(path) return PageContext(rootUrl, parsedData) } +/** + * Substitute uri in [PageContext] with uri in the call to properly resolve relative refs. Only host properties are substituted. + */ context(PageContext) inline fun withRequest(request: ApplicationRequest, block: context(PageContext) () -> Unit) { val uri = URLBuilder( protocol = URLProtocol.createOrDefault(request.origin.scheme), diff --git a/src/main/kotlin/space/kscience/snark/RouteBuilder.kt b/src/main/kotlin/space/kscience/snark/RouteBuilder.kt new file mode 100644 index 0000000..cb7df45 --- /dev/null +++ b/src/main/kotlin/space/kscience/snark/RouteBuilder.kt @@ -0,0 +1,61 @@ +package space.kscience.snark + +import io.ktor.server.application.call +import io.ktor.server.html.respondHtml +import io.ktor.server.http.content.file +import io.ktor.server.http.content.files +import io.ktor.server.http.content.static +import io.ktor.server.routing.Route +import io.ktor.server.routing.get +import io.ktor.server.routing.route +import kotlinx.html.HTML +import java.nio.file.Path + +interface RouteBuilder { + val pageContext: PageContext + + fun staticFile(remotePath: String, file: Path) + + fun staticDirectory(remotePath: String, directory: Path) + + fun page(route: String = "", content: context(PageContext) HTML.() -> Unit) + + fun route(route: String, block: RouteBuilder.() -> Unit) +} + +class KtorRouteBuilder(override val pageContext: PageContext, private val ktorRoute: Route) : RouteBuilder { + override fun staticFile(remotePath: String, file: Path) { + ktorRoute.file(remotePath, file.toFile()) + } + + override fun staticDirectory(remotePath: String, directory: Path) { + ktorRoute.static(remotePath) { + files(directory.toFile()) + } + } + + override fun page(route: String, content: context(PageContext) HTML.() -> Unit) { + ktorRoute.get(route) { + with(pageContext) { + withRequest(call.request) { + val innerContext = this + call.respondHtml { + content(innerContext, this) + } + } + } + } + } + + override fun route(route: String, block: RouteBuilder.() -> Unit) { + ktorRoute.route(route) { + block(KtorRouteBuilder(pageContext, this)) + } + } +} + +fun Route.snark(pageContext: PageContext, block: context(PageContext) RouteBuilder.() -> Unit) { + with(pageContext){ + block(KtorRouteBuilder(pageContext, this@snark)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/kscience/snark/SnarkHtmlParser.kt b/src/main/kotlin/space/kscience/snark/SnarkHtmlParser.kt deleted file mode 100644 index 66717b5..0000000 --- a/src/main/kotlin/space/kscience/snark/SnarkHtmlParser.kt +++ /dev/null @@ -1,20 +0,0 @@ -package space.kscience.snark - -import io.ktor.http.ContentType -import io.ktor.utils.io.core.Input -import kotlinx.html.div -import kotlinx.html.unsafe -import kotlin.reflect.KType -import kotlin.reflect.typeOf - -object SnarkHtmlParser : SnarkParser { - override val contentType: ContentType = ContentType.Text.Html - override val fileExtensions: Set = setOf("html") - override val type: KType = typeOf() - - override fun readObject(input: Input): HtmlFragment = { - div { - unsafe { +input.readText() } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/space/kscience/snark/SnarkPlugin.kt b/src/main/kotlin/space/kscience/snark/SnarkPlugin.kt index 3cd1df2..6ad9384 100644 --- a/src/main/kotlin/space/kscience/snark/SnarkPlugin.kt +++ b/src/main/kotlin/space/kscience/snark/SnarkPlugin.kt @@ -1,6 +1,5 @@ package space.kscience.snark -import io.ktor.http.ContentType import io.ktor.util.extension import io.ktor.utils.io.core.readBytes import space.kscience.dataforge.context.* @@ -24,10 +23,11 @@ import kotlin.reflect.KClass import kotlin.reflect.KType import kotlin.reflect.typeOf +/** + * A parser of binary content including priority flag and file extensions + */ @Type(SnarkParser.TYPE) interface SnarkParser : IOReader { - val contentType: ContentType - val fileExtensions: Set val priority: Int get() = DEFAULT_PRIORITY @@ -46,16 +46,17 @@ interface SnarkParser : IOReader { internal class SnarkParserWrapper( val reader: IOReader, override val type: KType, - override val contentType: ContentType, override val fileExtensions: Set, ) : SnarkParser, IOReader by reader +/** + * Create a generic parser from reader + */ @Suppress("FunctionName") inline fun SnarkParser( reader: IOReader, - contentType: ContentType, vararg fileExtensions: String, -): SnarkParser = SnarkParserWrapper(reader, typeOf(), contentType, fileExtensions.toSet()) +): SnarkParser = SnarkParserWrapper(reader, typeOf(), fileExtensions.toSet()) @OptIn(DFExperimental::class) class SnarkPlugin : AbstractPlugin() { @@ -88,8 +89,11 @@ class SnarkPlugin : AbstractPlugin() { SnarkParser.TYPE -> mapOf( "html".asName() to SnarkHtmlParser, "markdown".asName() to SnarkMarkdownParser, - "json".asName() to SnarkParser(JsonMetaFormat, ContentType.Application.Json, "json"), - "yaml".asName() to SnarkParser(YamlMetaFormat, ContentType.Application.Json, "yaml", "yml") + "json".asName() to SnarkParser(JsonMetaFormat, "json"), + "yaml".asName() to SnarkParser(YamlMetaFormat, "yaml", "yml"), + "png".asName() to SnarkParser(ImageIOReader, "png"), + "jpg".asName() to SnarkParser(ImageIOReader, "jpg", "jpeg"), + "gif".asName() to SnarkParser(ImageIOReader, "jpg", "jpeg"), ) else -> super.content(target) } diff --git a/src/main/kotlin/space/kscience/snark/SnarkMarkdownParser.kt b/src/main/kotlin/space/kscience/snark/parsers.kt similarity index 53% rename from src/main/kotlin/space/kscience/snark/SnarkMarkdownParser.kt rename to src/main/kotlin/space/kscience/snark/parsers.kt index f5eb322..05ac768 100644 --- a/src/main/kotlin/space/kscience/snark/SnarkMarkdownParser.kt +++ b/src/main/kotlin/space/kscience/snark/parsers.kt @@ -1,34 +1,53 @@ package space.kscience.snark -import io.ktor.http.ContentType +import io.ktor.util.asStream import io.ktor.utils.io.core.Input import kotlinx.html.div import kotlinx.html.unsafe import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor import org.intellij.markdown.html.HtmlGenerator import org.intellij.markdown.parser.MarkdownParser +import space.kscience.dataforge.io.IOReader +import java.awt.image.BufferedImage +import javax.imageio.ImageIO import kotlin.reflect.KType import kotlin.reflect.typeOf -object SnarkMarkdownParser:SnarkParser { - override val contentType: ContentType = ContentType.Text.Html +internal object SnarkHtmlParser : SnarkParser { + override val fileExtensions: Set = setOf("html") + override val type: KType = typeOf() + + override fun readObject(input: Input): HtmlFragment = { + div { + unsafe { +input.readText() } + } + } +} + +internal object SnarkMarkdownParser : SnarkParser { override val fileExtensions: Set = setOf("markdown", "mdown", "mkdn", "mkd", "md") override val type: KType = typeOf() private val markdownFlavor = CommonMarkFlavourDescriptor() private val markdownParser = MarkdownParser(markdownFlavor) - override fun readObject(input: Input): HtmlFragment { + override fun readObject(input: Input): HtmlFragment { val src = input.readText() val parsedTree = markdownParser.buildMarkdownTreeFromString(src) val htmlString = HtmlGenerator(src, parsedTree, markdownFlavor).generateHtml() return { - div{ + div { unsafe { +htmlString } } } } -} \ No newline at end of file +} + +internal object ImageIOReader : IOReader { + override val type: KType get() = typeOf() + + override fun readObject(input: Input): BufferedImage = ImageIO.read(input.asStream()) +}