Alternative way for link passing and substitution in markdown

This commit is contained in:
Alexander Nozik 2024-03-02 16:59:01 +03:00
parent 0c4ae405b8
commit 324afe8fd5
4 changed files with 143 additions and 59 deletions

View File

@ -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<IElementType, GeneratingProvider> {
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<String> = 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<PageFragment> = SnarkReader(this, "text/markdown")
}

View File

@ -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 = """\$\{(?<target>[\w.]*)(?:\((?:"|&quot;)(?<name>.*)(?:"|&quot;)\))?\}""".toRegex()
private val attributeRegex = """="(?<uri>snark://([^"]*))"""".toRegex()
}
}
/**
@ -49,26 +63,28 @@ public class WebPageTextProcessor(private val page: PageContext) : TextProcessor
public class Postprocessor<out R>(
public val page: PageContext,
private val consumer: TagConsumer<R>,
private val processor: TextProcessor = WebPageTextProcessor(page),
private val textProcessor: TextProcessor,
) : TagConsumer<R> 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()
}
}
}

View File

@ -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<PageFragment>
public object HtmlReader : SnarkHtmlReader {
override val types: Set<String> = setOf("html")
override fun readFrom(source: String): PageFragment = PageFragment {
div {
unsafe { +source }
}
}
override fun readFrom(source: Source): PageFragment = readFrom(source.readString())
}

View File

@ -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<PageFragment>
public object HtmlReader : SnarkHtmlReader {
override val types: Set<String> = 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<String> = 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<PageFragment> = SnarkReader(this, "text/markdown")
}