Skip to content

Commit d95db47

Browse files
committed
feat: correctly resolve link definitions
1 parent e207a32 commit d95db47

File tree

14 files changed

+113
-78
lines changed

14 files changed

+113
-78
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,43 @@
11
package com.quarkdown.core.ast.attributes.link
22

3-
import com.quarkdown.core.ast.base.inline.Image
3+
import com.quarkdown.core.ast.base.LinkNode
44
import com.quarkdown.core.context.Context
55
import com.quarkdown.core.context.MutableContext
66
import com.quarkdown.core.property.Property
77

88
/**
9-
* [Property] that is assigned to each image that points to a local relative path that is different from the original.
9+
* [Property], assigned to each image link, that points to a local relative URL (path) that is different from the original.
1010
* For instance, an image may have a link to `images/picture.png`,
1111
* but if it's loaded from an included document with a different base path, it may be resolved to, for example, `../images/picture.png`.
1212
* @see com.quarkdown.core.ast.base.inline.Link
13-
* @see com.quarkdown.core.context.hooks.location.Imag for the storing stage
13+
* @see com.quarkdown.core.context.hooks.LinkUrlResolverHook for the storing stage
1414
*/
15-
data class ResolvedImagePathProperty(
15+
data class ResolvedLinkUrlProperty(
1616
override val value: String,
1717
) : Property<String> {
1818
companion object : Property.Key<String>
1919

20-
override val key = ResolvedImagePathProperty
20+
override val key = ResolvedLinkUrlProperty
2121
}
2222

2323
/**
2424
* @param context context where resolution data is stored
2525
* @return the resolved URL of this node within the document handled by [context],
2626
* or the regular URL if a resolved one is not registered
2727
*/
28-
fun Image.getResolvedUrl(context: Context): String =
29-
context.attributes.of(this)[ResolvedImagePathProperty]
30-
?: this.link.url
28+
fun LinkNode.getResolvedUrl(context: Context): String =
29+
context.attributes.of(this)[ResolvedLinkUrlProperty]
30+
?: this.url
3131

3232
/**
3333
* Registers the resolved path of this node within the document handled by [context].
3434
* @param context context where resolution data is stored
3535
* @param resolvedUrl resolved URL to set
36-
* @see com.quarkdown.core.context.hooks.ImagePathResolverHook
36+
* @see com.quarkdown.core.context.hooks.LinkUrlResolverHook
3737
*/
38-
fun Image.setResolvedUrl(
38+
fun LinkNode.setResolvedUrl(
3939
context: MutableContext,
4040
resolvedUrl: String,
4141
) {
42-
context.attributes.of(this) += ResolvedImagePathProperty(resolvedUrl)
42+
context.attributes.of(this) += ResolvedLinkUrlProperty(resolvedUrl)
4343
}

quarkdown-core/src/main/kotlin/com/quarkdown/core/ast/base/LinkNode.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.quarkdown.core.ast.base
22

33
import com.quarkdown.core.ast.InlineContent
44
import com.quarkdown.core.ast.Node
5+
import com.quarkdown.core.context.file.FileSystem
56

67
/**
78
* A general link node.
@@ -23,4 +24,10 @@ interface LinkNode : Node {
2324
* Optional title.
2425
*/
2526
val title: String?
27+
28+
/**
29+
* Optional file system where this link is defined, used for resolving relative paths.
30+
* @see com.quarkdown.core.context.hooks.LinkUrlResolverHook
31+
*/
32+
val fileSystem: FileSystem?
2633
}

quarkdown-core/src/main/kotlin/com/quarkdown/core/ast/base/block/LinkDefinition.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@ package com.quarkdown.core.ast.base.block
33
import com.quarkdown.core.ast.InlineContent
44
import com.quarkdown.core.ast.base.LinkNode
55
import com.quarkdown.core.ast.base.TextNode
6+
import com.quarkdown.core.context.file.FileSystem
67
import com.quarkdown.core.visitor.node.NodeVisitor
78

89
/**
910
* Creation of a referenceable link definition.
1011
* @param label inline content of the displayed label
1112
* @param url URL this link points to
1213
* @param title optional title
14+
* @param fileSystem optional file system this link is relative to
1315
*/
1416
class LinkDefinition(
1517
override val label: InlineContent,
1618
override val url: String,
1719
override val title: String?,
20+
override val fileSystem: FileSystem? = null,
1821
) : LinkNode,
1922
TextNode {
2023
override fun <T> accept(visitor: NodeVisitor<T>) = visitor.visit(this)

quarkdown-core/src/main/kotlin/com/quarkdown/core/ast/base/inline/Link.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class Link(
1919
override val label: InlineContent,
2020
override val url: String,
2121
override val title: String?,
22-
val fileSystem: FileSystem? = null,
22+
override val fileSystem: FileSystem? = null,
2323
) : LinkNode,
2424
TextNode {
2525
override fun <T> accept(visitor: NodeVisitor<T>) = visitor.visit(this)

quarkdown-core/src/main/kotlin/com/quarkdown/core/context/BaseContext.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.quarkdown.core.context
22

33
import com.quarkdown.core.ast.attributes.AstAttributes
4+
import com.quarkdown.core.ast.attributes.link.getResolvedUrl
45
import com.quarkdown.core.ast.base.LinkNode
56
import com.quarkdown.core.ast.base.inline.Link
67
import com.quarkdown.core.ast.base.inline.ReferenceLink
@@ -74,7 +75,7 @@ open class BaseContext(
7475
override fun resolve(reference: ReferenceLink): LinkNode? =
7576
attributes.linkDefinitions
7677
.firstOrNull { it.label.toPlainText() == reference.reference.toPlainText() }
77-
?.let { Link(reference.label, it.url, it.title) }
78+
?.let { Link(reference.label, it.getResolvedUrl(this), it.title, it.fileSystem) }
7879
?.also { link ->
7980
reference.onResolve.forEach { action -> action(link) }
8081
}

quarkdown-core/src/main/kotlin/com/quarkdown/core/context/hooks/ImagePathResolverHook.kt

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.quarkdown.core.context.hooks
2+
3+
import com.quarkdown.core.ast.attributes.link.setResolvedUrl
4+
import com.quarkdown.core.ast.base.LinkNode
5+
import com.quarkdown.core.ast.base.block.LinkDefinition
6+
import com.quarkdown.core.ast.base.inline.Image
7+
import com.quarkdown.core.ast.iterator.AstIteratorHook
8+
import com.quarkdown.core.ast.iterator.ObservableAstIterator
9+
import com.quarkdown.core.context.MutableContext
10+
import com.quarkdown.core.util.isURL
11+
import java.nio.file.Path
12+
import kotlin.io.path.Path
13+
14+
/**
15+
* Hook that resolves relative link paths based on their file system.
16+
*
17+
* If a link uses a relative path and its file system
18+
* is different from the [context]'s file system,
19+
* the path is resolved relative to the context's file system.
20+
*
21+
* This is mainly applied to images.
22+
*
23+
* @param context root context to use for resolution
24+
* @see com.quarkdown.core.ast.attributes.link.ResolvedImagePathProperty
25+
*/
26+
class LinkUrlResolverHook(
27+
private val context: MutableContext,
28+
) : AstIteratorHook {
29+
/**
30+
* Resolves the URL of a [link] if it's a relative path
31+
* and its file system is different from the [context]'s file system.
32+
*
33+
* @param link link node to resolve
34+
*/
35+
private fun resolve(link: LinkNode) {
36+
val fileSystem = link.fileSystem
37+
if (fileSystem == null || fileSystem.isRoot) return // No need to resolve paths.
38+
39+
if (link.url.isURL || Path(link.url).isAbsolute) return // Not a relative path.
40+
41+
val resolved: Path? =
42+
context.fileSystem
43+
.relativePathTo(fileSystem)
44+
?.resolve(link.url)
45+
?.normalize()
46+
47+
resolved?.let {
48+
link.setResolvedUrl(context, it.toString())
49+
}
50+
}
51+
52+
override fun attach(iterator: ObservableAstIterator) {
53+
iterator.on<Image> { resolve(it.link) }
54+
iterator.on<LinkDefinition> { resolve(it) }
55+
}
56+
}

quarkdown-core/src/main/kotlin/com/quarkdown/core/context/hooks/MediaStorerHook.kt

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.quarkdown.core.context.hooks
22

3-
import com.quarkdown.core.ast.attributes.MutableAstAttributes
3+
import com.quarkdown.core.ast.attributes.link.getResolvedUrl
44
import com.quarkdown.core.ast.base.LinkNode
55
import com.quarkdown.core.ast.base.inline.Image
66
import com.quarkdown.core.ast.base.inline.ReferenceImage
@@ -9,38 +9,33 @@ import com.quarkdown.core.ast.iterator.ObservableAstIterator
99
import com.quarkdown.core.ast.media.StoredMediaProperty
1010
import com.quarkdown.core.context.MutableContext
1111
import com.quarkdown.core.log.Log
12-
import com.quarkdown.core.media.storage.MutableMediaStorage
1312
import com.quarkdown.core.media.storage.StoredMedia
14-
import java.io.File
1513

1614
/**
1715
* Hook that, when a node containing information about media is found,
1816
* registers it in the media [storage].
1917
* A media storage is a temporary lookup table that maps media to their paths, so that they can be resolved later.
20-
* @param storage media storage
21-
* @param workingDirectory directory from which media are resolved, in case they use relative paths
18+
* @param storage media storage where media are registered
2219
*/
2320
class MediaStorerHook(
24-
private val storage: MutableMediaStorage,
25-
private val attributes: MutableAstAttributes,
26-
private val workingDirectory: File?,
21+
private val context: MutableContext,
2722
) : AstIteratorHook {
28-
constructor(context: MutableContext) : this(
29-
context.mediaStorage,
30-
context.attributes,
31-
context.fileSystem.workingDirectory,
32-
)
33-
3423
/**
3524
* Registers a media contained within a link into the media storage
3625
* and attaches the new media to the node's extra attributes.
26+
*
27+
* [getResolvedUrl] is used rather than [LinkNode.url] in case a different URL was set by [LinkUrlResolverHook].
28+
*
3729
* @param link the link node containing the media to register.
3830
* It is also the node to attach the [StoredMediaProperty] to, into [com.quarkdown.core.ast.attributes.AstAttributes.properties]
3931
*/
4032
private fun register(link: LinkNode) {
4133
val media: StoredMedia? =
4234
try {
43-
storage.register(link.url, workingDirectory)
35+
context.mediaStorage.register(
36+
link.getResolvedUrl(context),
37+
context.fileSystem.workingDirectory,
38+
)
4439
} catch (_: IllegalArgumentException) {
4540
// If the media cannot be resolved, it is ignored and not stored.
4641
Log.warn("Media cannot be resolved: ${link.url}")
@@ -52,7 +47,7 @@ class MediaStorerHook(
5247
?.let(::StoredMediaProperty)
5348
?.also { Log.debug("Registered media: ${link.url} -> ${it.value}") }
5449
?.let {
55-
attributes.of(link) += it
50+
context.attributes.of(link) += it
5651
}
5752
}
5853

quarkdown-core/src/main/kotlin/com/quarkdown/core/flavor/base/BaseMarkdownTreeIteratorFactory.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package com.quarkdown.core.flavor.base
22

33
import com.quarkdown.core.ast.iterator.ObservableAstIterator
44
import com.quarkdown.core.context.MutableContext
5-
import com.quarkdown.core.context.hooks.ImagePathResolverHook
65
import com.quarkdown.core.context.hooks.LinkDefinitionRegistrationHook
6+
import com.quarkdown.core.context.hooks.LinkUrlResolverHook
77
import com.quarkdown.core.context.hooks.SubdocumentRegistrationHook
88
import com.quarkdown.core.context.hooks.presence.CodePresenceHook
99
import com.quarkdown.core.context.hooks.presence.MathPresenceHook
@@ -22,7 +22,7 @@ class BaseMarkdownTreeIteratorFactory : TreeIteratorFactory {
2222
// Registers subdocuments.
2323
.attach(SubdocumentRegistrationHook(context))
2424
// Resolves local URLs/paths for links and images loaded from different base paths.
25-
.attach(ImagePathResolverHook(context))
25+
.attach(LinkUrlResolverHook(context))
2626
// Resolves footnotes.
2727
.attach(FootnoteResolverHook(context))
2828
// Allows loading code libraries (e.g. highlight.js syntax highlighting)

quarkdown-core/src/main/kotlin/com/quarkdown/core/parser/BlockTokenParser.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ class BlockTokenParser(
178178
url = groups.next().trim(),
179179
// Remove first and last character
180180
title = groups.nextOrNull()?.trimDelimiters()?.trim(),
181+
fileSystem = context.fileSystem,
181182
)
182183
}
183184

0 commit comments

Comments
 (0)