I'm trying to parse a markdown string manually and create an attributedString for UITextView
When I try to format code blocks, I found some weird bug that I can't solve. I've reduced the problem to this minimum amount of demo code. I hope someone can help me.
The problem:
All the blocks have the same styling. But the same attributes lead to different appearances, as can be seen after the first 3 backticks in each code block.
The first block is working properly. But the ones below have this weird extra space between the first line and the second line.
The 3 blocks are styled using for-loop with identical code.
Code
I've reduced the code and put it all in one swift playground file. And I'm trying really hard to make the post easy to read and understand so that I can get helped.
The direct styling code is less than 10 lines, maybe 7 lines.
I divided the code into 3 parts here in my post for readability. But they can be put together directly and run in swift playground.
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let str = """
`` `
some
`` `
`` `
some
`` `
`` `
some
`` ` <- (conflicting with stackoverflow markdown editor, so i added extra space)
"""
var at = AttributedString(stringLiteral: str)
/* === ============= === */
/* === styling start === */
/* === ↓ ↓ ↓ ↓ ↓ ↓ ↓ === */
// manually counted range and indexes for 3 code blocks
for range in [at.intToRange(start: 0, end: 12),
at.intToRange(start: 14, end: 26),
at.intToRange(start: 28, end: 40)] {
/* apply code block attributes style */
at[range].mergeAttributes(.codeBlock)
// "lower" is the range of head 3 `
let lower = range.lowerBound..<at.index(range.lowerBound, offsetByCharacters: 3)
// "upper" is the range of tail 3 `
let upper = at.index(range.upperBound, offsetByCharacters: -3)..<range.upperBound
/* apply code block prefix ` and suffix ` attributes */
at[lower].mergeAttributes(.codeBlockPrefixBacktick)
at[upper].mergeAttributes(.codeBlockSuffixBacktick)
// still in loop
/* === ↑ ↑ ↑ ↑ ↑ ↑ === */
/* === styling end === */
/* === =========== === */
/* print out prefix and suffix paragraph attributes */
/* ↓ this part has nothing to do with styling ↓ */
[lower, upper].forEach {
for i in 0..<3 {
let a = at.index($0.lowerBound, offsetByCharacters: i)
let b = at.index($0.lowerBound, offsetByCharacters: i+1)
let c = at[a..<b]
print(c.characters[a])
print(c[AttributeScopes.UIKitAttributes.ParagraphStyleAttribute])
}
}
}
/* Give the AttributedString To UITextView and display */
let tv2 = UITextView()
tv2.isScrollEnabled = true
tv2.frame = CGRectMake(0, 0, 600, 800)
tv2.attributedText = NSAttributedString(at)
view.addSubview(tv2)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Style Presets: 3 main components: fonts, indent, and spacing.
extension AttributeContainer {
// code block main body style
static let codeBlock = AttributeContainer
.font(UIFont(name: "Menlo", size: 15)!)
.paragraphStyle(AttributeContainer.codeBlockStyle)
.backgroundColor(UIColor.lightGray)
private static let codeBlockStyle:NSParagraphStyle = {
let style = NSMutableParagraphStyle()
style.headIndent = 12
style.firstLineHeadIndent = 12
style.tailIndent = 12
return style
}()
// code block prefix style
static let codeBlockPrefixBacktick = AttributeContainer
.font(UIFont(name: "Menlo", size: 17)!)
.paragraphStyle(AttributeContainer.codeBlockPrefixStyle)
.backgroundColor(UIColor.lightGray)
private static let codeBlockPrefixStyle:NSParagraphStyle = {
let style = NSMutableParagraphStyle()
style.headIndent = 12
style.firstLineHeadIndent = 12
style.tailIndent = 12
style.paragraphSpacingBefore = 24
style.paragraphSpacing = 0
return style
}()
// code block suffix style
static let codeBlockSuffixBacktick = AttributeContainer
.font(UIFont(name: "Menlo", size: 17)!)
.paragraphStyle(AttributeContainer.codeBlockSuffixStyle)
.backgroundColor(UIColor.lightGray)
static let codeBlockSuffixStyle:NSParagraphStyle = {
let style = NSMutableParagraphStyle()
style.headIndent = 12
style.firstLineHeadIndent = 12
style.tailIndent = 12
style.paragraphSpacingBefore = 0
style.paragraphSpacing = 24
return style
}()
}
Some convenience funcs
extension AttributedString {
/// convert ints to range
func intToRange(start:Int, end:Int) -> Range<Index> {
let lower = convertIntToIndex(int: start)
let upper = convertIntToIndex(int: end)
return lower..<upper
}
/// convert Int to String.Index
func convertIntToIndex(int:Int) -> Index {
characters.index(startIndex, offsetBy: int)
}
/// get character at index i
/// string[i]
subscript(i:Index) -> Character {
return characters[i]
}
}
I believe the stylings of those 3 blocks are identical because I only used a for-loop with identical code.
When I tried to print out their styling, there seems to be no issue either. Seems like the appearance is contradicting the code.
Here is the printed result of each individual backtick (`), 18 in total, look at the ParagraphSpacing
and ParagraphSpacingBefore
attributes. They seem fine.
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 24, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 24, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 24, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 24, ParagraphSpacingBefore 0, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (
), Lists (
), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 24, ParagraphSpacingBefore 0, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (
), Lists (
), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 24, ParagraphSpacingBefore 0, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (
), Lists (
), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 24, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 24, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 24, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 24, ParagraphSpacingBefore 0, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (
), Lists (
), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 24, ParagraphSpacingBefore 0, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (
), Lists (
), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 24, ParagraphSpacingBefore 0, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (
), Lists (
), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 24, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 24, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 24, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 24, ParagraphSpacingBefore 0, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (
), Lists (
), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 24, ParagraphSpacingBefore 0, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (
), Lists (
), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '')
`
Optional(Alignment 4, LineSpacing 0, ParagraphSpacing 24, ParagraphSpacingBefore 0, HeadIndent 12, TailIndent 12, FirstLineHeadIndent 12, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
28L,
56L,
84L,
112L,
140L,
168L,
196L,
224L,
252L,
280L,
308L,
336L
), DefaultTabInterval 0, Blocks (
), Lists (
), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (
) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '')
Thanks in advance if anyone can help.