From 324afe8fd57abe5971593d7168748057eda24117 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 2 Mar 2024 16:59:01 +0300 Subject: [PATCH] Alternative way for link passing and substitution in markdown --- .../kscience/snark/html/MarkdownReader.kt | 94 +++++++++++++++++++ .../kscience/snark/html/Postprocessor.kt | 36 +++++-- .../kscience/snark/html/SnarkHtmlReader.kt | 23 +++++ .../space/kscience/snark/html/readers.kt | 49 ---------- 4 files changed, 143 insertions(+), 59 deletions(-) create mode 100644 snark-html/src/jvmMain/kotlin/space/kscience/snark/html/MarkdownReader.kt create mode 100644 snark-html/src/jvmMain/kotlin/space/kscience/snark/html/SnarkHtmlReader.kt delete mode 100644 snark-html/src/jvmMain/kotlin/space/kscience/snark/html/readers.kt diff --git a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/MarkdownReader.kt b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/MarkdownReader.kt new file mode 100644 index 0000000..25ae7f0 --- /dev/null +++ b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/MarkdownReader.kt @@ -0,0 +1,94 @@ +package space.kscience.snark.html + +import kotlinx.io.Source +import kotlinx.io.readString +import org.intellij.markdown.IElementType +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.ast.ASTNode +import org.intellij.markdown.ast.findChildOfType +import org.intellij.markdown.ast.getTextInNode +import org.intellij.markdown.flavours.space.SFMFlavourDescriptor +import org.intellij.markdown.html.* +import org.intellij.markdown.parser.LinkMap +import org.intellij.markdown.parser.MarkdownParser +import space.kscience.snark.SnarkReader + +private class SnarkInlineLinkGeneratingProvider( + baseURI: URI?, + resolveAnchors: Boolean = false, +) : LinkGeneratingProvider(baseURI, resolveAnchors) { + + override fun getRenderInfo(text: String, node: ASTNode): RenderInfo? { + return RenderInfo( + label = node.findChildOfType(MarkdownElementTypes.LINK_TEXT) ?: return null, + destination = node.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)?.getTextInNode(text)?.let { raw -> + val processedLink = WebPageTextProcessor.functionRegex.replace(raw) { match -> + when (match.groups["target"]?.value) { + "homeRef" -> "snark://homeRef" + "resolveRef" -> "snark://ref/${match.groups["name"]?.value ?: ""}" + "resolvePageRef" -> "snark://page/${match.groups["name"]?.value ?: ""}" + "pageMeta.get" -> "snark://meta/${match.groups["name"]?.value ?: ""}" + else -> match.value + } + } + LinkMap.normalizeDestination(processedLink, true) + } ?: "", + title = node.findChildOfType(MarkdownElementTypes.LINK_TITLE)?.getTextInNode(text)?.let { + LinkMap.normalizeTitle(it) + } + ) + } +} + +private class SnarkImageGeneratingProvider( + linkMap: LinkMap, + baseURI: URI?, +) : ImageGeneratingProvider(linkMap, baseURI) { + + val snarkInlineLinkProvider = SnarkInlineLinkGeneratingProvider(baseURI) + override fun getRenderInfo(text: String, node: ASTNode): RenderInfo? { + node.findChildOfType(MarkdownElementTypes.INLINE_LINK)?.let { linkNode -> + return snarkInlineLinkProvider.getRenderInfo(text, linkNode) + } + (node.findChildOfType(MarkdownElementTypes.FULL_REFERENCE_LINK) + ?: node.findChildOfType(MarkdownElementTypes.SHORT_REFERENCE_LINK)) + ?.let { linkNode -> + return referenceLinkProvider.getRenderInfo(text, linkNode) + } + return null + } +} + +public object SnarkFlavorDescriptor : SFMFlavourDescriptor(false) { + override fun createHtmlGeneratingProviders(linkMap: LinkMap, baseURI: URI?): Map { + return super.createHtmlGeneratingProviders(linkMap, baseURI) + mapOf( + MarkdownElementTypes.INLINE_LINK to SnarkInlineLinkGeneratingProvider(baseURI, absolutizeAnchorLinks) + .makeXssSafe(useSafeLinks), + MarkdownElementTypes.IMAGE to SnarkImageGeneratingProvider(linkMap, baseURI) + .makeXssSafe(useSafeLinks), + ) + } +} + +public object MarkdownReader : SnarkHtmlReader { + override val types: Set = setOf("text/markdown", "md", "markdown") + + override fun readFrom(source: String): PageFragment = PageFragment { + val parsedTree = markdownParser.parse(IElementType("ROOT"), source) +// markdownParser.buildMarkdownTreeFromString(source) + val htmlString = HtmlGenerator(source, parsedTree, markdownFlavor).generateHtml() + + consumer.onTagContentUnsafe { + +htmlString + } + + } + + private val markdownFlavor = SnarkFlavorDescriptor//SFMFlavourDescriptor(false) + private val markdownParser = MarkdownParser(markdownFlavor) + + override fun readFrom(source: Source): PageFragment = readFrom(source.readString()) + + public val snarkReader: SnarkReader = SnarkReader(this, "text/markdown") + +} \ No newline at end of file diff --git a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/Postprocessor.kt b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/Postprocessor.kt index c77c61f..223206d 100644 --- a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/Postprocessor.kt +++ b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/Postprocessor.kt @@ -4,9 +4,9 @@ import kotlinx.html.* import space.kscience.dataforge.meta.string import space.kscience.dataforge.names.parseAsName import space.kscience.snark.TextProcessor +import java.net.URI public class WebPageTextProcessor(private val page: PageContext) : TextProcessor { - private val regex = """\$\{([\w.]*)(?>\("(.*)"\))?}""".toRegex() /** * A basic [TextProcessor] that replaces `${...}` expressions in text. The following expressions are recognised: @@ -16,30 +16,44 @@ public class WebPageTextProcessor(private val page: PageContext) : TextProcessor * * `pageMeta.get("...") -> [PageContext.pageMeta] get string method * Otherwise return unchanged string */ - override fun process(text: CharSequence): String = text.replace(regex) { match -> - when (match.groups[1]!!.value) { + override fun process(text: CharSequence): String = text.replace(functionRegex) { match -> + when (match.groups["target"]?.value) { "homeRef" -> page.homeRef "resolveRef" -> { - val refString = match.groups[2]?.value ?: error("resolveRef requires a string (quoted) argument") + val refString = match.groups["name"]?.value ?: error("resolveRef requires a string (quoted) argument") page.resolveRef(refString) } "resolvePageRef" -> { - val refString = match.groups[2]?.value + val refString = match.groups["name"]?.value ?: error("resolvePageRef requires a string (quoted) argument") page.localisedPageRef(refString.parseAsName()) } "pageMeta.get" -> { - val nameString = match.groups[2]?.value + val nameString = match.groups["name"]?.value ?: error("resolvePageRef requires a string (quoted) argument") page.pageMeta[nameString.parseAsName()].string ?: "@null" } else -> match.value } + }.replace(attributeRegex){ match-> + val uri = URI(match.groups["uri"]!!.value) + val snarkUrl = when(uri.authority){ + "homeRef"->page.homeRef + "ref" -> page.resolveRef(uri.path) + "page" -> page.localisedPageRef(uri.path.parseAsName()) + "meta" -> page.pageMeta[uri.path.parseAsName()].string ?: "@null" + else -> match.value + } + "=\"$snarkUrl\"" } + public companion object { + internal val functionRegex = """\$\{(?[\w.]*)(?:\((?:"|")(?.*)(?:"|")\))?\}""".toRegex() + private val attributeRegex = """="(?snark://([^"]*))"""".toRegex() + } } /** @@ -49,26 +63,28 @@ public class WebPageTextProcessor(private val page: PageContext) : TextProcessor public class Postprocessor( public val page: PageContext, private val consumer: TagConsumer, - private val processor: TextProcessor = WebPageTextProcessor(page), + private val textProcessor: TextProcessor, ) : TagConsumer by consumer { override fun onTagAttributeChange(tag: Tag, attribute: String, value: String?) { if (tag is A && attribute == "href" && value != null) { - consumer.onTagAttributeChange(tag, attribute, processor.process(value)) + consumer.onTagAttributeChange(tag, attribute, textProcessor.process(value)) + } else if (tag is IMG && attribute == "src" && value != null) { + consumer.onTagAttributeChange(tag, attribute, textProcessor.process(value)) } else { consumer.onTagAttributeChange(tag, attribute, value) } } override fun onTagContent(content: CharSequence) { - consumer.onTagContent(processor.process(content)) + consumer.onTagContent(textProcessor.process(content)) } override fun onTagContentUnsafe(block: Unsafe.() -> Unit) { val proxy = object : Unsafe { override fun String.unaryPlus() { consumer.onTagContentUnsafe { - processor.process(this@unaryPlus).unaryPlus() + textProcessor.process(this@unaryPlus).unaryPlus() } } } diff --git a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/SnarkHtmlReader.kt b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/SnarkHtmlReader.kt new file mode 100644 index 0000000..924ef89 --- /dev/null +++ b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/SnarkHtmlReader.kt @@ -0,0 +1,23 @@ +package space.kscience.snark.html + +import kotlinx.html.div +import kotlinx.html.unsafe +import kotlinx.io.Source +import kotlinx.io.readString +import space.kscience.snark.SnarkReader + + +public interface SnarkHtmlReader : SnarkReader + +public object HtmlReader : SnarkHtmlReader { + override val types: Set = setOf("html") + + override fun readFrom(source: String): PageFragment = PageFragment { + div { + unsafe { +source } + } + } + + override fun readFrom(source: Source): PageFragment = readFrom(source.readString()) +} + diff --git a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/readers.kt b/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/readers.kt deleted file mode 100644 index 940d9d9..0000000 --- a/snark-html/src/jvmMain/kotlin/space/kscience/snark/html/readers.kt +++ /dev/null @@ -1,49 +0,0 @@ -package space.kscience.snark.html - -import kotlinx.html.div -import kotlinx.html.unsafe -import kotlinx.io.Source -import kotlinx.io.readString -import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor -import org.intellij.markdown.html.HtmlGenerator -import org.intellij.markdown.parser.MarkdownParser -import space.kscience.snark.SnarkReader - - -public interface SnarkHtmlReader: SnarkReader - -public object HtmlReader : SnarkHtmlReader { - override val types: Set = setOf("html") - - override fun readFrom(source: String): PageFragment = PageFragment { - div { - unsafe { +source } - } - } - - override fun readFrom(source: Source): PageFragment = readFrom(source.readString()) -} - -public object MarkdownReader : SnarkHtmlReader { - override val types: Set = setOf("text/markdown", "md", "markdown") - - override fun readFrom(source: String): PageFragment = PageFragment { - val parsedTree = markdownParser.buildMarkdownTreeFromString(source) - val htmlString = HtmlGenerator(source, parsedTree, markdownFlavor).generateHtml() - - div { - unsafe { - +htmlString - } - } - } - - private val markdownFlavor = CommonMarkFlavourDescriptor() - private val markdownParser = MarkdownParser(markdownFlavor) - - override fun readFrom(source: Source): PageFragment = readFrom(source.readString()) - - public val snarkReader: SnarkReader = SnarkReader(this, "text/markdown") - -} -