[SOLVED] Capturing attributes of markdown into array/dictionary in Swift

Issue

I am converting some Markdown into HTML using a Swift framework.

I want to be able to create custom Markdown elements that suit my needs outside of the normal default ones most frameworks provide.

Say I have the following custom Markdown:

# My heading

This is normal text with a [link](/).

Below is my custom markdown element called `!file`:

[!file title="This is my title" icon="rocket"](file.txt)

How would I be able to extract the attributes into an array or dictionary so I can convert them into HTML?

For example:

// from this: [!file title="This is my title" icon="rocket"](file.txt)

attributes = [
  "title" : "This is my title",
  "icon" : "rocket",
  "item" : "file.txt"
]

or from this: [!file](../files/docs/terms.pdf)

attributes = [
  "title" : "",
  "icon" : "",
  "item" : "../files/docs/terms.pdf"
]

I originally tried using .split(" ") but since the title="This is my title" contains spaces then it splits at those items.

I imagine the title and the icon are optionals and are nil by default.

I haven’t really used Swift outside the standard iOS/macOS usage so when relying on only Foundation I’m a bit lost.

Solution

If I haven’t misunderstood this completely you need a regex to match the custom !file element in a text and convert the result into a swift collection.

For this I used a regex pattern with named groups

let pattern = #"\[!file\s*(title="(?<title>.*)")?\s*(icon="(?<icon>.*)")?\]\((?<file>.*)\)"#

or you could use the pattern from the answer by @RizwanM.Tuman which might be more effective and looks like this when using named groups

let pattern = #"\[!file(?:\s*title="(?<title>[^"]*?)"\s*icon="(?<icon>[^"]*?)")?]\((?<file>[^)]+)\)"#

Then the matching and extracting of the result would be done like this

let regex = try NSRegularExpression(pattern: pattern, options: [])
let fullRange = NSRange(text.startIndex..<text.endIndex, in: text)

var components = [String: String]()
if let match = regex.firstMatch(in: text, options: [], range: fullRange) {
    for component in ["title", "icon", "file"] {
        let componentRange = match.range(withName: component)
        if componentRange.location != NSNotFound,
           let range = Range(componentRange, in: text)
        {
            components[component] = String(text[range])
        }
    }
}

This assumes there is only one of this custom element to match but if you have several you need to iterate over the matches like this

var allMarkdowns = [[String: String]]()
regex.enumerateMatches(in: text, options: [], range: fullRange) { (match, _, _) in
    guard let match = match else { return }

    var components = [String: String]()
    for component in ["title", "icon", "file"] {
        let componentRange = match.range(withName: component)
        if componentRange.location != NSNotFound,
           let range = Range(componentRange, in: text) {
            components[component] = String(text[range])
        }
    }
    allMarkdowns.append(components)
}

An example

let text = """
# My heading
This is normal text with a [link](/).
Below is my custom markdown element called `!file`:

[!file title="This is my title" icon="rocket"](file.txt)
bla bla

[!file](../files/docs/terms.pdf)
"""

Running the second solution on this would yield

[["file": "file.txt", "title": "This is my title", "icon": "rocket"], ["file": "../files/docs/terms.pdf"]]

Answered By – Joakim Danielson

Answer Checked By – Mary Flores (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published.