Skip to content

Commit 587f0e6

Browse files
authored
Merge branch 'main' into pagenumbering
2 parents 10113fc + b952e9e commit 587f0e6

File tree

46 files changed

+820
-117
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+820
-117
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.quarkdown.core.ast.attributes.link
2+
3+
import com.quarkdown.core.ast.base.LinkNode
4+
import com.quarkdown.core.context.Context
5+
import com.quarkdown.core.context.MutableContext
6+
import com.quarkdown.core.property.Property
7+
8+
/**
9+
* [Property], assigned to each image link, that points to a local relative URL (path) that is different from the original.
10+
* For instance, an image may have a link to `images/picture.png`,
11+
* but if it's loaded from an included document with a different base path, it may be resolved to, for example, `../images/picture.png`.
12+
* @see com.quarkdown.core.ast.base.inline.Link
13+
* @see com.quarkdown.core.context.hooks.LinkUrlResolverHook for the storing stage
14+
*/
15+
data class ResolvedLinkUrlProperty(
16+
override val value: String,
17+
) : Property<String> {
18+
companion object : Property.Key<String>
19+
20+
override val key = ResolvedLinkUrlProperty
21+
}
22+
23+
/**
24+
* @param context context where resolution data is stored
25+
* @return the resolved URL of this node within the document handled by [context],
26+
* or the regular URL if a resolved one is not registered
27+
*/
28+
fun LinkNode.getResolvedUrl(context: Context): String =
29+
context.attributes.of(this)[ResolvedLinkUrlProperty]
30+
?: this.url
31+
32+
/**
33+
* Registers the resolved path of this node within the document handled by [context].
34+
* @param context context where resolution data is stored
35+
* @param resolvedUrl resolved URL to set
36+
* @see com.quarkdown.core.context.hooks.LinkUrlResolverHook
37+
*/
38+
fun LinkNode.setResolvedUrl(
39+
context: MutableContext,
40+
resolvedUrl: String,
41+
) {
42+
context.attributes.of(this) += ResolvedLinkUrlProperty(resolvedUrl)
43+
}

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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,21 @@ import com.quarkdown.core.ast.Node
55
import com.quarkdown.core.ast.base.LinkNode
66
import com.quarkdown.core.ast.base.TextNode
77
import com.quarkdown.core.ast.base.block.LinkDefinition
8+
import com.quarkdown.core.context.file.FileSystem
89
import com.quarkdown.core.visitor.node.NodeVisitor
910

1011
/**
1112
* A link.
1213
* @param label inline content of the displayed label
1314
* @param url URL this link points to
1415
* @param title optional title
16+
* @param fileSystem optional file system where this link is defined, used for resolving relative paths
1517
*/
1618
class Link(
1719
override val label: InlineContent,
1820
override val url: String,
1921
override val title: String?,
22+
override val fileSystem: FileSystem? = null,
2023
) : LinkNode,
2124
TextNode {
2225
override fun <T> accept(visitor: NodeVisitor<T>) = visitor.visit(this)

quarkdown-core/src/main/kotlin/com/quarkdown/core/ast/dsl/InlineAstBuilder.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.quarkdown.core.ast.base.inline.Text
1111
import com.quarkdown.core.ast.quarkdown.inline.InlineCollapse
1212
import com.quarkdown.core.ast.quarkdown.inline.TextTransform
1313
import com.quarkdown.core.ast.quarkdown.inline.TextTransformData
14+
import com.quarkdown.core.context.file.SimpleFileSystem
1415
import com.quarkdown.core.document.size.Size
1516

1617
/**
@@ -64,7 +65,12 @@ class InlineAstBuilder : AstBuilder() {
6465
height: Size? = null,
6566
referenceId: String? = null,
6667
label: InlineAstBuilder.() -> Unit = {},
67-
) = +Image(Link(buildInline(label), url, title), width, height, referenceId)
68+
) = +Image(
69+
Link(buildInline(label), url, title, fileSystem = SimpleFileSystem()),
70+
width,
71+
height,
72+
referenceId,
73+
)
6874

6975
/**
7076
* @see InlineCollapse

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

Lines changed: 4 additions & 2 deletions
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
}
@@ -105,5 +106,6 @@ open class BaseContext(
105106
?: throw LocalizationKeyNotFoundException(tableName, locale, key)
106107
}
107108

108-
override fun fork(subdocument: Subdocument): ScopeContext = ScopeContext(parent = this, subdocument)
109+
override fun fork(subdocument: Subdocument): ScopeContext =
110+
throw UnsupportedOperationException("Forking is not supported in BaseContext")
109111
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.quarkdown.core.context
2+
3+
/**
4+
* A [Context] that has a parent context, forming a scope tree.
5+
* This context can access its parent's properties and inherit them.
6+
* @param C type of the parent context
7+
*/
8+
interface ChildContext<C : Context> : Context {
9+
/**
10+
* The parent context of this context in the scope tree.
11+
*/
12+
val parent: C
13+
14+
/**
15+
* @param predicate condition to match
16+
* @return the last context (upwards, towards the root, starting from this context) that matches the [predicate],
17+
* or `null` if no parent in the scope tree matches the given condition
18+
*/
19+
fun lastParentOrNull(predicate: (Context) -> Boolean): Context? =
20+
when {
21+
// This is the last context to match the condition.
22+
predicate(this) && !predicate(parent) -> this
23+
// The root context matches the condition.
24+
parent !is ChildContext<*> && predicate(parent) -> parent
25+
// Scan the parent context.
26+
else -> (parent as? ChildContext<*>)?.lastParentOrNull(predicate)
27+
}
28+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.quarkdown.core.function.call.FunctionCall
1010
import com.quarkdown.core.function.library.Library
1111
import com.quarkdown.core.function.library.LibraryRegistrant
1212
import com.quarkdown.core.graph.VisitableOnceGraph
13+
import com.quarkdown.core.localization.MutableLocalizationTables
1314
import com.quarkdown.core.media.storage.MutableMediaStorage
1415

1516
/**
@@ -33,7 +34,7 @@ open class MutableContext(
3334

3435
override val loadableLibraries: MutableSet<Library> = (super.loadableLibraries + loadableLibraries).toMutableSet()
3536

36-
override val localizationTables = super.localizationTables.toMutableMap()
37+
override val localizationTables: MutableLocalizationTables = super.localizationTables.toMutableMap()
3738

3839
override val mediaStorage: MutableMediaStorage
3940
get() = super.mediaStorage as MutableMediaStorage
@@ -71,6 +72,8 @@ open class MutableContext(
7172
it.onComplete = { attributes.functionCalls.remove(call) }
7273
}
7374

75+
override fun fork(subdocument: Subdocument): ScopeContext = ScopeContext(parent = this, subdocument)
76+
7477
/**
7578
* Loads a loadable library by name and registers it in the context.
7679
* After a successful load, the library is removed from [loadableLibraries] and added to [libraries],
Lines changed: 12 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,39 @@
11
package com.quarkdown.core.context
22

3-
import com.quarkdown.core.ast.attributes.MutableAstAttributes
4-
import com.quarkdown.core.ast.quarkdown.FunctionCallNode
5-
import com.quarkdown.core.document.DocumentInfo
63
import com.quarkdown.core.document.sub.Subdocument
74
import com.quarkdown.core.function.Function
8-
import com.quarkdown.core.function.library.Library
9-
import com.quarkdown.core.graph.VisitableOnceGraph
10-
import com.quarkdown.core.graph.visitableOnce
11-
import com.quarkdown.core.media.storage.MutableMediaStorage
125
import com.quarkdown.core.pipeline.Pipeline
136

147
/**
158
* A context that is the result of a fork from an original parent [Context].
16-
* Several properties are inherited from it.
9+
* All properties are inherited from it, but not all, such as libraries, are shared mutably.
1710
* @param parent context this scope was forked from
1811
* @param subdocument the subdocument this context is processing
1912
*/
20-
class ScopeContext(
21-
val parent: Context,
13+
open class ScopeContext(
14+
override val parent: MutableContext,
2215
subdocument: Subdocument = parent.subdocument,
2316
) : MutableContext(
2417
flavor = parent.flavor,
2518
libraries = emptySet(),
2619
subdocument = subdocument,
27-
) {
20+
),
21+
ChildContext<MutableContext> {
2822
override val attachedPipeline: Pipeline?
2923
get() = super.attachedPipeline ?: parent.attachedPipeline
3024

31-
override var documentInfo: DocumentInfo
32-
get() = parent.documentInfo
33-
set(value) {
34-
(parent as? MutableContext)?.documentInfo = value
35-
}
36-
37-
override val options: MutableContextOptions
38-
get() = parent.options as? MutableContextOptions ?: MutableContextOptions()
39-
40-
override val attributes: MutableAstAttributes
41-
get() = parent.attributes as? MutableAstAttributes ?: parent.attributes.toMutable()
42-
43-
override val loadableLibraries: MutableSet<Library>
44-
get() = (parent as? MutableContext)?.loadableLibraries ?: super.loadableLibraries
45-
46-
override val localizationTables
47-
get() = (parent as? MutableContext)?.localizationTables ?: parent.localizationTables.toMutableMap()
48-
49-
override val mediaStorage: MutableMediaStorage
50-
get() = parent.mediaStorage as? MutableMediaStorage ?: MutableMediaStorage(options)
51-
52-
override var subdocumentGraph: VisitableOnceGraph<Subdocument>
53-
get() = parent.subdocumentGraph as? VisitableOnceGraph ?: parent.subdocumentGraph.visitableOnce
54-
set(value) {
55-
(parent as? MutableContext)?.subdocumentGraph = value
56-
}
25+
override var documentInfo by parent::documentInfo
26+
override val options by parent::options
27+
override val attributes by parent::attributes
28+
override val loadableLibraries by parent::loadableLibraries
29+
override val localizationTables by parent::localizationTables
30+
override val mediaStorage by parent::mediaStorage
31+
override var subdocumentGraph by parent::subdocumentGraph
5732

5833
/**
5934
* If no matching function is found among this [ScopeContext]'s own [libraries],
6035
* [parent]'s libraries are scanned.
6136
* @see Context.getFunctionByName
6237
*/
6338
override fun getFunctionByName(name: String): Function<*>? = super.getFunctionByName(name) ?: parent.getFunctionByName(name)
64-
65-
/**
66-
* Enqueues a function call to the [parent]'s queue if it is a [MutableContext],
67-
* or to this context otherwise.
68-
* This lets the registration go up the context tree so that it can be expanded
69-
* from the root context in the next stage of the pipeline.
70-
* @param functionCall function call to register
71-
* @see MutableContext.register
72-
*/
73-
override fun register(functionCall: FunctionCallNode) {
74-
(parent as? MutableContext)?.register(functionCall)
75-
?: super.register(functionCall)
76-
}
77-
78-
/**
79-
* @param predicate condition to match
80-
* @return the last context (upwards, towards the root, starting from this context) that matches the [predicate],
81-
* or `null` if no parent in the scope tree matches the given condition
82-
*/
83-
fun lastParentOrNull(predicate: (Context) -> Boolean): Context? =
84-
when {
85-
// This is the last context to match the condition.
86-
predicate(this) && !predicate(parent) -> this
87-
// The root context matches the condition.
88-
parent !is ScopeContext && predicate(parent) -> parent
89-
// Scan the parent context.
90-
else -> (parent as? ScopeContext)?.lastParentOrNull(predicate)
91-
}
9239
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.quarkdown.core.context
2+
3+
import com.quarkdown.core.context.file.FileSystem
4+
import com.quarkdown.core.function.Function
5+
6+
/**
7+
* A context that shares all of its properties with its parent [MutableContext].
8+
* This is useful when a context needs to be forked, for example to update its [fileSystem], but still its state mutably.
9+
* @param parent context this shared context was forked from
10+
* @param fileSystem file system to use in this context
11+
*/
12+
open class SharedContext(
13+
override val parent: MutableContext,
14+
override val fileSystem: FileSystem = parent.fileSystem,
15+
) : MutableContext(
16+
flavor = parent.flavor,
17+
libraries = emptySet(),
18+
subdocument = parent.subdocument,
19+
),
20+
ChildContext<MutableContext> {
21+
override val attachedPipeline by parent::attachedPipeline
22+
override var documentInfo by parent::documentInfo
23+
override val libraries by parent::libraries
24+
override val options by parent::options
25+
override val attributes by parent::attributes
26+
override val loadableLibraries by parent::loadableLibraries
27+
override val localizationTables by parent::localizationTables
28+
override val mediaStorage by parent::mediaStorage
29+
override var subdocumentGraph by parent::subdocumentGraph
30+
31+
override fun getFunctionByName(name: String): Function<*>? = parent.getFunctionByName(name)
32+
}

0 commit comments

Comments
 (0)