Skip to content

Commit 3e206ca

Browse files
committed
Allow customization of image sizes in the AttributedStringGenerator. Support relative image links in the AttributedStringGenerator.
1 parent c057bf1 commit 3e206ca

File tree

4 files changed

+185
-12
lines changed

4 files changed

+185
-12
lines changed

MarkdownKit.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
CCD07466229FDFB90053B73C /* InlineParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCD07465229FDFB90053B73C /* InlineParser.swift */; };
8888
CCD0746822A060CF0053B73C /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCD0746722A060CF0053B73C /* Text.swift */; };
8989
CCD0746A22A324400053B73C /* InlineTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCD0746922A324400053B73C /* InlineTransformer.swift */; };
90+
CCEF3E3127CA6B8800FB56EB /* MarkdownASTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCEF3E3027CA6B8800FB56EB /* MarkdownASTests.swift */; };
9091
/* End PBXBuildFile section */
9192

9293
/* Begin PBXContainerItemProxy section */
@@ -178,6 +179,7 @@
178179
CCD07465229FDFB90053B73C /* InlineParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineParser.swift; sourceTree = "<group>"; };
179180
CCD0746722A060CF0053B73C /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = "<group>"; };
180181
CCD0746922A324400053B73C /* InlineTransformer.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = InlineTransformer.swift; sourceTree = "<group>"; };
182+
CCEF3E3027CA6B8800FB56EB /* MarkdownASTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownASTests.swift; sourceTree = "<group>"; };
181183
/* End PBXFileReference section */
182184

183185
/* Begin PBXFrameworksBuildPhase section */
@@ -287,6 +289,7 @@
287289
CC24757224C1EBB600678C59 /* ExtendedMarkdownBlockTests.swift */,
288290
CC7B603822AC67DC0092188C /* MarkdownInlineTests.swift */,
289291
CC088C2322E337BD0059460E /* MarkdownHtmlTests.swift */,
292+
CCEF3E3027CA6B8800FB56EB /* MarkdownASTests.swift */,
290293
CC8D559C24C3560700E11F96 /* ExtendedMarkdownHtmlTests.swift */,
291294
CC55DD87264B19D8001AAD10 /* MarkdownExtension.swift */,
292295
CCC48C0325D98FCB00D082AD /* MarkdownStringTests.swift */,
@@ -540,6 +543,7 @@
540543
files = (
541544
CC7B603922AC67DC0092188C /* MarkdownInlineTests.swift in Sources */,
542545
CC24757324C1EBB600678C59 /* ExtendedMarkdownBlockTests.swift in Sources */,
546+
CCEF3E3127CA6B8800FB56EB /* MarkdownASTests.swift in Sources */,
543547
CC55DD88264B19D8001AAD10 /* MarkdownExtension.swift in Sources */,
544548
CC088C2422E337BD0059460E /* MarkdownHtmlTests.swift in Sources */,
545549
CCC48C0425D98FCB00D082AD /* MarkdownStringTests.swift in Sources */,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1240"
4+
version = "1.3">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES">
8+
<BuildActionEntries>
9+
<BuildActionEntry
10+
buildForTesting = "YES"
11+
buildForRunning = "YES"
12+
buildForProfiling = "YES"
13+
buildForArchiving = "YES"
14+
buildForAnalyzing = "YES">
15+
<BuildableReference
16+
BuildableIdentifier = "primary"
17+
BlueprintIdentifier = "CCC48B8125CA14F400D082AD"
18+
BuildableName = "libMarkdownKit iOS.a"
19+
BlueprintName = "MarkdownKit iOS"
20+
ReferencedContainer = "container:MarkdownKit.xcodeproj">
21+
</BuildableReference>
22+
</BuildActionEntry>
23+
</BuildActionEntries>
24+
</BuildAction>
25+
<TestAction
26+
buildConfiguration = "Debug"
27+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
28+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29+
shouldUseLaunchSchemeArgsEnv = "YES">
30+
<Testables>
31+
</Testables>
32+
</TestAction>
33+
<LaunchAction
34+
buildConfiguration = "Debug"
35+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
36+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
37+
launchStyle = "0"
38+
useCustomWorkingDirectory = "NO"
39+
ignoresPersistentStateOnLaunch = "NO"
40+
debugDocumentVersioning = "YES"
41+
debugServiceExtension = "internal"
42+
allowLocationSimulation = "YES">
43+
</LaunchAction>
44+
<ProfileAction
45+
buildConfiguration = "Release"
46+
shouldUseLaunchSchemeArgsEnv = "YES"
47+
savedToolIdentifier = ""
48+
useCustomWorkingDirectory = "NO"
49+
debugDocumentVersioning = "YES">
50+
<MacroExpansion>
51+
<BuildableReference
52+
BuildableIdentifier = "primary"
53+
BlueprintIdentifier = "CCC48B8125CA14F400D082AD"
54+
BuildableName = "libMarkdownKit iOS.a"
55+
BlueprintName = "MarkdownKit iOS"
56+
ReferencedContainer = "container:MarkdownKit.xcodeproj">
57+
</BuildableReference>
58+
</MacroExpansion>
59+
</ProfileAction>
60+
<AnalyzeAction
61+
buildConfiguration = "Debug">
62+
</AnalyzeAction>
63+
<ArchiveAction
64+
buildConfiguration = "Release"
65+
revealArchiveInOrganizer = "YES">
66+
</ArchiveAction>
67+
</Scheme>

Sources/MarkdownKit/AttributedString/AttributedStringGenerator.swift

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,12 @@ open class AttributedStringGenerator {
3737
/// Customized html generator to work around limitations of the current HTML to
3838
/// `NSAttributedString` conversion logic provided by the operating system.
3939
open class InternalHtmlGenerator: HtmlGenerator {
40-
weak var outer: AttributedStringGenerator?
40+
var outer: AttributedStringGenerator?
4141

4242
public init(outer: AttributedStringGenerator) {
4343
self.outer = outer
4444
}
4545

46-
open override func generate(textFragment fragment: TextFragment) -> String {
47-
switch fragment {
48-
case .custom(let customTextFragment):
49-
return customTextFragment.generateHtml(via: self, and: self.outer)
50-
default:
51-
return super.generate(textFragment: fragment)
52-
}
53-
}
54-
5546
open override func generate(block: Block, tight: Bool = false) -> String {
5647
switch block {
5748
case .list(_, _, _):
@@ -121,6 +112,33 @@ open class AttributedStringGenerator {
121112
return super.generate(block: block, tight: tight)
122113
}
123114
}
115+
116+
open override func generate(textFragment fragment: TextFragment) -> String {
117+
switch fragment {
118+
case .image(let text, let uri, let title):
119+
let titleAttr = title == nil ? "" : " title=\"\(title!)\""
120+
if let uriStr = uri {
121+
let url = URL(string: uriStr)
122+
Swift.print("=== URL0 = \(url?.absoluteString ?? "none"), \(url?.scheme == nil), \(self.outer == nil)")
123+
if (url?.scheme == nil) || (url?.isFileURL ?? false),
124+
let baseUrl = self.outer?.imageBaseUrl {
125+
let url = URL(fileURLWithPath: uriStr, relativeTo: baseUrl)
126+
Swift.print(" URL2 = \(url.absoluteString)")
127+
if url.isFileURL {
128+
return "<img src=\"\(url.absoluteURL.path)\"" +
129+
" alt=\"\(text.rawDescription)\"\(titleAttr)/>"
130+
}
131+
}
132+
return "<img src=\"\(uriStr)\" alt=\"\(text.rawDescription)\"\(titleAttr)/>"
133+
} else {
134+
return self.generate(text: text)
135+
}
136+
case .custom(let customTextFragment):
137+
return customTextFragment.generateHtml(via: self, and: self.outer)
138+
default:
139+
return super.generate(textFragment: fragment)
140+
}
141+
}
124142
}
125143

126144
/// Default `AttributedStringGenerator` implementation.
@@ -171,6 +189,19 @@ open class AttributedStringGenerator {
171189
/// The color of H4 headers.
172190
public let h4Color: String
173191

192+
/// The maximum width of an image
193+
public let maxImageWidth: String?
194+
195+
/// The maximum height of an image
196+
public let maxImageHeight: String?
197+
198+
/// Custom CSS style
199+
public let customStyle: String
200+
201+
/// If provided, this URL is used as a base URL for relative image links
202+
public let imageBaseUrl: URL?
203+
204+
174205
/// Constructor providing customization options for the generated `NSAttributedString` markup.
175206
public init(fontSize: Float = 14.0,
176207
fontFamily: String = "\"Times New Roman\",Times,serif",
@@ -187,7 +218,11 @@ open class AttributedStringGenerator {
187218
h1Color: String = mdDefaultColor,
188219
h2Color: String = mdDefaultColor,
189220
h3Color: String = mdDefaultColor,
190-
h4Color: String = mdDefaultColor) {
221+
h4Color: String = mdDefaultColor,
222+
maxImageWidth: String? = nil,
223+
maxImageHeight: String? = nil,
224+
customStyle: String = "",
225+
imageBaseUrl: URL? = nil) {
191226
self.fontSize = fontSize
192227
self.fontFamily = fontFamily
193228
self.fontColor = fontColor
@@ -203,6 +238,10 @@ open class AttributedStringGenerator {
203238
self.h2Color = h2Color
204239
self.h3Color = h3Color
205240
self.h4Color = h4Color
241+
self.maxImageWidth = maxImageWidth
242+
self.maxImageHeight = maxImageHeight
243+
self.customStyle = customStyle
244+
self.imageBaseUrl = imageBaseUrl
206245
}
207246

208247
/// Generates an attributed string from the given Markdown document
@@ -266,14 +305,16 @@ open class AttributedStringGenerator {
266305
"td.codebox { \(self.codeBoxStyle) }\n" +
267306
"td.thematic { \(self.thematicBreakStyle) }\n" +
268307
"td.quote { \(self.quoteStyle) }\n" +
308+
"img { \(self.imgStyle) }\n" +
269309
"dt {\n" +
270310
" font-weight: bold;\n" +
271311
" margin: 0.6em 0 0.4em 0;\n" +
272312
"}\n" +
273313
"dd {\n" +
274314
" margin: 0.5em 0 1em 2em;\n" +
275315
" padding: 0.5em 0 1em 2em;\n" +
276-
"}\n"
316+
"}\n" +
317+
"\(self.customStyle)\n"
277318
}
278319

279320
open var bodyStyle: String {
@@ -361,6 +402,23 @@ open class AttributedStringGenerator {
361402
"width: 0.4em;"
362403
}
363404

405+
open var imgStyle: String {
406+
if let maxWidth = self.maxImageWidth {
407+
if let maxHeight = self.maxImageHeight {
408+
return "max-width: \(maxWidth);max-height: \(maxHeight); " +
409+
"!important;width: auto;height: auto;"
410+
} else {
411+
return "max-width: \(maxWidth);max-height: 100%; " +
412+
"!important;width: auto;height: auto;"
413+
}
414+
} else if let maxHeight = self.maxImageHeight {
415+
return "max-width: 100%;max-height: \(maxHeight); " +
416+
"!important;width: auto;height: auto;"
417+
} else {
418+
return ""
419+
}
420+
}
421+
364422
open var tableStyle: String {
365423
return "border-collapse: collapse;" +
366424
"margin: 0.3em 0;" +
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// MarkdownASTests.swift
3+
// MarkdownKitTests
4+
//
5+
// Created by Matthias Zenger on 26/02/2022.
6+
// Copyright © 2022 Matthias Zenger. All rights reserved.
7+
//
8+
9+
import XCTest
10+
import MarkdownKit
11+
12+
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
13+
14+
class MarkdownASTests: XCTestCase {
15+
16+
private func generateHtml(imageBaseUrl: URL? = nil, _ str: String) -> String {
17+
return AttributedStringGenerator(imageBaseUrl: imageBaseUrl)
18+
.htmlGenerator
19+
.generate(doc: MarkdownParser.standard.parse(str))
20+
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
21+
}
22+
23+
func testRelativeImageUrls() {
24+
XCTAssertEqual(generateHtml("![Test image](imagefile.png)"),
25+
"<p><img src=\"imagefile.png\" alt=\"Test image\"/></p>")
26+
XCTAssertEqual(generateHtml(imageBaseUrl: URL(fileURLWithPath: "/global/root/path/"),
27+
"![Test image](imagefile.png)"),
28+
"<p><img src=\"/global/root/path/imagefile.png\" alt=\"Test image\"/></p>")
29+
XCTAssertEqual(generateHtml(imageBaseUrl: URL(fileURLWithPath: "/global/root/path/"),
30+
"![Test image](/imagefile.png)"),
31+
"<p><img src=\"/imagefile.png\" alt=\"Test image\"/></p>")
32+
XCTAssertEqual(generateHtml("![Test image](one/imagefile.png)"),
33+
"<p><img src=\"one/imagefile.png\" alt=\"Test image\"/></p>")
34+
XCTAssertEqual(generateHtml(imageBaseUrl: URL(fileURLWithPath: "/global/root/path/"),
35+
"![Test image](one/imagefile.png)"),
36+
"<p><img src=\"/global/root/path/one/imagefile.png\" alt=\"Test image\"/></p>")
37+
XCTAssertEqual(generateHtml(imageBaseUrl: URL(fileURLWithPath: "/global/root/path/"),
38+
"![Test image](/one/imagefile.png)"),
39+
"<p><img src=\"/one/imagefile.png\" alt=\"Test image\"/></p>")
40+
41+
}
42+
}
43+
44+
#endif

0 commit comments

Comments
 (0)