I am at the moment creating a chatbot for an iOS app, leveraging the OpenAI API. One of many challenges I am going through is dynamically making a desk grid. My goal is to show every NSAttributedString inside a dynamically generated grid, mirroring the construction depicted right here:
what i get proper now from my chatbot response which is openai api is the road breaks like this “—|—|—|—|— |
” which isn’t skilled:
The codes for desk grid want to seem contained in the:
mutating inside func visitTable(_ desk: Desk) -> NSAttributedString {
let consequence = NSMutableAttributedString()
return consequence
}
These are my present codes, the place I am utilizing Markdownosaur to focus on the functioning parts. My query is centered across the strains that separate the rows and columns. I am in search of refinements to realize a professional-looking desk grid, akin to the one depicted within the first picture. How can I improve the presentation of the desk strains for a extra polished look?
//
// MarkdownAttributedStringParser.swift
// Besa
//
// Created by Artit on 12/01/24.
//
import Basis
import UIKit
import Markdown
import Highlighter
/// Based mostly on the supply code from Christian Selig
/// https://github.com/christianselig/Markdownosaur/blob/major/Sources/Markdownosaur/Markdownosaur.swift
public struct MarkdownAttributedStringParser: MarkupVisitor {
let baseFontSize: CGFloat = UIFont.preferredFont(forTextStyle: .physique).pointSize
let highlighter: Highlighter = {
let highlighter = Highlighter()!
highlighter.setTheme("stackoverflow-dark")
return highlighter
}()
let newLineFontSize: CGFloat = 12
public init() {}
public mutating func attributedString(from doc: Doc) -> NSAttributedString {
return go to(doc)
}
mutating func parserResults(from doc: Doc) -> [ParserResult] {
var outcomes = [ParserResult]()
var currentAttrString = NSMutableAttributedString()
func appendCurrentAttrString(tableData: [[NSAttributedString]]) {
if !currentAttrString.string.isEmpty {
let currentAttrStringToAppend = (attempt? AttributedString(currentAttrString, together with: .uiKit)) ?? AttributedString(stringLiteral: currentAttrString.string)
outcomes.append(.init(attributedString: currentAttrStringToAppend, isCodeBlock: false, codeBlockLanguage: nil))
}
}
doc.youngsters.forEach { markup in
let attrString = go to(markup)
if let codeBlock = markup as? CodeBlock {
appendCurrentAttrString(tableData: [])
let attrStringToAppend = (attempt? AttributedString(attrString, together with: .uiKit)) ?? AttributedString(stringLiteral: attrString.string)
outcomes.append(.init(attributedString: attrStringToAppend, isCodeBlock: true, codeBlockLanguage: codeBlock.language))
currentAttrString = NSMutableAttributedString()
} else {
currentAttrString.append(attrString)
}
}
appendCurrentAttrString(tableData: [])
return outcomes
}
mutating public func defaultVisit(_ markup: Markup) -> NSAttributedString {
let consequence = NSMutableAttributedString()
for baby in markup.youngsters {
consequence.append(go to(baby))
}
return consequence
}
mutating public func visitText(_ textual content: Textual content) -> NSAttributedString {
return NSAttributedString(string: textual content.plainText, attributes: [.font: UIFont.systemFont(ofSize: baseFontSize, weight: .regular)])
}
mutating public func visitEmphasis(_ emphasis: Emphasis) -> NSAttributedString {
let consequence = NSMutableAttributedString()
for baby in emphasis.youngsters {
consequence.append(go to(baby))
}
consequence.applyEmphasis()
return consequence
}
mutating inside func visitTable(_ desk: Desk) -> NSAttributedString {
let consequence = NSMutableAttributedString()
return consequence
}
mutating public func visitStrong(_ sturdy: Sturdy) -> NSAttributedString {
let consequence = NSMutableAttributedString()
for baby in sturdy.youngsters {
consequence.append(go to(baby))
}
consequence.applyStrong()
return consequence
}
mutating public func visitParagraph(_ paragraph: Paragraph) -> NSAttributedString {
let consequence = NSMutableAttributedString()
for baby in paragraph.youngsters {
consequence.append(go to(baby))
}
if paragraph.hasSuccessor {
consequence.append(paragraph.isContainedInList ? .singleNewline(withFontSize: newLineFontSize) : .doubleNewline(withFontSize: newLineFontSize))
}
return consequence
}
mutating public func visitHeading(_ heading: Heading) -> NSAttributedString {
let consequence = NSMutableAttributedString()
for baby in heading.youngsters {
consequence.append(go to(baby))
}
consequence.applyHeading(withLevel: heading.degree)
if heading.hasSuccessor {
consequence.append(.doubleNewline(withFontSize: newLineFontSize))
}
return consequence
}
mutating public func visitLink(_ hyperlink: Hyperlink) -> NSAttributedString {
let consequence = NSMutableAttributedString()
for baby in hyperlink.youngsters {
consequence.append(go to(baby))
}
let url = hyperlink.vacation spot != nil ? URL(string: hyperlink.vacation spot!) : nil
consequence.applyLink(withURL: url)
return consequence
}
mutating public func visitInlineCode(_ inlineCode: InlineCode) -> NSAttributedString {
return NSAttributedString(string: inlineCode.code, attributes: [.font: UIFont.monospacedSystemFont(ofSize: baseFontSize - 1.0, weight: .regular), .foregroundColor: UIColor.systemPink])
}
public func visitCodeBlock(_ codeBlock: CodeBlock) -> NSAttributedString {
let consequence = NSMutableAttributedString(attributedString: highlighter.spotlight(codeBlock.code, as: codeBlock.language) ?? NSAttributedString(string: codeBlock.code))
if codeBlock.hasSuccessor {
consequence.append(.singleNewline(withFontSize: newLineFontSize))
}
return consequence
}
mutating public func visitStrikethrough(_ strikethrough: Strikethrough) -> NSAttributedString {
let consequence = NSMutableAttributedString()
for baby in strikethrough.youngsters {
consequence.append(go to(baby))
}
consequence.applyStrikethrough()
return consequence
}
mutating public func visitUnorderedList(_ unorderedList: UnorderedList) -> NSAttributedString {
let consequence = NSMutableAttributedString()
let font = UIFont.systemFont(ofSize: baseFontSize, weight: .common)
for listItem in unorderedList.listItems {
var listItemAttributes: [NSAttributedString.Key: Any] = [:]
let listItemParagraphStyle = NSMutableParagraphStyle()
let baseLeftMargin: CGFloat = 15.0
let leftMarginOffset = baseLeftMargin + (20.0 * CGFloat(unorderedList.listDepth))
let spacingFromIndex: CGFloat = 8.0
let bulletWidth = ceil(NSAttributedString(string: "•", attributes: [.font: font]).dimension().width)
let firstTabLocation = leftMarginOffset + bulletWidth
let secondTabLocation = firstTabLocation + spacingFromIndex
listItemParagraphStyle.tabStops = [
NSTextTab(textAlignment: .right, location: firstTabLocation),
NSTextTab(textAlignment: .left, location: secondTabLocation)
]
listItemParagraphStyle.headIndent = secondTabLocation
listItemAttributes[.paragraphStyle] = listItemParagraphStyle
listItemAttributes[.font] = UIFont.systemFont(ofSize: baseFontSize, weight: .common)
listItemAttributes[.listDepth] = unorderedList.listDepth
let listItemAttributedString = go to(listItem).mutableCopy() as! NSMutableAttributedString
listItemAttributedString.insert(NSAttributedString(string: "t•t", attributes: listItemAttributes), at: 0)
consequence.append(listItemAttributedString)
}
if unorderedList.hasSuccessor {
consequence.append(.doubleNewline(withFontSize: newLineFontSize))
}
return consequence
}
mutating public func visitListItem(_ listItem: ListItem) -> NSAttributedString {
let consequence = NSMutableAttributedString()
for baby in listItem.youngsters {
consequence.append(go to(baby))
}
if listItem.hasSuccessor {
consequence.append(.singleNewline(withFontSize: newLineFontSize))
}
return consequence
}
mutating public func visitOrderedList(_ orderedList: OrderedList) -> NSAttributedString {
let consequence = NSMutableAttributedString()
for (index, listItem) in orderedList.listItems.enumerated() {
var listItemAttributes: [NSAttributedString.Key: Any] = [:]
let font = UIFont.systemFont(ofSize: baseFontSize, weight: .common)
let numeralFont = UIFont.monospacedDigitSystemFont(ofSize: baseFontSize, weight: .common)
let listItemParagraphStyle = NSMutableParagraphStyle()
// Implement a base quantity to be spaced from the left facet always to higher visually differentiate it as a listing
let baseLeftMargin: CGFloat = 15.0
let leftMarginOffset = baseLeftMargin + (20.0 * CGFloat(orderedList.listDepth))
// Seize the very best quantity to be displayed and measure its width (sure usually some digits are wider than others however since we're utilizing the numeral mono font all would be the similar width on this case)
let highestNumberInList = orderedList.childCount
let numeralColumnWidth = ceil(NSAttributedString(string: "(highestNumberInList).", attributes: [.font: numeralFont]).dimension().width)
let spacingFromIndex: CGFloat = 8.0
let firstTabLocation = leftMarginOffset + numeralColumnWidth
let secondTabLocation = firstTabLocation + spacingFromIndex
listItemParagraphStyle.tabStops = [
NSTextTab(textAlignment: .right, location: firstTabLocation),
NSTextTab(textAlignment: .left, location: secondTabLocation)
]
listItemParagraphStyle.headIndent = secondTabLocation
listItemAttributes[.paragraphStyle] = listItemParagraphStyle
listItemAttributes[.font] = font
listItemAttributes[.listDepth] = orderedList.listDepth
let listItemAttributedString = go to(listItem).mutableCopy() as! NSMutableAttributedString
// Identical as the traditional record attributes, however for prettiness in formatting we need to use the cool monospaced numeral font
var numberAttributes = listItemAttributes
numberAttributes[.font] = numeralFont
let numberAttributedString = NSAttributedString(string: "t(index + 1).t", attributes: numberAttributes)
listItemAttributedString.insert(numberAttributedString, at: 0)
consequence.append(listItemAttributedString)
}
if orderedList.hasSuccessor {
consequence.append(orderedList.isContainedInList ? .singleNewline(withFontSize: newLineFontSize) : .doubleNewline(withFontSize: newLineFontSize))
}
return consequence
}
mutating public func visitBlockQuote(_ blockQuote: BlockQuote) -> NSAttributedString {
let consequence = NSMutableAttributedString()
for baby in blockQuote.youngsters {
var quoteAttributes: [NSAttributedString.Key: Any] = [:]
let quoteParagraphStyle = NSMutableParagraphStyle()
let baseLeftMargin: CGFloat = 15.0
let leftMarginOffset = baseLeftMargin + (20.0 * CGFloat(blockQuote.quoteDepth))
quoteParagraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: leftMarginOffset)]
quoteParagraphStyle.headIndent = leftMarginOffset
quoteAttributes[.paragraphStyle] = quoteParagraphStyle
quoteAttributes[.font] = UIFont.systemFont(ofSize: baseFontSize, weight: .common)
quoteAttributes[.listDepth] = blockQuote.quoteDepth
let quoteAttributedString = go to(baby).mutableCopy() as! NSMutableAttributedString
quoteAttributedString.insert(NSAttributedString(string: "t", attributes: quoteAttributes), at: 0)
quoteAttributedString.addAttribute(.foregroundColor, worth: UIColor.systemGray)
consequence.append(quoteAttributedString)
}
if blockQuote.hasSuccessor {
consequence.append(.doubleNewline(withFontSize: newLineFontSize))
}
return consequence
}
}
// MARK: - Extensions Land
extension NSMutableAttributedString {
func applyEmphasis() {
enumerateAttribute(.font, in: NSRange(location: 0, size: size), choices: []) { worth, vary, cease in
guard let font = worth as? UIFont else { return }
let newFont = font.apply(newTraits: .traitItalic)
addAttribute(.font, worth: newFont, vary: vary)
}
}
func applyStrong() {
enumerateAttribute(.font, in: NSRange(location: 0, size: size), choices: []) { worth, vary, cease in
guard let font = worth as? UIFont else { return }
let newFont = font.apply(newTraits: .traitBold)
addAttribute(.font, worth: newFont, vary: vary)
}
}
func applyLink(withURL url: URL?) {
addAttribute(.foregroundColor, worth: UIColor.systemBlue)
if let url = url {
addAttribute(.hyperlink, worth: url)
}
}
func applyBlockquote() {
addAttribute(.foregroundColor, worth: UIColor.systemGray)
}
func applyHeading(withLevel headingLevel: Int) {
enumerateAttribute(.font, in: NSRange(location: 0, size: size), choices: []) { worth, vary, cease in
guard let font = worth as? UIFont else { return }
let newFont = font.apply(newTraits: .traitBold, newPointSize: 28.0 - CGFloat(headingLevel * 2))
addAttribute(.font, worth: newFont, vary: vary)
}
}
func applyStrikethrough() {
addAttribute(.strikethroughStyle, worth: NSUnderlineStyle.single.rawValue)
}
}
extension UIFont {
func apply(newTraits: UIFontDescriptor.SymbolicTraits, newPointSize: CGFloat? = nil) -> UIFont {
var existingTraits = fontDescriptor.symbolicTraits
existingTraits.insert(newTraits)
guard let newFontDescriptor = fontDescriptor.withSymbolicTraits(existingTraits) else { return self }
return UIFont(descriptor: newFontDescriptor, dimension: newPointSize ?? pointSize)
}
}
extension ListItemContainer {
/// Depth of the record if nested inside others. Index begins at 0.
var listDepth: Int {
var index = 0
var currentElement = mother or father
whereas currentElement != nil {
if currentElement is ListItemContainer {
index += 1
}
currentElement = currentElement?.mother or father
}
return index
}
}
extension BlockQuote {
/// Depth of the quote if nested inside others. Index begins at 0.
var quoteDepth: Int {
var index = 0
var currentElement = mother or father
whereas currentElement != nil {
if currentElement is BlockQuote {
index += 1
}
currentElement = currentElement?.mother or father
}
return index
}
}
extension NSAttributedString.Key {
static let listDepth = NSAttributedString.Key("ListDepth")
static let quoteDepth = NSAttributedString.Key("QuoteDepth")
}
extension NSMutableAttributedString {
func addAttribute(_ title: NSAttributedString.Key, worth: Any) {
addAttribute(title, worth: worth, vary: NSRange(location: 0, size: size))
}
func addAttributes(_ attrs: [NSAttributedString.Key : Any]) {
addAttributes(attrs, vary: NSRange(location: 0, size: size))
}
}
extension Markup {
/// Returns true if this ingredient has sibling components after it.
var hasSuccessor: Bool {
guard let childCount = mother or father?.childCount else { return false }
return indexInParent < childCount - 1
}
var isContainedInList: Bool {
var currentElement = mother or father
whereas currentElement != nil {
if currentElement is ListItemContainer {
return true
}
currentElement = currentElement?.mother or father
}
return false
}
}
extension NSAttributedString {
static func singleNewline(withFontSize fontSize: CGFloat) -> NSAttributedString {
return NSAttributedString(string: "n", attributes: [.font: UIFont.systemFont(ofSize: fontSize)])
}
static func doubleNewline(withFontSize fontSize: CGFloat) -> NSAttributedString {
return NSAttributedString(string: "nn", attributes: [.font: UIFont.systemFont(ofSize: fontSize, weight: .regular)])
}
}