Skip to content

Commit

Permalink
feat: add embed link support (TuanManhCao#25)
Browse files Browse the repository at this point in the history
* fix: image data handling issue

* refactor: avoid loading unnecessary file data

* refactor: replace paragraph element tag to div

* chore: add kotlin codes

* refactor: remove unused imports

* feat: add embed link support

* docs: add embed link in Features
  • Loading branch information
turtton authored Mar 26, 2023
1 parent 912f3c6 commit 59fc85b
Show file tree
Hide file tree
Showing 18 changed files with 789 additions and 598 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ https://volglass.turtton.net

- **Search** documents
- **Dark** Theme
- **Embed Link** Support
- **Mermaid** Support

Also, I fixed many issues that the original has.
Expand Down
8 changes: 8 additions & 0 deletions kotlin/src/main/kotlin/Cache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ val cacheScope = CoroutineScope(Dispatchers.Default + SupervisorJob())

val json = Json

// only available on initializing cache
var embedTargets: MutableSet<FileNameString>? = null

// CacheData + DirectoryTreeData
val cacheData: KStore<Pair<CacheData, String>> = storeOf("volglass.cache")

Expand Down Expand Up @@ -75,6 +78,7 @@ fun initCache(
nameCache += plainFileName.fileName
}

embedTargets = mutableSetOf()
val dependencyData = DependencyData()
val fileNameInfo = FileNameInfo(postFolder, duplicatedFile, fileNameToPath, fileNameToSlug, fileNameToMediaSlug)
getAllFiles().forEach { filePath ->
Expand All @@ -84,6 +88,10 @@ fun initCache(
convertMarkdownToReactElement(PathString(filePath).toFileName(postFolder, duplicatedFile), content, dependencyData, fileNameInfo, null, null, null)
}
}
embedTargets!!.forEach {
val path = fileNameToPath[it]!!
dependencyData.embedContents[it] = readContent(path.path)
}

cacheData.set(CacheData(dependencyData, fileNameInfo) to directoryTreeData)
return@promise fileNameToSlug.values.map { it.slug }.toTypedArray()
Expand Down
3 changes: 3 additions & 0 deletions kotlin/src/main/kotlin/CustomString.kt
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ fun toFileName(slug: String, cacheData: String): String {
return SlugString(slug).toFileName(fileNameInfo.duplicatedFile).fileName
}

@JsExport
fun isMediaFile(fileName: String): Boolean = FileNameString(fileName).isMediaFile

fun String.removeMdExtension(): String = replace("\\.md$".toRegex(), "")

/**
Expand Down
4 changes: 4 additions & 0 deletions kotlin/src/main/kotlin/DependencyData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ data class DependencyData(
* key <- values
*/
val linkDependencies: MutableMap<FileNameString, MutableSet<FileNameString>> = mutableMapOf(),
/**
* Preloaded contents
*/
val embedContents: MutableMap<FileNameString, String> = mutableMapOf(),
)
4 changes: 2 additions & 2 deletions kotlin/src/main/kotlin/GraphData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ fun getRawGraphData(slugString: String, cacheData: String): GraphData {
val targetFileName = slug.toFileName(fileNameInfo.duplicatedFile)
nodes += Node(slugString, targetFileName.fileName)
dependencyData.dependingLinks[targetFileName]?.forEach {
val targetSlug = fileNameInfo.fileNameToSlug[it]!!
val targetSlug = fileNameInfo.fileNameToSlug[it] ?: error("Failed to get slug. FileName:$it")
nodes += Node(targetSlug.slug, it.fileName)
edges += Edge(slugString, targetSlug.slug)
}
dependencyData.linkDependencies[targetFileName]?.forEach {
val targetSlug = fileNameInfo.fileNameToSlug[it]!!
val targetSlug = fileNameInfo.fileNameToSlug[it] ?: error("Failed to get slug. FileName:$it")
nodes += Node(targetSlug.slug, it.fileName)
edges += Edge(targetSlug.slug, slugString)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package markdown

import markdown.parser.ObsidianEmbedLinkParser
import markdown.parser.ObsidianLineBreakParser
import markdown.parser.ObsidianLinkParser
import org.intellij.markdown.parser.sequentialparsers.SequentialParser
Expand All @@ -9,6 +10,6 @@ class ObsidianSequentialParserManager(
private val parentParsers: List<SequentialParser>,
) : SequentialParserManager() {
override fun getParserSequence(): List<SequentialParser> {
return listOf(ObsidianLinkParser(), ObsidianLineBreakParser()) + parentParsers
return listOf(ObsidianLinkParser(), ObsidianEmbedLinkParser(), ObsidianLineBreakParser()) + parentParsers
}
}
41 changes: 41 additions & 0 deletions kotlin/src/main/kotlin/markdown/parser/ObsidianEmbedLinkParser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package markdown.parser

import markdown.type.ObsidianElementTypes
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.parser.sequentialparsers.RangesListBuilder
import org.intellij.markdown.parser.sequentialparsers.SequentialParser
import org.intellij.markdown.parser.sequentialparsers.TokensCache

class ObsidianEmbedLinkParser : SequentialParser {
override fun parse(tokens: TokensCache, rangesToGlue: List<IntRange>): SequentialParser.ParsingResult {
var result = SequentialParser.ParsingResultBuilder()
val delegateIndices = RangesListBuilder()
var iterator: TokensCache.Iterator = tokens.RangesListIterator(rangesToGlue)
while (iterator.type != null) {
val isTargetFile = iterator.type == MarkdownTokenTypes.EXCLAMATION_MARK &&
iterator.rawLookup(1) == MarkdownTokenTypes.LBRACKET &&
iterator.rawLookup(2) == MarkdownTokenTypes.LBRACKET
if (isTargetFile) {
// ![>>[<<link]]
iterator = iterator.advance().advance()
val target = ObsidianLinkParser.parseTarget(iterator)
if (target != null) {
result = result.withNode(
SequentialParser.Node(
// Target >>![[link]]<<
iterator.index - 2..target.iteratorPosition.index + 1,
ObsidianElementTypes.EMBED_LINK,
),
)
iterator = target.iteratorPosition.advance()
continue
}
}

delegateIndices.put(iterator.index)
iterator = iterator.advance()
}

return result.withFurtherProcessing(delegateIndices.get())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ class ImageElementProcessor<Parent>(

override fun <Visitor> renderLink(visitor: Visitor, markdownText: String, node: ASTNode, info: LinkGeneratingProvider.RenderInfo) where Visitor : TagConsumer<IntrinsicType<HTMLAttributes<HTMLElement>>, Parent>, Visitor : org.intellij.markdown.ast.visitors.Visitor, Visitor : LeafVisitor {
visitor.consumeTagOpen(node, img.unsafeCast<IntrinsicType<HTMLAttributes<HTMLElement>>>())
val url = makeAbsoluteUrl(info.destination)

val url = when (val resolvedDestination = resolveUrl(info.destination)) {
is Destination.Router -> resolvedDestination.slug.slug
is Destination.RawLink -> resolvedDestination.link.toString()
}
val label = getPlainTextFrom(info.label, markdownText)
val title = info.title?.toString()
visitor.consume {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,20 @@ abstract class LinkElementProcessor<Parent>(
}
val expectedFileName = destination.toString().removePrefix("/").removeMdExtension()
val fileNameToSlug = fileNameInfo.fileNameToSlug
val slug = if (!destination.startsWith("http")) {
fileNameToSlug[FileNameString(expectedFileName)]
// path/to/target -> target
?: fileNameToSlug[FileNameString(expectedFileName.split('/').last())]
val fileNameToMediaSlug = fileNameInfo.fileNameToMediaSlug
val duplicatedFile = fileNameInfo.duplicatedFile
val (mediaSlug, slug) = if (!destination.startsWith("http")) {
val expectedFileNameString = SlugString("/$expectedFileName").toFileName(duplicatedFile)
fileNameToMediaSlug[expectedFileNameString] to fileNameToSlug[expectedFileNameString]
} else {
null
null to null
}
if (slug != null) {
val targetFile = slug.toFileName(fileNameInfo.duplicatedFile)
val targetFile = slug.toFileName(duplicatedFile)
dependencyData.dependingLinks.getOrPut(fileName) { mutableSetOf() }.add(targetFile)
dependencyData.linkDependencies.getOrPut(targetFile) { mutableSetOf() }.add(fileName)

return Destination.Router(slug)
return Destination.Router(mediaSlug ?: slug)
}

return baseURI?.resolveToStringSafe(destination.toString())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package markdown.processor.element

import DependencyData
import FileNameInfo
import FileNameString
import SlugString
import csstype.ClassName
import embedTargets
import external.CodeEncoder
import external.MermaidRender
import external.NextRouter
import markdown.LeafVisitor
import markdown.TagConsumer
import markdown.convertMarkdownToReactElement
import org.intellij.markdown.ast.ASTNode
import org.intellij.markdown.html.LinkGeneratingProvider
import org.intellij.markdown.html.URI
import org.w3c.dom.HTMLElement
import react.ChildrenBuilder
import react.IntrinsicType
import react.dom.html.HTMLAttributes
import react.dom.html.ReactHTML.audio
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.img
import react.dom.html.ReactHTML.strong

/**
* See [Official Help about EmbeddingLink](https://help.obsidian.md/Linking+notes+and+files/Embedding+files)
*/
class ObsidianEmbedLinkProcessor<Parent>(
baseURI: URI?,
router: NextRouter?,
fileName: FileNameString,
dependencyData: DependencyData,
fileNameInfo: FileNameInfo,
private val codeEncoder: CodeEncoder?,
private val mermaidRender: MermaidRender?,
resolveAnchors: Boolean = false,
) : LinkElementProcessor<Parent>(baseURI, router, fileName, dependencyData, fileNameInfo, resolveAnchors)
where Parent : HTMLAttributes<HTMLElement>, Parent : ChildrenBuilder {
private val obsidianLinkProcessor = ObsidianLinkElementProcessor<Parent>(baseURI, router, fileName, dependencyData, fileNameInfo, resolveAnchors)
override fun getRenderInfo(markdownText: String, node: ASTNode): LinkGeneratingProvider.RenderInfo {
return obsidianLinkProcessor.getRenderInfo(markdownText, node)
}

override fun <Visitor> renderLink(visitor: Visitor, markdownText: String, node: ASTNode, info: LinkGeneratingProvider.RenderInfo) where Visitor : TagConsumer<IntrinsicType<HTMLAttributes<HTMLElement>>, Parent>, Visitor : org.intellij.markdown.ast.visitors.Visitor, Visitor : LeafVisitor {
val destination = info.destination.toString()
// [internalDestination] specifies the display range for markdown and pdf
val (rawUrl, internalDestination) = destination.split('#').let { it.first() to it.getOrNull(1) }
// [url] may point to media content in the public folder
val url = when (val resolvedDestination = resolveUrl(rawUrl)) {
is Destination.Router -> resolvedDestination.slug.slug
is Destination.RawLink -> {
println("Warn: Failed to get slug data. target:$rawUrl")
"/${resolvedDestination.link}"
}
}
val sizeOption = info.title
// for image link option
val (imageWidth, imageHeight) = if (sizeOption != null && destination != sizeOption) {
sizeOption.split('x').let { it.first().toDouble() to it.getOrNull(1)?.toDouble() }
} else {
null to null
}
val targetFile = FileNameString(destination)
when {
targetFile.isImageFile -> visitor.consume {
img {
src = url
alt = destination
if (imageWidth != null) {
width = imageWidth
}
if (imageWidth != null) {
height = imageHeight
}
}
}
targetFile.isSoundFile -> visitor.consume {
audio {
controls = true
controlsList = "nodownload"
src = url
}
}
// markdown file
// TODO support internalDestination
else -> {
val targetFileName = SlugString(url).toFileName(fileNameInfo.duplicatedFile)
if (targetFileName == fileName) {
println("Warning: Embed loop is detected in ${fileName.fileName}")
return
}
if (embedTargets != null) {
embedTargets?.add(targetFile)
} else {
val content = dependencyData.embedContents[targetFileName] ?: ""
val element = convertMarkdownToReactElement(targetFileName, content, dependencyData, fileNameInfo, router, codeEncoder, mermaidRender)
visitor.consume {
div {
className = ClassName("border-l-2 px-4 border-gray-300 dark:border-gray-600")
strong {
+targetFileName.fileName
}
element()
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ class ObsidianLinkElementProcessor<Parent>(
// title or {link|title -> listOf(link, title) -> title}
val title = rawTitle.split('|').last()

val lbracketNode = node.children[0]
val rbracketNode = node.children.last()
val lbracketNode = node.children.first { it.type == MarkdownTokenTypes.LBRACKET }
val rbracketNode = node.children.first { it.type == MarkdownTokenTypes.RBRACKET }

// link|>>t<<itle or >>t<<itle
val verticalLinePosition = rawTitle.indexOf('|') + 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import react.dom.html.ReactHTML.h4
import react.dom.html.ReactHTML.h5
import react.dom.html.ReactHTML.h6
import react.dom.html.ReactHTML.hr
import react.dom.html.ReactHTML.p
import react.dom.html.ReactHTML.strong
import react.dom.html.ReactHTML.ul

Expand All @@ -54,6 +53,7 @@ fun <Parent> createReactElementGeneratingProcessors(
where Parent : HTMLAttributes<HTMLElement>, Parent : ChildrenBuilder =
mapOf(
ObsidianElementTypes.LINK to ObsidianLinkElementProcessor<Parent>(baseURI, router, filename, dependencyData, fileNameInfo, absolutizeAnchorLinks).makeXssSafe(useSafeLinks),
ObsidianElementTypes.EMBED_LINK to ObsidianEmbedLinkProcessor<Parent>(baseURI, router, filename, dependencyData, fileNameInfo, encoder, mermaidRender, absolutizeAnchorLinks).makeXssSafe(useSafeLinks),

MarkdownElementTypes.MARKDOWN_FILE to SimpleElementNodeProcessor(div),
MarkdownElementTypes.HTML_BLOCK to HtmlBlockElementProcessor(),
Expand Down Expand Up @@ -96,7 +96,7 @@ fun <Parent> createReactElementGeneratingProcessors(
MarkdownTokenTypes.HORIZONTAL_RULE to SingleElementProcessor(hr),
MarkdownTokenTypes.HARD_LINE_BREAK to SingleElementProcessor(br),

MarkdownElementTypes.PARAGRAPH to SandwichTrimmingElementProcessor(p),
MarkdownElementTypes.PARAGRAPH to SandwichTrimmingElementProcessor(div),
MarkdownElementTypes.EMPH to SimpleInlineElementProcessor(em, 1, -1),
MarkdownElementTypes.STRONG to SimpleInlineElementProcessor(strong, 2, -2),
MarkdownElementTypes.CODE_SPAN to CodeSpanElementProvider(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import org.intellij.markdown.IElementType

object ObsidianElementTypes {
val LINK: IElementType = ObsidianElementType("LINK")
val EMBED_LINK: IElementType = ObsidianElementType("EMBED_LINK")
}
Loading

0 comments on commit 59fc85b

Please sign in to comment.