├── README.md
├── Demo
├── MastoParseDemoApp
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── MastoParseDemoAppApp.swift
│ ├── ExamplePostHTML.swift
│ └── ContentView.swift
└── MastoParseDemoApp.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── swiftpm
│ │ └── Package.resolved
│ └── project.pbxproj
├── Tests
└── MastoParseTests
│ └── MastoParseTests.swift
├── Package.resolved
├── LICENSE
├── Package.swift
├── .gitignore
└── Sources
└── MastoParse
├── MastoParseContentBlock.swift
├── MastoParseHtmlTree.swift
└── MastoParseAccumulator.swift
/README.md:
--------------------------------------------------------------------------------
1 | # MastoParse
2 | Parses Mastodon post contents for use in SwiftUI
3 |
--------------------------------------------------------------------------------
/Demo/MastoParseDemoApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/MastoParseDemoApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/MastoParseDemoApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Tests/MastoParseTests/MastoParseTests.swift:
--------------------------------------------------------------------------------
1 | import Testing
2 | @testable import MastoParse
3 |
4 | @Test func example() async throws {
5 | // Write your test here and use APIs like `#expect(...)` to check expected conditions.
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/MastoParseDemoApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Demo/MastoParseDemoApp/MastoParseDemoAppApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MastoParseDemoAppApp.swift
3 | // MastoParseDemoApp
4 | //
5 | // Created by Shannon Hughes on 7/2/25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct MastoParseDemoAppApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | DemoView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "ca7e1563569a1bcf8fffdcba63f6e62ea61476227fb2265c52e8e6ff05c2f7fd",
3 | "pins" : [
4 | {
5 | "identity" : "swiftsoup",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/scinfu/SwiftSoup.git",
8 | "state" : {
9 | "revision" : "aa85ee96017a730031bafe411cde24a08a17a9c9",
10 | "version" : "2.8.8"
11 | }
12 | }
13 | ],
14 | "version" : 3
15 | }
16 |
--------------------------------------------------------------------------------
/Demo/MastoParseDemoApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "7164d7bfd1cb7736e82985d8b1abdf090c043005faa4e402636e59c18332cdfd",
3 | "pins" : [
4 | {
5 | "identity" : "swiftsoup",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/scinfu/SwiftSoup.git",
8 | "state" : {
9 | "revision" : "aa85ee96017a730031bafe411cde24a08a17a9c9",
10 | "version" : "2.8.8"
11 | }
12 | }
13 | ],
14 | "version" : 3
15 | }
16 |
--------------------------------------------------------------------------------
/Demo/MastoParseDemoApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "appearances" : [
10 | {
11 | "appearance" : "luminosity",
12 | "value" : "dark"
13 | }
14 | ],
15 | "idiom" : "universal",
16 | "platform" : "ios",
17 | "size" : "1024x1024"
18 | },
19 | {
20 | "appearances" : [
21 | {
22 | "appearance" : "luminosity",
23 | "value" : "tinted"
24 | }
25 | ],
26 | "idiom" : "universal",
27 | "platform" : "ios",
28 | "size" : "1024x1024"
29 | }
30 | ],
31 | "info" : {
32 | "author" : "xcode",
33 | "version" : 1
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Mastodon
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "MastoParse",
8 | platforms: [
9 | .macOS(.v14),
10 | .iOS(.v16)
11 | ],
12 | products: [
13 | // Products define the executables and libraries a package produces, making them visible to other packages.
14 | .library(
15 | name: "MastoParse",
16 | targets: ["MastoParse"]),
17 | ],
18 | dependencies: [
19 | .package(url: "https://github.com/scinfu/SwiftSoup.git", .upToNextMajor(from: "2.8.8")),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package, defining a module or a test suite.
23 | // Targets can depend on other targets in this package and products from dependencies.
24 | .target(
25 | name: "MastoParse",
26 | dependencies: ["SwiftSoup"]
27 | ),
28 | .testTarget(
29 | name: "MastoParseTests",
30 | dependencies: ["MastoParse"]
31 | ),
32 | ]
33 | )
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 | .DS_Store
8 |
9 | ## Obj-C/Swift specific
10 | *.hmap
11 |
12 | ## App packaging
13 | *.ipa
14 | *.dSYM.zip
15 | *.dSYM
16 |
17 | ## Playgrounds
18 | timeline.xctimeline
19 | playground.xcworkspace
20 |
21 | # Swift Package Manager
22 | #
23 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
24 | # Packages/
25 | # Package.pins
26 | # Package.resolved
27 | # *.xcodeproj
28 | #
29 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
30 | # hence it is not needed unless you have added a package configuration file to your project
31 | # .swiftpm
32 |
33 | .build/
34 |
35 | # CocoaPods
36 | #
37 | # We recommend against adding the Pods directory to your .gitignore. However
38 | # you should judge for yourself, the pros and cons are mentioned at:
39 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
40 | #
41 | # Pods/
42 | #
43 | # Add this line if you want to avoid checking in source code from the Xcode workspace
44 | # *.xcworkspace
45 |
46 | # Carthage
47 | #
48 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
49 | # Carthage/Checkouts
50 |
51 | Carthage/Build/
52 |
53 | # fastlane
54 | #
55 | # It is recommended to not store the screenshots in the git repo.
56 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
57 | # For more information about the recommended setup visit:
58 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
59 |
60 | fastlane/report.xml
61 | fastlane/Preview.html
62 | fastlane/screenshots/**/*.png
63 | fastlane/test_output
64 |
--------------------------------------------------------------------------------
/Sources/MastoParse/MastoParseContentBlock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MastoParseContentBlock.swift
3 | // MastoParse
4 | //
5 | // Created by Shannon Hughes on 7/1/25.
6 | //
7 |
8 | public func getParseBlocks(from html: String) throws -> [MastoParseContentBlock] {
9 | let nodes = try buildContentTree(from: html)
10 | let blocks = getParseBlocks(from: nodes)
11 | return blocks
12 | }
13 |
14 | func getParseBlocks(from nodes: [MastoParseNode]) -> [MastoParseContentBlock] {
15 | let blocks = toMastoParseAccumulators(nodes, addingTo: nil).flatMap { accumulator in
16 | if let block = accumulator.contentBlock() {
17 | return [block]
18 | } else {
19 | return accumulator.contentRows(inheritingNestedFormatting: [], withPrefix: nil)
20 | }
21 | }
22 |
23 | return blocks
24 | }
25 |
26 | public class MastoParseContentBlock: Identifiable {
27 | }
28 |
29 | public enum MastoParseNestedFormat {
30 | case topLevelBlockquote
31 | case subordinateBlockquote
32 | case listLevel
33 | }
34 |
35 | public class MastoParseContentRow: MastoParseContentBlock {
36 | public enum Style {
37 | case paragraph
38 | case code
39 | }
40 |
41 | public let style: Style
42 | public let listItemPrefix: String?
43 | public let nestedFormatting: [MastoParseNestedFormat]
44 | public let contents: [MastoParseInlineElement]
45 |
46 | public init(contents: [MastoParseInlineElement], style: Style, listItemPrefix: String?, nestedFormatting: [MastoParseNestedFormat]) {
47 | self.style = style
48 | self.listItemPrefix = listItemPrefix
49 | self.contents = contents
50 | self.nestedFormatting = nestedFormatting
51 | }
52 | }
53 |
54 | public class MastoParseBlockquote: MastoParseContentBlock {
55 | public let contents: [MastoParseContentRow]
56 |
57 | init(contents: [MastoParseContentRow]) {
58 | self.contents = contents
59 | }
60 | }
61 |
62 | public struct MastoParseInlineElement: Sendable {
63 | public enum ElementType: Sendable {
64 | case text
65 | case code
66 | }
67 |
68 | public let type: ElementType
69 | public let contents: String
70 |
71 | public init(type: ElementType, contents: String) {
72 | self.type = type
73 | self.contents = contents
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Demo/MastoParseDemoApp/ExamplePostHTML.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExamplePostHTML.swift
3 | // TestingStrings
4 | //
5 | // Created by Shannon Hughes on 6/25/25.
6 | //
7 |
8 | let examples : [String] = [
9 |
10 | "
This is a post with a blockquote...
\n\n This thing, here, is a block quote
with some bold as well
\n I can even nest another blockquote!And then nest yet another one! But we'll keep this one short.
\n \n - Here is the first item in a list which is still inside the first blockquote
\n - \n and here is a second item with\n
\n - a second level of list items
and a nested blockquote! inside the list item!
\n - here's another secondary list item
\n
\n \n
\n
Here is some plaintext after the list. This is still inside the first blockquote.
Here's another nested blockquote! This could go on and on and on, but just long enough to wrap would be ok.
",
11 |
12 | "This is a post with a list...
\n \n - which has blockquote nested inside this item:
Imagine a beautiful quotation! Isn't it so lovely!
\n - \n and another list item with\n \n
\n
\n Some plaintext after the list.
And another blockquote! Just another silly blockquote...
",
13 |
14 | "This is a post with a variety of HTML in it
\nFor instance, this text is bold and this one as well, while this text is stricken through and this one as well.
\n\n This thing, here, is a block quote
with some bold as well
\n \n - a list item
\n - \n and another with\n \n
\n
\n
\n// And this is some code\n// with some comments\nlet x = 5
\nAnd this is inline code
\nFinally, please observe this Ruby element: 明日
\n",
15 |
16 | "A blog… https://blog.joinmastodon.org/2025/06/mastodon-dpga/
Can be a great thing to read!
",
17 |
18 | "This is a post with an unordered list:
\n \n - a list item
\n - \n and another with\n \n
\n
",
19 |
20 | "This is a post with an ordered list:
\n \n - a list item
\n - \n and another with\n
\n - nested
\n - items!
\n
\n \n - a final, unnested list item
",
21 |
22 | "This is a self-quote of a remote formatted post
\nRE: https://example.org/foo/bar/baz
\n"
23 | ]
24 |
--------------------------------------------------------------------------------
/Sources/MastoParse/MastoParseHtmlTree.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MastoParseTree.swift
3 | // Created by Shannon Hughes on 7/1/25.
4 | //
5 |
6 | import Foundation
7 | import SwiftSoup
8 |
9 | // MARK: MastoParseTree
10 |
11 | /// Convert Mastodon post contents html into a tree of MastoParseNodes
12 | func buildContentTree(from html: String) throws -> [MastoParseNode] {
13 | let document = try SwiftSoup.parseBodyFragment(html)
14 | guard let body = document.body() else { return [] }
15 |
16 | return try body.getChildNodes().compactMap { try transform(node: $0) }
17 | }
18 |
19 | /// One node in the tree
20 | indirect enum MastoParseNode: CustomDebugStringConvertible {
21 | case text(String) // leaf
22 | case element(ElementNode) // internal
23 |
24 | var debugDescription: String {
25 | switch self {
26 | case .text(let t): return "TEXT: " + #""\#(t)""#
27 | case .element(let e): return "<\(e.name) \(e.attributes)> (\(e.children.count) children)"
28 | }
29 | }
30 | }
31 |
32 | struct ElementNode {
33 | let name: String
34 | let attributes: [String:String]
35 | var children: [MastoParseNode]
36 | }
37 |
38 |
39 | private func transform(node: SwiftSoup.Node) throws -> MastoParseNode? {
40 | switch node {
41 | case let textNode as SwiftSoup.TextNode:
42 | let text = textNode.getWholeText()
43 | return text.isEmpty ? nil : .text(text)
44 |
45 | case let element as SwiftSoup.Element:
46 | guard Allowed.elements.contains(element.tagName()) else {
47 | // Skip unsupported elements but keep their children
48 | return try wrapChildren(of: element)
49 | }
50 |
51 | let attrs = filteredAttributes(from: element)
52 |
53 | let children = try element.getChildNodes().compactMap { try transform(node: $0) }
54 | return .element(ElementNode(name: element.tagName(),
55 | attributes: attrs,
56 | children: children))
57 |
58 | default:
59 | return nil
60 | }
61 | }
62 |
63 | private func wrapChildren(of element: SwiftSoup.Element) throws -> MastoParseNode? {
64 | // Flatten an unsupported tag by lifting children one level up
65 | let converted = try element.getChildNodes().compactMap { try transform(node: $0) }
66 | return converted.count == 1 ? converted.first : .element(
67 | ElementNode(name: "span", attributes: [:], children: converted)
68 | )
69 | }
70 |
71 | private func filteredAttributes(from element: SwiftSoup.Element) -> [String:String] {
72 | guard let attributes = element.getAttributes(), let allowed = Allowed.attributes[element.tagName()] else { return [:] }
73 |
74 | return attributes.reduce(into: [String:String]()) { dict, attr in
75 | if allowed.contains(attr.getKey()) {
76 | dict[attr.getKey()] = attr.getValue()
77 | }
78 | }
79 | }
80 |
81 | private enum Allowed {
82 |
83 | static let elements: Set = [
84 | "p","br","span","a","del","s","pre","blockquote","code","b","strong","u","i","em",
85 | "ul","ol","li","ruby","rt","rp"
86 | ]
87 |
88 | /// attributes per element
89 | static let attributes: [String: Set] = [
90 | "a" : ["href","rel","class","translate"],
91 | "span": ["class","translate"],
92 | "ol" : ["start","reversed"],
93 | "li" : ["value"],
94 | "p" : ["class"],
95 | ]
96 | }
97 |
--------------------------------------------------------------------------------
/Demo/MastoParseDemoApp/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // MastoParseDemoApp
4 | //
5 | // Created by Shannon Hughes on 7/2/25.
6 | //
7 |
8 | import SwiftUI
9 | import MastoParse
10 |
11 | struct DisplayExample: Identifiable {
12 | let id: Int
13 | let displayBlocks: [MastoParseContentBlock]
14 | }
15 |
16 | @MainActor
17 | let displayExamples: [ DisplayExample ] = {
18 | examples.enumerated().map { (idx, html) in
19 | do {
20 | let blocks = try getParseBlocks(from: html)
21 | return DisplayExample(id: idx, displayBlocks: blocks)
22 | } catch {
23 | let errorItem = MastoParseInlineElement(type: .text, contents: "ERROR: \(error)")
24 | let block = MastoParseContentRow(contents: [errorItem], style: .paragraph, nestedFormatting: [])
25 | return DisplayExample(id: idx, displayBlocks: [block])
26 | }
27 | }
28 | }()
29 |
30 | struct DemoView: View {
31 |
32 | @State var emoji: Image?
33 | static let font: Font.TextStyle = .body // customize this to the font style you wish to use
34 | @ScaledMetric(relativeTo: font) private var emojiSize: CGFloat = 25
35 |
36 | var body: some View {
37 | ScrollView {
38 | VStack(alignment: .leading) {
39 | ForEach(displayExamples) { html in
40 | VStack(alignment: .leading) {
41 | ForEach(html.displayBlocks) { block in
42 | if let blockquote = block as? MastoParseBlockquote {
43 | BlockquoteView(block: blockquote)
44 | } else if let row = block as? MastoParseContentRow {
45 | RowView(row: row)
46 | } else {
47 | Text("CASE NOT HANDLED")
48 | }
49 | }
50 | }
51 | .onTapGesture {
52 | print("tapped!!!")
53 | }
54 | Rectangle()
55 | .fill(.gray)
56 | .frame(width: 300, height: 1)
57 | }
58 | }
59 | }
60 | .padding()
61 | }
62 | }
63 |
64 | let indent: CGFloat = 16
65 | let nestedBlockQuoteIndicatorWidth: CGFloat = 2
66 | let indicatorToBlockQuoteSpacing: CGFloat = 4
67 |
68 | let blockquoteColor = Color.purple.opacity(0.5)
69 | struct BlockquoteView: View {
70 | let block: MastoParseBlockquote
71 |
72 | var body: some View {
73 | HStack {
74 | VStack {
75 | Image(systemName: "quote.opening")
76 | .font(.title)
77 | .fontWeight(.bold)
78 | .foregroundStyle(blockquoteColor)
79 |
80 | Spacer()
81 | }
82 | VStack(alignment: .leading, spacing: 0) {
83 | ForEach(Array(block.contents.enumerated()), id: \.offset) { idx, element in
84 | RowView(row: element)
85 | }
86 | }
87 | }
88 | }
89 | }
90 |
91 | enum TextElement {
92 | case image(Image)
93 | case text(LocalizedStringKey)
94 | case code(String)
95 | }
96 |
97 | struct RowView: View {
98 | static let font: Font.TextStyle = .body
99 | @ScaledMetric(relativeTo: font) private var imgBaseline: CGFloat = -5 // without this, the custom emoji sit too high amidst the surrounding text
100 |
101 | let row: MastoParseContentRow
102 |
103 | var body: some View {
104 | let totalFormattingSpaceRequired = row.nestedFormatting.reduce(into: CGFloat.zero) { partialResult, format in
105 | switch format {
106 | case .listLevel:
107 | partialResult += indent
108 | case .subordinateBlockquote:
109 | partialResult += nestedBlockQuoteIndicatorWidth + indicatorToBlockQuoteSpacing
110 | case .topLevelBlockquote:
111 | break
112 | }
113 | }
114 |
115 | combineElements(row.contents.map({ element in
116 | switch element.type {
117 | case .text:
118 | return .text(LocalizedStringKey(element.contents))
119 | case .code:
120 | return .code(element.contents)
121 | }
122 |
123 | }))
124 | .padding(EdgeInsets(top: 0, leading: totalFormattingSpaceRequired, bottom: 0, trailing: 0))
125 | .background() {
126 | // Putting the nested blockquote bar in a background correctly expands its height to match the contents of the row. Trying to include it in the same HStack as the content leaves the bar too short.
127 | HStack(spacing: 0) {
128 | ForEach(Array(row.nestedFormatting.enumerated()), id: \.offset) { idx, indicator in
129 | switch indicator {
130 | case .topLevelBlockquote:
131 | EmptyView()
132 | case .subordinateBlockquote:
133 | blockquoteColor
134 | .frame(width: nestedBlockQuoteIndicatorWidth)
135 | Spacer()
136 | .frame(maxWidth: indicatorToBlockQuoteSpacing)
137 | case .listLevel:
138 | Spacer()
139 | .frame(width: indent)
140 | }
141 | }
142 | Spacer()
143 | .frame(maxWidth: .infinity)
144 | }
145 | }
146 | }
147 |
148 | @ViewBuilder func combineElements(_ elements: [TextElement]) -> some View {
149 | let pieces = elements.map { element in
150 | switch element {
151 | case .image(let image):
152 | return Text("\(image)").baselineOffset(imgBaseline)
153 | case .text(let text):
154 | return Text(text)
155 | case .code(let text):
156 | var attributed = AttributedString(text)
157 | attributed.backgroundColor = blockquoteColor
158 | attributed.font = .system(.body, design: .monospaced)
159 | return Text(attributed)
160 | }
161 | }
162 | pieces.reduce(Text(""), +)
163 | .fixedSize(horizontal: false, vertical: true)
164 | }
165 | }
166 |
167 | #Preview {
168 | DemoView()
169 | }
170 |
171 |
--------------------------------------------------------------------------------
/Sources/MastoParse/MastoParseAccumulator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MastoParseAccumulator.swift
3 | // MastoParse
4 | //
5 | // Created by Shannon Hughes on 7/1/25.
6 | //
7 |
8 | import Foundation
9 |
10 | public class MarkdownAccumulator {
11 | var charactersToTrim: CharacterSet? {
12 | return nil
13 | }
14 |
15 | func appendInlineElement(_ element: MastoParseInlineElement) {
16 | fatalError("subclasses must implement")
17 | }
18 |
19 | var canAppendBlocks: Bool {
20 | return false
21 | }
22 |
23 | func appendBlock(_ block: MarkdownAccumulator) -> Bool {
24 | fatalError("subclasses must implement")
25 | }
26 |
27 | func contentRows(inheritingNestedFormatting inherited: [MastoParseNestedFormat], withPrefix prefix: String?) -> [MastoParseContentRow] {
28 | fatalError("subclasses must implement")
29 | }
30 |
31 | func contentBlock() -> MastoParseContentBlock? {
32 | fatalError("subclasses must implement")
33 | }
34 | }
35 |
36 | private class MarkdownFlowAccumulator: MarkdownAccumulator {
37 | enum FlowAccumulatorType {
38 | case paragraph
39 | case code
40 | }
41 |
42 | let type: FlowAccumulatorType
43 | var inlineElements = [MastoParseInlineElement]()
44 |
45 | init(_ type: FlowAccumulatorType) {
46 | self.type = type
47 | }
48 |
49 | override var charactersToTrim: CharacterSet? {
50 | switch type {
51 | case .code:
52 | return nil
53 | case .paragraph:
54 | return .newlines
55 | }
56 | }
57 |
58 | override func appendInlineElement(_ element: MastoParseInlineElement) {
59 | self.inlineElements.append(element)
60 | }
61 |
62 | override func appendBlock(_ block: MarkdownAccumulator) -> Bool {
63 | return false
64 | }
65 |
66 | override func contentRows(inheritingNestedFormatting inherited: [MastoParseNestedFormat], withPrefix prefix: String?) -> [MastoParseContentRow] {
67 | switch type {
68 | case .paragraph:
69 | if !inlineElements.isEmpty {
70 | return [MastoParseContentRow(contents: inlineElements, style: .paragraph, listItemPrefix: prefix, nestedFormatting: inherited)]
71 | }
72 | case .code:
73 | if !inlineElements.isEmpty {
74 | return [MastoParseContentRow(contents: inlineElements, style: .code, listItemPrefix: prefix, nestedFormatting: inherited)]
75 | }
76 | }
77 | return []
78 | }
79 |
80 | override func contentBlock() -> MastoParseContentBlock? {
81 | return nil
82 | }
83 | }
84 |
85 | class MarkdownBlockAccumulator: MarkdownAccumulator {
86 | enum BlockType: Equatable {
87 | case blockquote
88 | case list(prefix: String?)
89 |
90 | var prefix: String? {
91 | switch self {
92 | case .blockquote:
93 | return nil
94 | case .list(let prefix):
95 | return prefix
96 | }
97 | }
98 | }
99 |
100 | let type: BlockType
101 | var contents = [MarkdownAccumulator]()
102 |
103 | init(_ type: BlockType) {
104 | self.type = type
105 | }
106 |
107 | override var charactersToTrim: CharacterSet? {
108 | switch type {
109 | case .blockquote:
110 | return .newlines
111 | case .list:
112 | return .whitespacesAndNewlines
113 | }
114 | }
115 |
116 | override func appendInlineElement(_ element: MastoParseInlineElement) {
117 | if let currentParagraph = contents.last as? MarkdownFlowAccumulator {
118 | currentParagraph.appendInlineElement(element)
119 | } else {
120 | let newParagraph = MarkdownFlowAccumulator(.paragraph)
121 | newParagraph.appendInlineElement(element)
122 | contents.append(newParagraph)
123 | }
124 | }
125 |
126 | override var canAppendBlocks: Bool {
127 | return true
128 | }
129 |
130 | override func appendBlock(_ block: MarkdownAccumulator) -> Bool {
131 | contents.append(block)
132 | return true
133 | }
134 |
135 | override func contentRows(inheritingNestedFormatting inherited: [MastoParseNestedFormat], withPrefix prefix: String?) -> [MastoParseContentRow] {
136 | let immutableContents = contents.flatMap { accumulator in
137 | let childFormatting: [MastoParseNestedFormat] = {
138 | switch type {
139 | case .blockquote:
140 | if inherited.isEmpty {
141 | return [.topLevelBlockquote]
142 | } else {
143 | return inherited + [.subordinateBlockquote]
144 | }
145 | case .list:
146 | if let childBlockquote = accumulator as? MarkdownBlockAccumulator, childBlockquote.type == .blockquote {
147 | return inherited + [.listLevel, .listLevel]
148 | } else {
149 | return inherited + [.listLevel]
150 | }
151 | }
152 | }()
153 | return accumulator.contentRows(inheritingNestedFormatting: childFormatting, withPrefix: type.prefix)
154 | }
155 | return immutableContents
156 | }
157 |
158 | override func contentBlock() -> MastoParseContentBlock? {
159 | let contents = contentRows(inheritingNestedFormatting: [], withPrefix: nil)
160 | guard !contents.isEmpty else {
161 | return nil
162 | }
163 | switch type {
164 | case .blockquote:
165 | return MastoParseBlockquote(contents: contents)
166 | case .list:
167 | return nil
168 | }
169 | }
170 | }
171 |
172 | private func listItems(_ nodes: [MastoParseNode], ordered: Bool, startingIndex: Int?) -> [MarkdownAccumulator] {
173 | let listItemContents: [[MastoParseNode]] = nodes.compactMap { node -> [MastoParseNode]? in
174 | guard case .element(let li) = node, li.name == "li" else { return nil }
175 | return li.children
176 | }
177 |
178 | var index = startingIndex ?? 1
179 |
180 | let accumulatedItems: [MarkdownAccumulator] = listItemContents.map { contents in
181 | defer { index += 1 }
182 | let listPrefix = ordered ? "\(index). " : "• "
183 | let listItem = MarkdownBlockAccumulator(.list(prefix: listPrefix))
184 |
185 | _ = toMastoParseAccumulators(contents, addingTo: listItem)
186 |
187 | return listItem
188 | }
189 |
190 | return accumulatedItems
191 | }
192 |
193 |
194 | func toMastoParseAccumulators(_ nodes: [MastoParseNode], addingTo containingAccumulator: MarkdownAccumulator?) -> [MarkdownAccumulator] {
195 | var accumulatedBlocks = [MarkdownAccumulator]()
196 |
197 | var currentAccumulator: MarkdownAccumulator = containingAccumulator ?? MarkdownFlowAccumulator(.paragraph)
198 |
199 | for node in nodes {
200 | func append(_ inlineElement: MastoParseInlineElement.ElementType, contents: String) {
201 | let trimmed = {
202 | if let charactersToTrim = currentAccumulator.charactersToTrim {
203 | contents.trimmingCharacters(in: charactersToTrim)
204 | } else {
205 | contents
206 | }
207 | }()
208 | currentAccumulator.appendInlineElement(MastoParseInlineElement(type: inlineElement, contents: trimmed))
209 | }
210 |
211 | switch node {
212 | case .text(let t):
213 | append(.text, contents: t)
214 | case .element(let element):
215 | var skipChildren = false
216 |
217 | if isInlineElement(element.name) {
218 | switch element.name {
219 | case "br":
220 | currentAccumulator.appendInlineElement(MastoParseInlineElement(type: .text, contents: "\n"))
221 | default:
222 | let markdownString = toString([node])
223 | append(element.name == "code" ? .code : .text, contents: markdownString)
224 | }
225 | skipChildren = true
226 | } else {
227 | let newAccumulator: MarkdownAccumulator? = { () -> MarkdownAccumulator? in
228 | switch element.name {
229 | case "p":
230 | return MarkdownFlowAccumulator(.paragraph)
231 | case "pre":
232 | return MarkdownFlowAccumulator(.code)
233 | case "blockquote":
234 | let blockquote = MarkdownBlockAccumulator(.blockquote)
235 | let contents = toMastoParseAccumulators(element.children, addingTo: nil)
236 | blockquote.contents = contents
237 | if !currentAccumulator.appendBlock(blockquote) {
238 | accumulatedBlocks.append(currentAccumulator)
239 | accumulatedBlocks.append(blockquote)
240 | }
241 | skipChildren = true
242 | currentAccumulator = MarkdownFlowAccumulator(.paragraph) // we've taken care of the whole blockquote already, now we wipe the slate clean for a fresh start
243 | return nil
244 | case "ul", "ol": // unordered or ordered list
245 | var startIndex: Int
246 | if let start = element.attributes["start"], let intStart = Int(start) {
247 | startIndex = intStart
248 | } else {
249 | startIndex = 1
250 | }
251 |
252 | let itemAccumulators = listItems(element.children, ordered: element.name == "ol", startingIndex: element.name == "ol" ? startIndex : nil)
253 |
254 | let appendToCurrent = currentAccumulator.canAppendBlocks
255 | if !appendToCurrent {
256 | accumulatedBlocks.append(currentAccumulator)
257 | }
258 | for itemAccumulator in itemAccumulators {
259 | if appendToCurrent {
260 | _ = currentAccumulator.appendBlock(itemAccumulator)
261 | } else {
262 | accumulatedBlocks.append(itemAccumulator)
263 | }
264 | }
265 | skipChildren = true
266 | currentAccumulator = MarkdownFlowAccumulator(.paragraph) // we've taken care of the whole list already, now we wipe the slate clean for a fresh start
267 | return nil
268 | case "li": // list item
269 | assertionFailure("list items should be handled by the listItems() function")
270 | return nil
271 | default:
272 | // treat as text
273 | let markdownString = toString([node])
274 | append(element.name == "code" ? .code : .text, contents: markdownString)
275 | skipChildren = true
276 | return nil
277 | }
278 | }()
279 | if let newAccumulator, let currentAccumulator = currentAccumulator as? MarkdownBlockAccumulator, currentAccumulator.appendBlock(newAccumulator) {
280 | if !skipChildren {
281 | _ = toMastoParseAccumulators(element.children, addingTo: newAccumulator)
282 | }
283 | continue
284 | } else if let newAccumulator {
285 | accumulatedBlocks.append(currentAccumulator)
286 | if !skipChildren {
287 | _ = toMastoParseAccumulators(element.children, addingTo: newAccumulator)
288 | }
289 | currentAccumulator = newAccumulator
290 | } else {
291 | continue
292 | }
293 | }
294 | }
295 | }
296 | accumulatedBlocks.append(currentAccumulator)
297 |
298 | return accumulatedBlocks
299 | }
300 |
301 | private func isInlineElement(_ elementName: String) -> Bool {
302 | switch elementName {
303 | case "strong", "b", "em", "i", "u", "del", "s", "code", "a", "br": return true
304 | default:
305 | return false
306 | }
307 | }
308 |
309 | private func escapeMarkdown(_ text: String) -> String {
310 | // Escape Markdown characters unless inside code blocks
311 | let specialChars = ["\\", "`", "*", "_", "{", "}", "[", "]", "(", ")", "#", "+", "-", ".", "!"]
312 | var escaped = text
313 | for char in specialChars {
314 | escaped = escaped.replacingOccurrences(of: char, with: "\\" + char)
315 | }
316 | return escaped
317 | }
318 |
319 | func toString(_ nodes: [MastoParseNode]) -> String {
320 | nodes.map { node in
321 | switch node {
322 | case .text(let t):
323 | #if DEBUG && false
324 | print("toString of text is: \(t)")
325 | #endif
326 | return escapeMarkdown(t)
327 |
328 | case .element(let element):
329 | if !isInlineElement(element.name) {
330 | return toString(element.children)
331 | }
332 |
333 | let childrenMarkdown = toString(element.children)
334 |
335 | switch element.name {
336 | case "strong", "b": return "**\(childrenMarkdown)**"
337 | case "em", "i": return "_\(childrenMarkdown)_"
338 | case "u": return childrenMarkdown // Markdown doesn't support underline
339 | case "del", "s": return "~~\(childrenMarkdown)~~"
340 | case "code":
341 | if element.attributes["class"]?.contains("language-") == true {
342 | return "\n\(childrenMarkdown)\n"
343 | } else {
344 | return "\(childrenMarkdown)"
345 | }
346 | case "pre", "blockquote": assertionFailure(); return ""
347 | case "a":
348 | let href = element.attributes["href"] ?? "#"
349 | return "[\(trimUrlStringForDisplay(childrenMarkdown))](\(href))"
350 | case "br": return " \n"
351 | default:
352 | return childrenMarkdown
353 | }
354 | }
355 | }.joined()
356 | }
357 |
358 | func trimUrlStringForDisplay(_ urlString: String) -> String {
359 | let maxLength: Int = 30
360 | var trimmed = urlString
361 | let https = "https://"
362 | let http = "http://"
363 | let escapedWww = "www\\."
364 | let www = "www."
365 |
366 | trimmed = trimmed.replacingOccurrences(of: https, with: "", options: .anchored)
367 | trimmed = trimmed.replacingOccurrences(of: http, with: "", options: .anchored)
368 | trimmed = trimmed.replacingOccurrences(of: escapedWww, with: "", options: .anchored)
369 | trimmed = trimmed.replacingOccurrences(of: www, with: "", options: .anchored)
370 | if trimmed.count > maxLength {
371 | return String(trimmed.prefix(maxLength)) + "…"
372 | } else {
373 | return trimmed
374 | }
375 | }
376 |
--------------------------------------------------------------------------------
/Demo/MastoParseDemoApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 77;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | FBA2EA8D2E1C0833005ADEF0 /* MastoParse in Frameworks */ = {isa = PBXBuildFile; productRef = FBA2EA8C2E1C0833005ADEF0 /* MastoParse */; };
11 | /* End PBXBuildFile section */
12 |
13 | /* Begin PBXContainerItemProxy section */
14 | FBB76EAA2E1562BE00480EBA /* PBXContainerItemProxy */ = {
15 | isa = PBXContainerItemProxy;
16 | containerPortal = FBB76E912E1562BC00480EBA /* Project object */;
17 | proxyType = 1;
18 | remoteGlobalIDString = FBB76E982E1562BC00480EBA;
19 | remoteInfo = MastoParseDemoApp;
20 | };
21 | FBB76EB42E1562BE00480EBA /* PBXContainerItemProxy */ = {
22 | isa = PBXContainerItemProxy;
23 | containerPortal = FBB76E912E1562BC00480EBA /* Project object */;
24 | proxyType = 1;
25 | remoteGlobalIDString = FBB76E982E1562BC00480EBA;
26 | remoteInfo = MastoParseDemoApp;
27 | };
28 | /* End PBXContainerItemProxy section */
29 |
30 | /* Begin PBXFileReference section */
31 | FBB76E992E1562BC00480EBA /* MastoParseDemoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MastoParseDemoApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
32 | FBB76EA92E1562BE00480EBA /* MastoParseDemoAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MastoParseDemoAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
33 | FBB76EB32E1562BE00480EBA /* MastoParseDemoAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MastoParseDemoAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
34 | /* End PBXFileReference section */
35 |
36 | /* Begin PBXFileSystemSynchronizedRootGroup section */
37 | FBB76E9B2E1562BC00480EBA /* MastoParseDemoApp */ = {
38 | isa = PBXFileSystemSynchronizedRootGroup;
39 | path = MastoParseDemoApp;
40 | sourceTree = "";
41 | };
42 | /* End PBXFileSystemSynchronizedRootGroup section */
43 |
44 | /* Begin PBXFrameworksBuildPhase section */
45 | FBB76E962E1562BC00480EBA /* Frameworks */ = {
46 | isa = PBXFrameworksBuildPhase;
47 | buildActionMask = 2147483647;
48 | files = (
49 | FBA2EA8D2E1C0833005ADEF0 /* MastoParse in Frameworks */,
50 | );
51 | runOnlyForDeploymentPostprocessing = 0;
52 | };
53 | FBB76EA62E1562BE00480EBA /* Frameworks */ = {
54 | isa = PBXFrameworksBuildPhase;
55 | buildActionMask = 2147483647;
56 | files = (
57 | );
58 | runOnlyForDeploymentPostprocessing = 0;
59 | };
60 | FBB76EB02E1562BE00480EBA /* Frameworks */ = {
61 | isa = PBXFrameworksBuildPhase;
62 | buildActionMask = 2147483647;
63 | files = (
64 | );
65 | runOnlyForDeploymentPostprocessing = 0;
66 | };
67 | /* End PBXFrameworksBuildPhase section */
68 |
69 | /* Begin PBXGroup section */
70 | FBB76E902E1562BC00480EBA = {
71 | isa = PBXGroup;
72 | children = (
73 | FBB76E9B2E1562BC00480EBA /* MastoParseDemoApp */,
74 | FBB76E9A2E1562BC00480EBA /* Products */,
75 | );
76 | sourceTree = "";
77 | };
78 | FBB76E9A2E1562BC00480EBA /* Products */ = {
79 | isa = PBXGroup;
80 | children = (
81 | FBB76E992E1562BC00480EBA /* MastoParseDemoApp.app */,
82 | FBB76EA92E1562BE00480EBA /* MastoParseDemoAppTests.xctest */,
83 | FBB76EB32E1562BE00480EBA /* MastoParseDemoAppUITests.xctest */,
84 | );
85 | name = Products;
86 | sourceTree = "";
87 | };
88 | /* End PBXGroup section */
89 |
90 | /* Begin PBXNativeTarget section */
91 | FBB76E982E1562BC00480EBA /* MastoParseDemoApp */ = {
92 | isa = PBXNativeTarget;
93 | buildConfigurationList = FBB76EBD2E1562BE00480EBA /* Build configuration list for PBXNativeTarget "MastoParseDemoApp" */;
94 | buildPhases = (
95 | FBB76E952E1562BC00480EBA /* Sources */,
96 | FBB76E962E1562BC00480EBA /* Frameworks */,
97 | FBB76E972E1562BC00480EBA /* Resources */,
98 | );
99 | buildRules = (
100 | );
101 | dependencies = (
102 | );
103 | fileSystemSynchronizedGroups = (
104 | FBB76E9B2E1562BC00480EBA /* MastoParseDemoApp */,
105 | );
106 | name = MastoParseDemoApp;
107 | packageProductDependencies = (
108 | FBA2EA8C2E1C0833005ADEF0 /* MastoParse */,
109 | );
110 | productName = MastoParseDemoApp;
111 | productReference = FBB76E992E1562BC00480EBA /* MastoParseDemoApp.app */;
112 | productType = "com.apple.product-type.application";
113 | };
114 | FBB76EA82E1562BE00480EBA /* MastoParseDemoAppTests */ = {
115 | isa = PBXNativeTarget;
116 | buildConfigurationList = FBB76EC02E1562BE00480EBA /* Build configuration list for PBXNativeTarget "MastoParseDemoAppTests" */;
117 | buildPhases = (
118 | FBB76EA52E1562BE00480EBA /* Sources */,
119 | FBB76EA62E1562BE00480EBA /* Frameworks */,
120 | FBB76EA72E1562BE00480EBA /* Resources */,
121 | );
122 | buildRules = (
123 | );
124 | dependencies = (
125 | FBB76EAB2E1562BE00480EBA /* PBXTargetDependency */,
126 | );
127 | name = MastoParseDemoAppTests;
128 | packageProductDependencies = (
129 | );
130 | productName = MastoParseDemoAppTests;
131 | productReference = FBB76EA92E1562BE00480EBA /* MastoParseDemoAppTests.xctest */;
132 | productType = "com.apple.product-type.bundle.unit-test";
133 | };
134 | FBB76EB22E1562BE00480EBA /* MastoParseDemoAppUITests */ = {
135 | isa = PBXNativeTarget;
136 | buildConfigurationList = FBB76EC32E1562BE00480EBA /* Build configuration list for PBXNativeTarget "MastoParseDemoAppUITests" */;
137 | buildPhases = (
138 | FBB76EAF2E1562BE00480EBA /* Sources */,
139 | FBB76EB02E1562BE00480EBA /* Frameworks */,
140 | FBB76EB12E1562BE00480EBA /* Resources */,
141 | );
142 | buildRules = (
143 | );
144 | dependencies = (
145 | FBB76EB52E1562BE00480EBA /* PBXTargetDependency */,
146 | );
147 | name = MastoParseDemoAppUITests;
148 | packageProductDependencies = (
149 | );
150 | productName = MastoParseDemoAppUITests;
151 | productReference = FBB76EB32E1562BE00480EBA /* MastoParseDemoAppUITests.xctest */;
152 | productType = "com.apple.product-type.bundle.ui-testing";
153 | };
154 | /* End PBXNativeTarget section */
155 |
156 | /* Begin PBXProject section */
157 | FBB76E912E1562BC00480EBA /* Project object */ = {
158 | isa = PBXProject;
159 | attributes = {
160 | BuildIndependentTargetsInParallel = 1;
161 | LastSwiftUpdateCheck = 1620;
162 | LastUpgradeCheck = 1620;
163 | TargetAttributes = {
164 | FBB76E982E1562BC00480EBA = {
165 | CreatedOnToolsVersion = 16.2;
166 | };
167 | FBB76EA82E1562BE00480EBA = {
168 | CreatedOnToolsVersion = 16.2;
169 | TestTargetID = FBB76E982E1562BC00480EBA;
170 | };
171 | FBB76EB22E1562BE00480EBA = {
172 | CreatedOnToolsVersion = 16.2;
173 | TestTargetID = FBB76E982E1562BC00480EBA;
174 | };
175 | };
176 | };
177 | buildConfigurationList = FBB76E942E1562BC00480EBA /* Build configuration list for PBXProject "MastoParseDemoApp" */;
178 | developmentRegion = en;
179 | hasScannedForEncodings = 0;
180 | knownRegions = (
181 | en,
182 | Base,
183 | );
184 | mainGroup = FBB76E902E1562BC00480EBA;
185 | minimizedProjectReferenceProxies = 1;
186 | packageReferences = (
187 | FBA2EA8B2E1C0833005ADEF0 /* XCLocalSwiftPackageReference "../../MastoParse" */,
188 | );
189 | preferredProjectObjectVersion = 77;
190 | productRefGroup = FBB76E9A2E1562BC00480EBA /* Products */;
191 | projectDirPath = "";
192 | projectRoot = "";
193 | targets = (
194 | FBB76E982E1562BC00480EBA /* MastoParseDemoApp */,
195 | FBB76EA82E1562BE00480EBA /* MastoParseDemoAppTests */,
196 | FBB76EB22E1562BE00480EBA /* MastoParseDemoAppUITests */,
197 | );
198 | };
199 | /* End PBXProject section */
200 |
201 | /* Begin PBXResourcesBuildPhase section */
202 | FBB76E972E1562BC00480EBA /* Resources */ = {
203 | isa = PBXResourcesBuildPhase;
204 | buildActionMask = 2147483647;
205 | files = (
206 | );
207 | runOnlyForDeploymentPostprocessing = 0;
208 | };
209 | FBB76EA72E1562BE00480EBA /* Resources */ = {
210 | isa = PBXResourcesBuildPhase;
211 | buildActionMask = 2147483647;
212 | files = (
213 | );
214 | runOnlyForDeploymentPostprocessing = 0;
215 | };
216 | FBB76EB12E1562BE00480EBA /* Resources */ = {
217 | isa = PBXResourcesBuildPhase;
218 | buildActionMask = 2147483647;
219 | files = (
220 | );
221 | runOnlyForDeploymentPostprocessing = 0;
222 | };
223 | /* End PBXResourcesBuildPhase section */
224 |
225 | /* Begin PBXSourcesBuildPhase section */
226 | FBB76E952E1562BC00480EBA /* Sources */ = {
227 | isa = PBXSourcesBuildPhase;
228 | buildActionMask = 2147483647;
229 | files = (
230 | );
231 | runOnlyForDeploymentPostprocessing = 0;
232 | };
233 | FBB76EA52E1562BE00480EBA /* Sources */ = {
234 | isa = PBXSourcesBuildPhase;
235 | buildActionMask = 2147483647;
236 | files = (
237 | );
238 | runOnlyForDeploymentPostprocessing = 0;
239 | };
240 | FBB76EAF2E1562BE00480EBA /* Sources */ = {
241 | isa = PBXSourcesBuildPhase;
242 | buildActionMask = 2147483647;
243 | files = (
244 | );
245 | runOnlyForDeploymentPostprocessing = 0;
246 | };
247 | /* End PBXSourcesBuildPhase section */
248 |
249 | /* Begin PBXTargetDependency section */
250 | FBB76EAB2E1562BE00480EBA /* PBXTargetDependency */ = {
251 | isa = PBXTargetDependency;
252 | target = FBB76E982E1562BC00480EBA /* MastoParseDemoApp */;
253 | targetProxy = FBB76EAA2E1562BE00480EBA /* PBXContainerItemProxy */;
254 | };
255 | FBB76EB52E1562BE00480EBA /* PBXTargetDependency */ = {
256 | isa = PBXTargetDependency;
257 | target = FBB76E982E1562BC00480EBA /* MastoParseDemoApp */;
258 | targetProxy = FBB76EB42E1562BE00480EBA /* PBXContainerItemProxy */;
259 | };
260 | /* End PBXTargetDependency section */
261 |
262 | /* Begin XCBuildConfiguration section */
263 | FBB76EBB2E1562BE00480EBA /* Debug */ = {
264 | isa = XCBuildConfiguration;
265 | buildSettings = {
266 | ALWAYS_SEARCH_USER_PATHS = NO;
267 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
268 | CLANG_ANALYZER_NONNULL = YES;
269 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
270 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
271 | CLANG_ENABLE_MODULES = YES;
272 | CLANG_ENABLE_OBJC_ARC = YES;
273 | CLANG_ENABLE_OBJC_WEAK = YES;
274 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
275 | CLANG_WARN_BOOL_CONVERSION = YES;
276 | CLANG_WARN_COMMA = YES;
277 | CLANG_WARN_CONSTANT_CONVERSION = YES;
278 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
279 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
280 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
281 | CLANG_WARN_EMPTY_BODY = YES;
282 | CLANG_WARN_ENUM_CONVERSION = YES;
283 | CLANG_WARN_INFINITE_RECURSION = YES;
284 | CLANG_WARN_INT_CONVERSION = YES;
285 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
286 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
287 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
288 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
289 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
290 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
291 | CLANG_WARN_STRICT_PROTOTYPES = YES;
292 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
293 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
294 | CLANG_WARN_UNREACHABLE_CODE = YES;
295 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
296 | COPY_PHASE_STRIP = NO;
297 | DEBUG_INFORMATION_FORMAT = dwarf;
298 | ENABLE_STRICT_OBJC_MSGSEND = YES;
299 | ENABLE_TESTABILITY = YES;
300 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
301 | GCC_C_LANGUAGE_STANDARD = gnu17;
302 | GCC_DYNAMIC_NO_PIC = NO;
303 | GCC_NO_COMMON_BLOCKS = YES;
304 | GCC_OPTIMIZATION_LEVEL = 0;
305 | GCC_PREPROCESSOR_DEFINITIONS = (
306 | "DEBUG=1",
307 | "$(inherited)",
308 | );
309 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
310 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
311 | GCC_WARN_UNDECLARED_SELECTOR = YES;
312 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
313 | GCC_WARN_UNUSED_FUNCTION = YES;
314 | GCC_WARN_UNUSED_VARIABLE = YES;
315 | IPHONEOS_DEPLOYMENT_TARGET = 18.2;
316 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
317 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
318 | MTL_FAST_MATH = YES;
319 | ONLY_ACTIVE_ARCH = YES;
320 | SDKROOT = iphoneos;
321 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
322 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
323 | };
324 | name = Debug;
325 | };
326 | FBB76EBC2E1562BE00480EBA /* Release */ = {
327 | isa = XCBuildConfiguration;
328 | buildSettings = {
329 | ALWAYS_SEARCH_USER_PATHS = NO;
330 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
331 | CLANG_ANALYZER_NONNULL = YES;
332 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
333 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
334 | CLANG_ENABLE_MODULES = YES;
335 | CLANG_ENABLE_OBJC_ARC = YES;
336 | CLANG_ENABLE_OBJC_WEAK = YES;
337 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
338 | CLANG_WARN_BOOL_CONVERSION = YES;
339 | CLANG_WARN_COMMA = YES;
340 | CLANG_WARN_CONSTANT_CONVERSION = YES;
341 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
342 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
343 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
344 | CLANG_WARN_EMPTY_BODY = YES;
345 | CLANG_WARN_ENUM_CONVERSION = YES;
346 | CLANG_WARN_INFINITE_RECURSION = YES;
347 | CLANG_WARN_INT_CONVERSION = YES;
348 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
349 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
350 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
351 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
352 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
353 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
354 | CLANG_WARN_STRICT_PROTOTYPES = YES;
355 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
356 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
357 | CLANG_WARN_UNREACHABLE_CODE = YES;
358 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
359 | COPY_PHASE_STRIP = NO;
360 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
361 | ENABLE_NS_ASSERTIONS = NO;
362 | ENABLE_STRICT_OBJC_MSGSEND = YES;
363 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
364 | GCC_C_LANGUAGE_STANDARD = gnu17;
365 | GCC_NO_COMMON_BLOCKS = YES;
366 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
367 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
368 | GCC_WARN_UNDECLARED_SELECTOR = YES;
369 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
370 | GCC_WARN_UNUSED_FUNCTION = YES;
371 | GCC_WARN_UNUSED_VARIABLE = YES;
372 | IPHONEOS_DEPLOYMENT_TARGET = 18.2;
373 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
374 | MTL_ENABLE_DEBUG_INFO = NO;
375 | MTL_FAST_MATH = YES;
376 | SDKROOT = iphoneos;
377 | SWIFT_COMPILATION_MODE = wholemodule;
378 | VALIDATE_PRODUCT = YES;
379 | };
380 | name = Release;
381 | };
382 | FBB76EBE2E1562BE00480EBA /* Debug */ = {
383 | isa = XCBuildConfiguration;
384 | buildSettings = {
385 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
386 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
387 | CODE_SIGN_STYLE = Automatic;
388 | CURRENT_PROJECT_VERSION = 1;
389 | DEVELOPMENT_ASSET_PATHS = "\"MastoParseDemoApp/Preview Content\"";
390 | DEVELOPMENT_TEAM = 8795L94465;
391 | ENABLE_PREVIEWS = YES;
392 | GENERATE_INFOPLIST_FILE = YES;
393 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
394 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
395 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
396 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
397 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
398 | LD_RUNPATH_SEARCH_PATHS = (
399 | "$(inherited)",
400 | "@executable_path/Frameworks",
401 | );
402 | MARKETING_VERSION = 1.0;
403 | PRODUCT_BUNDLE_IDENTIFIER = com.whattherestimefor.MastoParseDemoApp;
404 | PRODUCT_NAME = "$(TARGET_NAME)";
405 | SWIFT_EMIT_LOC_STRINGS = YES;
406 | SWIFT_VERSION = 5.0;
407 | TARGETED_DEVICE_FAMILY = "1,2";
408 | };
409 | name = Debug;
410 | };
411 | FBB76EBF2E1562BE00480EBA /* Release */ = {
412 | isa = XCBuildConfiguration;
413 | buildSettings = {
414 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
415 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
416 | CODE_SIGN_STYLE = Automatic;
417 | CURRENT_PROJECT_VERSION = 1;
418 | DEVELOPMENT_ASSET_PATHS = "\"MastoParseDemoApp/Preview Content\"";
419 | DEVELOPMENT_TEAM = 8795L94465;
420 | ENABLE_PREVIEWS = YES;
421 | GENERATE_INFOPLIST_FILE = YES;
422 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
423 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
424 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
425 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
426 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
427 | LD_RUNPATH_SEARCH_PATHS = (
428 | "$(inherited)",
429 | "@executable_path/Frameworks",
430 | );
431 | MARKETING_VERSION = 1.0;
432 | PRODUCT_BUNDLE_IDENTIFIER = com.whattherestimefor.MastoParseDemoApp;
433 | PRODUCT_NAME = "$(TARGET_NAME)";
434 | SWIFT_EMIT_LOC_STRINGS = YES;
435 | SWIFT_VERSION = 5.0;
436 | TARGETED_DEVICE_FAMILY = "1,2";
437 | };
438 | name = Release;
439 | };
440 | FBB76EC12E1562BE00480EBA /* Debug */ = {
441 | isa = XCBuildConfiguration;
442 | buildSettings = {
443 | BUNDLE_LOADER = "$(TEST_HOST)";
444 | CODE_SIGN_STYLE = Automatic;
445 | CURRENT_PROJECT_VERSION = 1;
446 | DEVELOPMENT_TEAM = 8795L94465;
447 | GENERATE_INFOPLIST_FILE = YES;
448 | IPHONEOS_DEPLOYMENT_TARGET = 18.2;
449 | MARKETING_VERSION = 1.0;
450 | PRODUCT_BUNDLE_IDENTIFIER = com.whattherestimefor.MastoParseDemoAppTests;
451 | PRODUCT_NAME = "$(TARGET_NAME)";
452 | SWIFT_EMIT_LOC_STRINGS = NO;
453 | SWIFT_VERSION = 5.0;
454 | TARGETED_DEVICE_FAMILY = "1,2";
455 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MastoParseDemoApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MastoParseDemoApp";
456 | };
457 | name = Debug;
458 | };
459 | FBB76EC22E1562BE00480EBA /* Release */ = {
460 | isa = XCBuildConfiguration;
461 | buildSettings = {
462 | BUNDLE_LOADER = "$(TEST_HOST)";
463 | CODE_SIGN_STYLE = Automatic;
464 | CURRENT_PROJECT_VERSION = 1;
465 | DEVELOPMENT_TEAM = 8795L94465;
466 | GENERATE_INFOPLIST_FILE = YES;
467 | IPHONEOS_DEPLOYMENT_TARGET = 18.2;
468 | MARKETING_VERSION = 1.0;
469 | PRODUCT_BUNDLE_IDENTIFIER = com.whattherestimefor.MastoParseDemoAppTests;
470 | PRODUCT_NAME = "$(TARGET_NAME)";
471 | SWIFT_EMIT_LOC_STRINGS = NO;
472 | SWIFT_VERSION = 5.0;
473 | TARGETED_DEVICE_FAMILY = "1,2";
474 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MastoParseDemoApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MastoParseDemoApp";
475 | };
476 | name = Release;
477 | };
478 | FBB76EC42E1562BE00480EBA /* Debug */ = {
479 | isa = XCBuildConfiguration;
480 | buildSettings = {
481 | CODE_SIGN_STYLE = Automatic;
482 | CURRENT_PROJECT_VERSION = 1;
483 | DEVELOPMENT_TEAM = 8795L94465;
484 | GENERATE_INFOPLIST_FILE = YES;
485 | MARKETING_VERSION = 1.0;
486 | PRODUCT_BUNDLE_IDENTIFIER = com.whattherestimefor.MastoParseDemoAppUITests;
487 | PRODUCT_NAME = "$(TARGET_NAME)";
488 | SWIFT_EMIT_LOC_STRINGS = NO;
489 | SWIFT_VERSION = 5.0;
490 | TARGETED_DEVICE_FAMILY = "1,2";
491 | TEST_TARGET_NAME = MastoParseDemoApp;
492 | };
493 | name = Debug;
494 | };
495 | FBB76EC52E1562BE00480EBA /* Release */ = {
496 | isa = XCBuildConfiguration;
497 | buildSettings = {
498 | CODE_SIGN_STYLE = Automatic;
499 | CURRENT_PROJECT_VERSION = 1;
500 | DEVELOPMENT_TEAM = 8795L94465;
501 | GENERATE_INFOPLIST_FILE = YES;
502 | MARKETING_VERSION = 1.0;
503 | PRODUCT_BUNDLE_IDENTIFIER = com.whattherestimefor.MastoParseDemoAppUITests;
504 | PRODUCT_NAME = "$(TARGET_NAME)";
505 | SWIFT_EMIT_LOC_STRINGS = NO;
506 | SWIFT_VERSION = 5.0;
507 | TARGETED_DEVICE_FAMILY = "1,2";
508 | TEST_TARGET_NAME = MastoParseDemoApp;
509 | };
510 | name = Release;
511 | };
512 | /* End XCBuildConfiguration section */
513 |
514 | /* Begin XCConfigurationList section */
515 | FBB76E942E1562BC00480EBA /* Build configuration list for PBXProject "MastoParseDemoApp" */ = {
516 | isa = XCConfigurationList;
517 | buildConfigurations = (
518 | FBB76EBB2E1562BE00480EBA /* Debug */,
519 | FBB76EBC2E1562BE00480EBA /* Release */,
520 | );
521 | defaultConfigurationIsVisible = 0;
522 | defaultConfigurationName = Release;
523 | };
524 | FBB76EBD2E1562BE00480EBA /* Build configuration list for PBXNativeTarget "MastoParseDemoApp" */ = {
525 | isa = XCConfigurationList;
526 | buildConfigurations = (
527 | FBB76EBE2E1562BE00480EBA /* Debug */,
528 | FBB76EBF2E1562BE00480EBA /* Release */,
529 | );
530 | defaultConfigurationIsVisible = 0;
531 | defaultConfigurationName = Release;
532 | };
533 | FBB76EC02E1562BE00480EBA /* Build configuration list for PBXNativeTarget "MastoParseDemoAppTests" */ = {
534 | isa = XCConfigurationList;
535 | buildConfigurations = (
536 | FBB76EC12E1562BE00480EBA /* Debug */,
537 | FBB76EC22E1562BE00480EBA /* Release */,
538 | );
539 | defaultConfigurationIsVisible = 0;
540 | defaultConfigurationName = Release;
541 | };
542 | FBB76EC32E1562BE00480EBA /* Build configuration list for PBXNativeTarget "MastoParseDemoAppUITests" */ = {
543 | isa = XCConfigurationList;
544 | buildConfigurations = (
545 | FBB76EC42E1562BE00480EBA /* Debug */,
546 | FBB76EC52E1562BE00480EBA /* Release */,
547 | );
548 | defaultConfigurationIsVisible = 0;
549 | defaultConfigurationName = Release;
550 | };
551 | /* End XCConfigurationList section */
552 |
553 | /* Begin XCLocalSwiftPackageReference section */
554 | FBA2EA8B2E1C0833005ADEF0 /* XCLocalSwiftPackageReference "../../MastoParse" */ = {
555 | isa = XCLocalSwiftPackageReference;
556 | relativePath = ../../MastoParse;
557 | };
558 | /* End XCLocalSwiftPackageReference section */
559 |
560 | /* Begin XCSwiftPackageProductDependency section */
561 | FBA2EA8C2E1C0833005ADEF0 /* MastoParse */ = {
562 | isa = XCSwiftPackageProductDependency;
563 | productName = MastoParse;
564 | };
565 | /* End XCSwiftPackageProductDependency section */
566 | };
567 | rootObject = FBB76E912E1562BC00480EBA /* Project object */;
568 | }
569 |
--------------------------------------------------------------------------------