up and running, unit tests

This commit is contained in:
Duong Thai 2024-03-07 09:16:40 +07:00
parent 732a253c7a
commit 6dc437b226
31 changed files with 7390 additions and 3 deletions

View file

@ -101,6 +101,10 @@
9FFF677C299B7B2C00FE700A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677B299B7B2C00FE700A /* Notifications */; };
9FFF6780299B7D2B00FE700A /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677F299B7D2B00FE700A /* DesignSystem */; };
9FFF6782299B7D3A00FE700A /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF6781299B7D3A00FE700A /* Account */; };
B039C36D2B8CE14E00DEDCED /* RSParser in Frameworks */ = {isa = PBXBuildFile; productRef = B039C36C2B8CE14E00DEDCED /* RSParser */; };
B039C3702B8CE4DE00DEDCED /* RSParser in Embed Frameworks */ = {isa = PBXBuildFile; productRef = B039C36C2B8CE14E00DEDCED /* RSParser */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
B039C37D2B935E3100DEDCED /* RSS in Frameworks */ = {isa = PBXBuildFile; productRef = B039C37C2B935E3100DEDCED /* RSS */; };
B0BAE1CA2B9720FC005432FF /* RSSTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BAE1C92B9720FC005432FF /* RSSTab.swift */; };
C9B22677297F6C2E001F9EFE /* ContentSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B22676297F6C2E001F9EFE /* ContentSettingsView.swift */; };
D08A9C3529956CFA00204A4A /* SwipeActionsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08A9C3429956CFA00204A4A /* SwipeActionsSettingsView.swift */; };
DA0B24FB2A6876D50045BDD7 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = DA0B24FA2A6876D50045BDD7 /* SFSafeSymbols */; };
@ -153,6 +157,17 @@
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
B039C3712B8CE4DE00DEDCED /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
B039C3702B8CE4DE00DEDCED /* RSParser in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
@ -243,7 +258,9 @@
9FE0346A2ADD59AC00529EA8 /* MediaUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MediaUI; path = Packages/MediaUI; sourceTree = "<group>"; };
9FE151A5293C90F900E9683D /* IconSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelectorView.swift; sourceTree = "<group>"; };
9FE3DB55296FEF5800628CB0 /* AppAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppAccount; path = Packages/AppAccount; sourceTree = "<group>"; };
B039C37B2B935CBF00DEDCED /* RSS */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = RSS; path = Packages/RSS; sourceTree = "<group>"; };
B0BAB49E29B3D7A9008F54D7 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
B0BAE1C92B9720FC005432FF /* RSSTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSSTab.swift; sourceTree = "<group>"; };
C4CBB90B298A0DA3007E1707 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
C4FBCF6F298FD88A0015DF22 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
C9B22676297F6C2E001F9EFE /* ContentSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSettingsView.swift; sourceTree = "<group>"; };
@ -307,10 +324,12 @@
9F7335EA2966B3F800AFF0BA /* Conversations in Frameworks */,
9FE3DB57296FEFCA00628CB0 /* AppAccount in Frameworks */,
9F398AA92935FFDB00A889F2 /* Account in Frameworks */,
B039C36D2B8CE14E00DEDCED /* RSParser in Frameworks */,
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */,
9FD542E72962D2FF0045321A /* Lists in Frameworks */,
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */,
9F5E581929545BE700A53960 /* Env in Frameworks */,
B039C37D2B935E3100DEDCED /* RSS in Frameworks */,
9F2A540A29699705009B2D7C /* ReceiptParser in Frameworks */,
DA0B24FB2A6876D50045BDD7 /* SFSafeSymbols in Frameworks */,
9F295540292B6C3400E0E81B /* Timeline in Frameworks */,
@ -424,6 +443,7 @@
9FAE4AC9293783A200772766 /* Tabs */ = {
isa = PBXGroup;
children = (
B0BAE1C92B9720FC005432FF /* RSSTab.swift */,
9F0811402B14869F0020F85E /* TagGroup */,
9F7335F02967607A00AFF0BA /* Timeline */,
9FE151A4293C90EA00E9683D /* Settings */,
@ -468,6 +488,7 @@
9F55C68E295598F900F94077 /* Explore */,
9F5E581729545B5500A53960 /* Env */,
9F398AA32935F90100A889F2 /* Models */,
B039C37B2B935CBF00DEDCED /* RSS */,
9FC2A3892B49D10000DFD1C1 /* StatusKit */,
9FE0346A2ADD59AC00529EA8 /* MediaUI */,
9F29553D292B67B600E0E81B /* Network */,
@ -624,6 +645,7 @@
9FBFE636292A715500C250E9 /* Frameworks */,
9FBFE637292A715500C250E9 /* Resources */,
9F2A5421296AB631009B2D7C /* Embed Foundation Extensions */,
B039C3712B8CE4DE00DEDCED /* Embed Frameworks */,
);
buildRules = (
);
@ -650,6 +672,8 @@
DA0B24FA2A6876D50045BDD7 /* SFSafeSymbols */,
9FC2A38A2B49D19A00DFD1C1 /* StatusKit */,
9FE4CCAA2B4C848A00DA5F13 /* GiphyUISDK */,
B039C36C2B8CE14E00DEDCED /* RSParser */,
B039C37C2B935E3100DEDCED /* RSS */,
);
productName = IceCubesApp;
productReference = 9FBFE639292A715500C250E9 /* Ice Cubes.app */;
@ -731,6 +755,7 @@
9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */,
DA0B24F92A6876D40045BDD7 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
9FE4CCA92B4C848A00DA5F13 /* XCRemoteSwiftPackageReference "giphy-ios-sdk" */,
B039C36B2B8CE14E00DEDCED /* XCRemoteSwiftPackageReference "RSParser" */,
);
productRefGroup = 9FBFE63A292A715500C250E9 /* Products */;
projectDirPath = "";
@ -843,6 +868,7 @@
9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */,
9F398AA62935FE8A00A889F2 /* AppRegistry.swift in Sources */,
9F15D6002B3D6A850008C220 /* NavigationTab.swift in Sources */,
B0BAE1CA2B9720FC005432FF /* RSSTab.swift in Sources */,
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */,
9F4A48192976B21900A1A038 /* ProfileTab.swift in Sources */,
9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */,
@ -1123,6 +1149,7 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = "";
};
name = Debug;
};
@ -1182,6 +1209,7 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = "";
};
name = Release;
};
@ -1439,6 +1467,14 @@
minimumVersion = 2.2.7;
};
};
B039C36B2B8CE14E00DEDCED /* XCRemoteSwiftPackageReference "RSParser" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Ranchero-Software/RSParser";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.0.3;
};
};
DA0B24F92A6876D40045BDD7 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols";
@ -1581,6 +1617,15 @@
isa = XCSwiftPackageProductDependency;
productName = Account;
};
B039C36C2B8CE14E00DEDCED /* RSParser */ = {
isa = XCSwiftPackageProductDependency;
package = B039C36B2B8CE14E00DEDCED /* XCRemoteSwiftPackageReference "RSParser" */;
productName = RSParser;
};
B039C37C2B935E3100DEDCED /* RSS */ = {
isa = XCSwiftPackageProductDependency;
productName = RSS;
};
DA0B24FA2A6876D50045BDD7 /* SFSafeSymbols */ = {
isa = XCSwiftPackageProductDependency;
package = DA0B24F92A6876D40045BDD7 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;

View file

@ -81,6 +81,15 @@
"version" : "4.34.0"
}
},
{
"identity" : "rsparser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Ranchero-Software/RSParser",
"state" : {
"revision" : "d5b50ff78905ebfaf26dd698e0e5d3ed8269dd9b",
"version" : "2.0.3"
}
},
{
"identity" : "sfsafesymbols",
"kind" : "remoteSourceControl",

View file

@ -10,6 +10,7 @@ import RevenueCat
import StatusKit
import SwiftUI
import Timeline
import RSS
@main
struct IceCubesApp: App {
@ -34,7 +35,9 @@ struct IceCubesApp: App {
var body: some Scene {
appScene
.environment(\.managedObjectContext, RSSDataController.shared.container.viewContext)
otherScenes
.environment(\.managedObjectContext, RSSDataController.shared.container.viewContext)
}
func setNewClientsInEnv(client: Client) {

View file

@ -0,0 +1,112 @@
//
// RSSTab.swift
// IceCubesApp
//
// Created by Duong Thai on 26/02/2024.
//
import SwiftUI
import RSParser
import SwiftSoup
import DesignSystem
import RSS
import Env
@MainActor
public struct RSSTab: View {
@FetchRequest(sortDescriptors: [SortDescriptor(\.date, order: .reverse)])
private var items: FetchedResults<RSSItem>
@Environment(\.managedObjectContext) private var moContext
@State private var isLoading = true
@State private var showAlert = false
@State private var routerPath = RouterPath()
public init() {}
public var body: some View {
NavigationStack {
if isLoading {
ProgressView()
} else {
List {
ForEach(items) { i in
Button(action: {
if let url = i.url {
_ = routerPath.handle(url: url)
} else {
showAlert = true
}
}, label: {
RSSItemView(i)
})
.buttonStyle(.plain)
.alert("rss.item.url.unavailable", isPresented: $showAlert) {
Button("rss.item.url.unavailable.action.OK") { showAlert = false }
} message: {
Text("rss.item.url.unavailable.message")
}
}
}
.listStyle(PlainListStyle())
}
}
// TODO: remove this
.onDisappear {
for f in items { moContext.delete(f) }
}
.task {
isLoading = true
let feedURLs = [
"https://www.swift.org/atom.xml",
"https://wadetregaskis.com/feed",
"https://121clicks.com/feed",
"https://iso.500px.com/feed/",
]
let sendableFeeds = await Task<[RSSFeed.SendableData], Never>.detached(priority: .userInitiated, operation: {
return await withTaskGroup(of: RSSFeed.SendableData?.self) { taskGroup in
for fURL in feedURLs {
taskGroup.addTask {
guard
let url = URL(string: fURL),
let data = try? Data(contentsOf: url),
let feed = try? FeedParser.parse(ParserData(url: fURL, data: data))
else { return nil }
return RSSFeed.SendableData(parsedFeed: feed, sourceURL: url)
}
}
var _feeds = [RSSFeed.SendableData]()
for await f in taskGroup {
if let f { _feeds.append(f) }
}
return _feeds
}
}).value
let rssFeeds = sendableFeeds.compactMap {
(feed: RSSFeed(context: moContext, sendableData: $0), sendableFeed: $0)
}
for feed in rssFeeds {
let sendableFeed = feed.sendableFeed
let sendableItems = await Task.detached {
await sendableFeed.getSendableItemData()
}.value
let items = sendableItems.compactMap {
RSSItem(context: moContext, sendableData: $0)
}
feed.feed.items = NSSet(array: items)
}
isLoading = false
}
}
}

View file

@ -3,12 +3,14 @@ import DesignSystem
import Explore
import Foundation
import StatusKit
import RSS
import SwiftUI
import Env
@MainActor
enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
case timeline, notifications, mentions, explore, messages, settings, other
case trending, federated, local
case trending, federated, local, rss
case profile
case bookmarks
case favorites
@ -74,9 +76,16 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
VStack {}
case .other:
EmptyView()
case .rss:
// FIXME: open in web view
RSSTab()
.withSafariRouter()
.environment(Self.routerPath)
}
}
private static let routerPath = RouterPath()
@ViewBuilder
var label: some View {
switch self {
@ -100,6 +109,8 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
Label("tab.settings", systemImage: iconName)
case .profile:
Label("tab.profile", systemImage: iconName)
case .rss:
Label("tab.rss", systemImage: iconName)
case .bookmarks:
Label("accessibility.tabs.profile.picker.bookmarks", systemImage: iconName)
case .favorites:
@ -153,6 +164,8 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
"newspaper"
case .other:
""
case .rss:
"dot.radiowaves.up.forward"
}
}
}
@ -170,6 +183,7 @@ class SidebarTabs {
.init(tab: .trending, enabled: true),
.init(tab: .federated, enabled: true),
.init(tab: .local, enabled: true),
.init(tab: .rss, enabled: true),
.init(tab: .notifications, enabled: true),
.init(tab: .mentions, enabled: true),
.init(tab: .messages, enabled: true),

View file

@ -38977,6 +38977,15 @@
}
}
}
},
"rss.item.url.unavailable" : {
},
"rss.item.url.unavailable.action.OK" : {
},
"rss.item.url.unavailable.message" : {
},
"see-more" : {
"extractionState" : "manual",
@ -74098,6 +74107,9 @@
}
}
}
},
"tab.rss" : {
},
"tab.settings" : {
"localizations" : {
@ -79155,4 +79167,4 @@
}
},
"version" : "1.0"
}
}

View file

@ -25,8 +25,24 @@ final class HTMLStringTests: XCTestCase {
func testHTMLStringInit() throws {
let decoder = JSONDecoder()
let complexContent = "\"" + """
<code>some View</code>
"""
.replacingOccurrences(of: "\"", with: "\\\"")
.replacingOccurrences(of: "\n", with: "\\n")
.replacingOccurrences(of: "<code>", with: "`")
.replacingOccurrences(of: "</code>", with: "`")
+ "\""
var htmlString = try decoder.decode(HTMLString.self, from: Data(complexContent.utf8))
XCTAssertEqual("This is a test", htmlString.asRawText)
XCTAssertEqual("<p>This is a test</p>", htmlString.htmlValue)
XCTAssertEqual("This is a test", htmlString.asMarkdown)
XCTAssertEqual(0, htmlString.statusesURLs.count)
XCTAssertEqual(0, htmlString.links.count)
let basicContent = "\"<p>This is a test</p>\""
var htmlString = try decoder.decode(HTMLString.self, from: Data(basicContent.utf8))
htmlString = try decoder.decode(HTMLString.self, from: Data(basicContent.utf8))
XCTAssertEqual("This is a test", htmlString.asRawText)
XCTAssertEqual("<p>This is a test</p>", htmlString.htmlValue)
XCTAssertEqual("This is a test", htmlString.asMarkdown)

8
Packages/RSS/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1520"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "RSS"
BuildableName = "RSS"
BlueprintName = "RSS"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "RSSTests"
BuildableName = "RSSTests"
BlueprintName = "RSSTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "RSS"
BuildableName = "RSS"
BlueprintName = "RSS"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1520"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "RSSTests"
BuildableName = "RSSTests"
BlueprintName = "RSSTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,51 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "RSS",
defaultLocalization: "en",
platforms: [
.iOS(.v17),
.visionOS(.v1),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "RSS",
targets: ["RSS"]),
],
dependencies: [
.package(name: "DesignSystem", path: "../DesignSystem"),
.package(name: "StatusKit", path: "../StatusKit"),
.package(url: "https://github.com/Ranchero-Software/RSParser.git", from: "2.0.3"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "RSS",
dependencies: [
.product(name: "DesignSystem", package: "DesignSystem"),
.product(name: "StatusKit", package: "StatusKit"),
.product(name: "RSParser", package: "RSParser"),
],
resources: [
.process("Models/RSSModel.xcdatamodeld"), // Process the model
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
.unsafeFlags(["-enable-bare-slash-regex"]),
]
),
.testTarget(
name: "RSSTests",
dependencies: ["RSS"],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
.unsafeFlags(["-enable-bare-slash-regex"]),
]
),
]
)

View file

@ -0,0 +1,137 @@
//
// HTMLTools.swift
// IceCubesApp
//
// Created by Duong Thai on 02/03/2024.
//
import RSParser
import SwiftSoup
public enum HTMLTools {
static func convert(_ html: String, baseURL _: URL?, withMedia: Bool = false) -> NSAttributedString? {
let string = if withMedia {
html
} else {
html.replacing(
/(<img.*?>|<video.*?<\/video>|<iframe.*?<\/iframe>)/,
with: { _ in "" }
)
}
let data = Data(string.utf8)
return try? NSAttributedString(
data: data,
options: [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue,
],
documentAttributes: nil
)
}
public static func getFaviconOf(html: String, sourceURL: URL) -> URL? {
let pattern = MetadataPattern.favicon
guard let match = html.firstMatch(of: pattern),
let string = NonEmptyString(String(match.1)),
let url = URL(string: string.string)
else { return nil }
if url.host() != nil {
return url
} else if let host = sourceURL.host() {
return URL(string: "https://" + host + url.absoluteString)
} else {
return nil
}
}
public static func getIconOf(html: String, sourceURL: URL) -> URL? {
let pattern = MetadataPattern.icon
let matches = html.matches(of: pattern)
let icons: [Icon] = matches
.compactMap {
guard let url = URL(string: String($0.1)) else { return nil }
guard let d_w = Double(String($0.2)) else { return nil }
let width = CGFloat(d_w)
guard let h_w = Double(String($0.3)) else { return nil }
let height = CGFloat(h_w)
return Icon(url: url, with: width, height: height)
}
.sorted { $0.with > $1.with }
return icons.first?.url
}
public static func getTitleOf(html: String) -> NonEmptyString? {
let pattern = MetadataPattern.title
guard let match = html.firstMatch(of: pattern) else { return nil }
return NonEmptyString(String(match.1))
}
public static func getContentTypeOf(html: String) -> NonEmptyString? {
let pattern = MetadataPattern.type
guard let match = html.firstMatch(of: pattern) else { return nil }
return NonEmptyString(String(match.1))
}
public static func getPreviewImageOf(html: String) -> URL? {
let pattern = MetadataPattern.image
guard let match = html.firstMatch(of: pattern),
let string = NonEmptyString(String(match.1)),
let url = URL(string: string.string)
else { return nil }
return url
}
public static func getURLOf(html: String) -> URL? {
let pattern = MetadataPattern.url
guard let match = html.firstMatch(of: pattern),
let url = URL(string: String(match.1))
else { return nil }
return url
}
public static func getSiteNameOf(html: String) -> NonEmptyString? {
let pattern = MetadataPattern.siteName
guard let match = html.firstMatch(of: pattern) else { return nil }
return NonEmptyString(String(match.1))
}
public static func getFirstImageOf(html: String) -> URL? {
guard let match = html.firstMatch(of: /<img[\s\S]+?src="(.+?)"/) else { return nil }
return URL(string: String(match.1))
}
}
private struct Icon {
let url: URL
let with: CGFloat
let height: CGFloat
}
private enum MetadataPattern {
static let favicon = /<link[\s\S]*?rel=\".*?icon\"[\s\S]+?href=\"(.+?)\"/
static let icon = /<link[\s\S]*?rel=\".*?icon\"[\s\S]+?href=\"(.+?)\".+?sizes=\"(\d+?)x(\d+?)"/
static let title = /<meta[\s\S]*?property=\"og:title\" content=\"(.+?)\"/
static let type = /<meta[\s\S]*?property=\"og:type\" content=\"(.+?)\"/
static let image = /<meta[\s\S]*?property=\"og:image\" content=\"(.+?)\"/
static let url = /<meta[\s\S]*?property=\"og:url\" content=\"(.+?)\"/
static let siteName = /<meta[\s\S]*?property=\"og:site_name\" content=\"(.+?)\"/
}
public struct NonEmptyString {
let string: String
init?(_ string: String) {
let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty { return nil } else { self.string = trimmed }
}
}
extension ParsedItem: @unchecked Sendable {} // checked

View file

@ -0,0 +1,36 @@
//
// RSSDataController.swift
//
//
// Created by Duong Thai on 05/03/2024.
//
import CoreData
public class RSSDataController {
private static let modelName = "RSSModel"
public let container: NSPersistentContainer
public static let shared = RSSDataController()
private init() {
guard
let modelURL = Bundle
.module
.url(forResource: Self.modelName, withExtension: "momd")
else {
fatalError("Failed to get \(Self.modelName)'s URL.")
}
guard let mom = NSManagedObjectModel(contentsOf: modelURL) else {
fatalError("Failed to initialize NSManagedObjectModel from: \(modelURL)")
}
self.container = NSPersistentContainer(name: Self.modelName, managedObjectModel: mom)
self.container.loadPersistentStores { _, error in
if let error {
fatalError("Core Data failed to load Persistent Stores:\(error.localizedDescription)")
}
}
}
}

View file

@ -0,0 +1,37 @@
//
// RSSAuthor+init.swift
//
//
// Created by Duong Thai on 05/03/2024.
//
import CoreData
import RSParser
extension RSSAuthor {
public convenience init?(
context: NSManagedObjectContext,
parsedAuthor: ParsedAuthor,
feedURL: URL
) {
self.init(context: context)
guard let displayName = [
parsedAuthor.name,
parsedAuthor.emailAddress,
parsedAuthor.url,
parsedAuthor.avatarURL
].compactMap({ $0 }).first
else { return nil }
self.id = feedURL.appending(path: "ica-rss-author/\(displayName)")
self.displayName = displayName
self.name = parsedAuthor.name
self.url = if let url = parsedAuthor.url { URL(string: url) } else { nil }
self.avatarURL = if let avatarURL = parsedAuthor.avatarURL { URL(string: avatarURL) } else { nil }
// self.email = if let email = parsedAuthor.emailAddress { RSSEmail(email) } else { nil }
self.email = if let email = parsedAuthor.emailAddress { email } else { nil }
}
}

View file

@ -0,0 +1,102 @@
//
// RSSFeed+SendableData.swift.swift
//
//
// Created by Duong Thai on 07/03/2024.
//
import RSParser
extension RSSFeed {
public struct SendableData: Sendable {
public let parsedFeed: ParsedFeed
public let sourceURL : URL
public let enhancedIconURL : URL?
public let enhancedFaviconURL : URL?
public init(parsedFeed: ParsedFeed, sourceURL: URL) {
self.parsedFeed = parsedFeed
self.sourceURL = sourceURL
if parsedFeed.iconURL == nil && parsedFeed.faviconURL == nil {
let pageURL: URL?
let _feedURL = parsedFeed.getRSSFeedURL(sourceURL: sourceURL)
if let homePageURL = URL(string: parsedFeed.homePageURL ?? "")
{
pageURL = homePageURL
} else if
let host = _feedURL.host,
let scheme = _feedURL.scheme,
let hostURL = URL(string: scheme + "://" + host)
{
pageURL = hostURL
} else {
pageURL = nil
}
if let pageURL,
let pageData = try? Data(contentsOf: pageURL),
let pageHTML = String(bytes: pageData, encoding: .utf8)
{
self.enhancedIconURL = HTMLTools.getIconOf(html: pageHTML, sourceURL: pageURL)
self.enhancedFaviconURL = HTMLTools.getFaviconOf(html: pageHTML, sourceURL: pageURL)
} else {
self.enhancedIconURL = nil
self.enhancedFaviconURL = nil
}
} else {
self.enhancedIconURL = nil
self.enhancedFaviconURL = nil
}
}
private var parsedItems: [ParsedItem] { Array(self.parsedFeed.items) }
public func getSendableItemData() async -> [RSSItem.SendableData] {
return await withTaskGroup(of: RSSItem.SendableData?.self) { taskGroup in
let threshold = 5
let subArrays = stride(from: 0, to: parsedItems.count, by: threshold)
.map {
parsedItems[$0..<min($0 + threshold, parsedItems.count)]
}
var accumulatedItems = [RSSItem.SendableData]()
for s in subArrays {
for item in s {
taskGroup.addTask {
await Task.detached(priority: .userInitiated) {
RSSItem.SendableData(
parsedItem: item,
feedURL: self.sourceURL,
feedAuthors: self.parsedFeed.authors ?? []
)
}.value
}
}
for await i in taskGroup {
if let i {
accumulatedItems.append(i)
}
}
}
return accumulatedItems
}
}
}
}
extension ParsedFeed: @unchecked Sendable {} // checked
extension ParsedFeed {
func getRSSFeedURL(sourceURL: URL) -> URL {
if
let feedURL = self.feedURL,
let url = URL(string: feedURL)
{ url } else { sourceURL }
}
}

View file

@ -0,0 +1,55 @@
//
// RSSFeed+init.swift
//
//
// Created by Duong Thai on 05/03/2024.
//
import CoreData
import RSParser
extension RSSFeed {
public convenience init(
context: NSManagedObjectContext,
sendableData: RSSFeed.SendableData
) {
self.init(context: context)
let _feedURL = sendableData.parsedFeed.getRSSFeedURL(sourceURL: sendableData.sourceURL)
self.feedURL = _feedURL
self.homePageURL = if let homePageURL = sendableData.parsedFeed.homePageURL {
URL(string: homePageURL)
} else {
nil
}
self.type = RSSFeedType(sendableData.parsedFeed.type).rawValue
self.title = sendableData.parsedFeed.title
self.feedDescription = sendableData.parsedFeed.feedDescription
self.nextURL = if let nextURL = sendableData.parsedFeed.nextURL {
URL(string: nextURL)
} else {
nil
}
self.iconURL = if let iconURLString = sendableData.parsedFeed.iconURL {
URL(string: iconURLString)
} else if let iconURL = sendableData.enhancedIconURL {
iconURL
}else {
nil
}
self.faviconURL = if let faviconURLString = sendableData.parsedFeed.faviconURL {
URL(string: faviconURLString)
} else if let faviconURL = sendableData.enhancedFaviconURL {
faviconURL
} else {
nil
}
self.expired = sendableData.parsedFeed.expired
}
}

View file

@ -0,0 +1,28 @@
//
// RSSFeedType.swift
//
//
// Created by Duong Thai on 07/03/2024.
//
import RSParser
public enum RSSFeedType: String, Codable {
case rss
case atom
case jsonFeed
case rssInJSON
case unknown
case notAFeed
init(_ parsedFeedType: FeedType) {
switch parsedFeedType {
case .rss: self = .rss
case .atom: self = .atom
case .jsonFeed: self = .jsonFeed
case .rssInJSON: self = .rssInJSON
case .unknown: self = .unknown
case .notAFeed: self = .notAFeed
}
}
}

View file

@ -0,0 +1,105 @@
//
// RSSItem+SendableData.swift
//
//
// Created by Duong Thai on 07/03/2024.
//
import CoreData
import RSParser
import SwiftUI
extension RSSItem {
public struct SendableData: Sendable {
let parsedItem: ParsedItem
let feedURL: URL
let feedAuthors: Set<ParsedAuthor>
let previewImageData: (url: URL, size: CGSize)?
init(parsedItem: ParsedItem, feedURL: URL, feedAuthors: Set<ParsedAuthor>) {
self.parsedItem = parsedItem
self.feedURL = feedURL
self.feedAuthors = feedAuthors
self.previewImageData = parsedItem.getRSSPreviewImageData()
}
}
}
extension ParsedAuthor: @unchecked Sendable {} // checked
extension ParsedItem {
func getRSSSummary() -> String {
let _summary = if let summary = self.summary {
summary.replacingOccurrences(of: "\n\n", with: "\n")
.trimmingCharacters(in: .whitespacesAndNewlines)
} else if let contentText = self.contentText {
contentText
.replacingOccurrences(of: "\n\n", with: "\n")
.trimmingCharacters(in: .whitespacesAndNewlines)
} else if
let contentHTML = self.contentHTML,
let contentHTML = HTMLTools.convert(contentHTML, baseURL: self.getRSSURL())?.string
{
contentHTML
.replacingOccurrences(of: "\n\n", with: "\n")
.trimmingCharacters(in: .whitespacesAndNewlines)
} else {
""
}
return _summary
.replacingOccurrences(of: "\n\n", with: "\n")
.trimmingCharacters(in: .whitespacesAndNewlines)
}
func getRSSPreviewImageData() -> (url: URL, size: CGSize)? {
if
let imageURLString = self.imageURL,
let imageURL = URL(string: imageURLString),
let imageData = try? Data(contentsOf: imageURL),
let image = UIImage(data: imageData)
{
(url: imageURL, size: image.size)
} else if
let contentHTML = self.contentHTML,
let imageURL = HTMLTools.getFirstImageOf(html: contentHTML),
let imageData = try? Data(contentsOf: imageURL),
let image = UIImage(data: imageData)
{
(url: imageURL, size: image.size)
} else {
nil
}
}
func getRSSURL() -> URL? {
if let url = self.url {
URL(string: url)
} else if let externalURL = self.externalURL {
URL(string: externalURL)
} else if let uniqueURL = URL(string: self.uniqueID) {
uniqueURL
} else {
nil
}
}
func getRSSDate() -> Date {
self.dateModified ?? self.datePublished ?? .now
}
func getRSSAuthors(
context: NSManagedObjectContext,
feedAuthors: Set<ParsedAuthor>,
feedURL: URL
) -> NSSet {
let authors = self.authors?.union(feedAuthors)
.compactMap {
RSSAuthor(context: context, parsedAuthor: $0, feedURL: feedURL)
}
?? []
return NSSet(array: authors)
}
}

View file

@ -0,0 +1,56 @@
//
// RSSItem+init.swift
//
//
// Created by Duong Thai on 05/03/2024.
//
import CoreData
extension RSSItem {
public convenience init?(
context: NSManagedObjectContext,
sendableData: RSSItem.SendableData
) {
self.init(context: context)
let title = sendableData.parsedItem.title ?? ""
let summary = sendableData.parsedItem.getRSSSummary()
let previewImageData = sendableData.previewImageData
if
title.isEmpty,
summary.isEmpty,
previewImageData == nil
{ return nil }
self.title = title
self.summary = summary
if let previewImageData {
self.previewImageURL = previewImageData.url
self.previewImageWidth = previewImageData.size.width
self.previewImageHeight = previewImageData.size.height
}
self.uniqueID = sendableData.parsedItem.uniqueID
self.url = sendableData.parsedItem.getRSSURL()
self.date = sendableData.parsedItem.getRSSDate()
self.authors = sendableData.parsedItem
.getRSSAuthors(context: context, feedAuthors: sendableData.feedAuthors, feedURL: sendableData.feedURL)
self.tags = NSSet(set: sendableData.parsedItem.tags ?? [])
}
public var authorsAsString: String? {
if authors?.allObjects.isEmpty ?? true {
nil
} else {
(authors?.allObjects as? [RSSAuthor])?.map {
$0.displayName ?? ""
}
.sorted { $0 < $1 }
.joined(separator: "")
}
}
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="23D60" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="RSSFeed" representedClassName="RSSFeed" syncable="YES" codeGenerationType="class">
<attribute name="attribute" optional="YES" attributeType="String"/>
<attribute name="feedURL" optional="YES" attributeType="URI"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="feedURL"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
</model>

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="23D60" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="RSSAuthor" representedClassName="RSSAuthor" syncable="YES" codeGenerationType="class">
<attribute name="avatarURL" optional="YES" attributeType="URI"/>
<attribute name="displayName" optional="YES" attributeType="String"/>
<attribute name="email" optional="YES" attributeType="String"/>
<attribute name="id" optional="YES" attributeType="URI"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="URI"/>
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RSSFeed" inverseName="authors" inverseEntity="RSSFeed"/>
<relationship name="items" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="RSSItem" inverseName="authors" inverseEntity="RSSItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RSSFeed" representedClassName="RSSFeed" syncable="YES" codeGenerationType="class">
<attribute name="expired" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="faviconURL" optional="YES" attributeType="URI"/>
<attribute name="feedDescription" optional="YES" attributeType="String"/>
<attribute name="feedURL" optional="YES" attributeType="URI"/>
<attribute name="homePageURL" optional="YES" attributeType="URI"/>
<attribute name="iconURL" optional="YES" attributeType="URI"/>
<attribute name="nextURL" optional="YES" attributeType="URI"/>
<attribute name="title" optional="YES" attributeType="String"/>
<attribute name="type" optional="YES" attributeType="String"/>
<relationship name="authors" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="RSSAuthor" inverseName="feed" inverseEntity="RSSAuthor"/>
<relationship name="items" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="RSSItem" inverseName="feed" inverseEntity="RSSItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="feedURL"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RSSItem" representedClassName="RSSItem" syncable="YES" codeGenerationType="class">
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="language" optional="YES" attributeType="String"/>
<attribute name="previewImageHeight" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="previewImageURL" optional="YES" attributeType="URI"/>
<attribute name="previewImageWidth" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="summary" optional="YES" attributeType="String"/>
<attribute name="title" optional="YES" attributeType="String"/>
<attribute name="uniqueID" optional="YES" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="URI"/>
<relationship name="authors" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="RSSAuthor" inverseName="items" inverseEntity="RSSAuthor"/>
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RSSFeed" inverseName="items" inverseEntity="RSSFeed"/>
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="RSSTag" inverseName="items" inverseEntity="RSSTag"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="uniqueID"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RSSTag" representedClassName="RSSTag" syncable="YES" codeGenerationType="class">
<attribute name="name" optional="YES" attributeType="String"/>
<relationship name="items" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="RSSItem" inverseName="tags" inverseEntity="RSSItem"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="name"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
</model>

View file

@ -0,0 +1,185 @@
//
// RSSExampleData.swift
// IceCubesApp
//
// Created by Duong Thai on 28/02/2024.
//
#if DEBUG
import Foundation
import RSParser
enum RSSExampleData {
static let htmlString = """
<p>Swift 5.8 is now officially released! 🎉 This release includes major additions to the <a href="#language-and-standard-library">language and standard library</a>, including <code class="language-plaintext highlighter-rouge">hasFeature</code> to support piecemeal adoption of upcoming features, an improved <a href="#developer-experience">developer experience</a>, improvements to tools in the Swift ecosystem including <a href="#swift-docc">Swift-DocC</a>, <a href="#swift-package-manager">Swift Package Manager</a>, and <a href="#swiftsyntax">SwiftSyntax</a>, refined <a href="#windows-platform">Windows support</a>, and more.</p>
<p>Thank you to everyone in the Swift community who made this release possible. Your Swift Forums discussions, bug reports, pull requests, educational content, and other contributions are always appreciated!</p>
<p>For a quick dive into some of whats new in Swift 5.8, check out this <a href="https://github.com/twostraws/whats-new-in-swift-5-8">playground</a> put together by Paul Hudson.</p>
<p><a href="https://docs.swift.org/swift-book/documentation/the-swift-programming-language/">The Swift Programming Language</a> book has been updated for Swift 5.8 and is now published with DocC. This is the official Swift guide and a great entry point for those new to Swift. The Swift community also maintains a number of <a href="/documentation/tspl/#translations">translations</a>.</p>
<h2 id="language-and-standard-library">Language and Standard Library</h2>
<p>Swift 5.8 enables you to start incrementally preparing your projects for Swift 6 by <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0362-piecemeal-future-features.md">using <em>upcoming features</em></a>. By default, upcoming features are disabled. To enable a feature, pass the compiler flag <code class="language-plaintext highlighter-rouge">-enable-upcoming-feature</code> followed by the features identifier.</p>
<p>Feature identifiers can also be <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0362-piecemeal-future-features.md#feature-detection-in-source-code">used in source code</a> using <code class="language-plaintext highlighter-rouge">#if hasFeature(FeatureIdentifier)</code> so that code can still compile with older tools where the upcoming feature is not available.</p>
<p>Swift 5.8 includes upcoming features for the following Swift evolution proposals:</p>
<ul>
<li>SE-0274: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0274-magic-file.md">Concise magic file names</a> (<code class="language-plaintext highlighter-rouge">ConciseMagicFile</code>)</li>
<li>SE-0286: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0286-forward-scan-trailing-closures.md">Forward-scan matching for trailing closures</a> (<code class="language-plaintext highlighter-rouge">ForwardTrailingClosures</code>)</li>
<li>SE-0335: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0335-existential-any.md">Introduce existential any</a> (<code class="language-plaintext highlighter-rouge">ExistentialAny</code>)</li>
<li>SE-0354: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0354-regex-literals.md">Regex literals</a> (<code class="language-plaintext highlighter-rouge">BareSlashRegexLiterals</code>)</li>
</ul>
<p>For example, building the following file at <code class="language-plaintext highlighter-rouge">/Users/example/Desktop/0274-magic-file.swift</code> in a module called <code class="language-plaintext highlighter-rouge">MagicFile</code> with <code class="language-plaintext highlighter-rouge">-enable-experimental-feature ConciseMagicFile</code> will opt into the concise format for <code class="language-plaintext highlighter-rouge">#file</code> and <code class="language-plaintext highlighter-rouge">#filePath</code> described in SE-0274:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">print</span><span class="p">(</span><span class="k">#file</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="k">#filePath</span><span class="p">)</span>
<span class="nf">fatalError</span><span class="p">(</span><span class="s">"Something bad happened!"</span><span class="p">)</span>
</code></pre></div></div>
<p>The above code will produce the following output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>MagicFile/0274-magic-file.swift
/Users/example/Desktop/0274-magic-file.swift
Fatal error: Something bad happened!: file MagicFile/0274-magic-file.swift, line 3
</code></pre></div></div>
<p>Swift 5.8 also includes <em>conditional attributes</em> to reduce the maintenance cost of libraries that support multiple Swift tools versions. <code class="language-plaintext highlighter-rouge">#if</code> checks can now surround attributes on a declaration, and a new <code class="language-plaintext highlighter-rouge">hasAttribute(AttributeName)</code> conditional directive can be used to check whether the compiler version has support for the attribute with the name <code class="language-plaintext highlighter-rouge">AttributeName</code> in the current language mode:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#if hasAttribute(preconcurrency)</span>
<span class="kd">@preconcurrency</span>
<span class="cp">#endif</span>
<span class="kd">protocol</span> <span class="kt">P</span><span class="p">:</span> <span class="kt">Sendable</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>
</code></pre></div></div>
<p>Swift 5.8 brings other language and standard library enhancements, including <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0375-opening-existential-optional.md">unboxing for <code class="language-plaintext highlighter-rouge">any</code> arguments to optional parameters</a>, <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0373-vars-without-limits-in-result-builders.md">local wrapped properties in result builders</a>, <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0369-add-customdebugdescription-conformance-to-anykeypath.md">improved debug printing for key paths</a>, and more.</p>
<p>You can find the complete list of Swift Evolution proposals that were implemented in Swift 5.8 in the <a href="#swift-evolution-appendix">Swift Evolution Appendix</a> below.</p>
<h2 id="developer-experience">Developer Experience</h2>
<h3 id="improved-result-builder-implementation">Improved Result Builder Implementation</h3>
<p>The result builder implementation has been reworked in Swift 5.8 to greatly improve compile-time performance, code completion results, and diagnostics. The Swift 5.8 result builder implementation enforces stricter type inference that matches the semantics in <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0289-result-builders.md">SE-0289: Result Builders</a>, which has an impact on some existing code that relied on invalid type inference.</p>
<p>The new implementation takes advantage of the <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0326-extending-multi-statement-closure-inference.md">extended multi-statement closure inference</a> introduced in Swift 5.7 and applies the result builder transformation exactly as specified by the result builder proposal - a source-level transformation which is type-checked like a multi-statement closure. Doing so enables the compiler to take advantage of all the benefits of the improved closure inference for result builder-transformed code, including optimized type-checking performance (especially in invalid code) and improved error messages.</p>
<p>For more details, please refer to the <a href="https://forums.swift.org/t/improved-result-builder-implementation-in-swift-5-8/63192">Swift Forums post</a> that outlines the improvements and provides more information about invalid inference scenarios.</p>
<h2 id="ecosystem">Ecosystem</h2>
<h3 id="swift-docc">Swift-DocC</h3>
<p>As <a href="https://www.swift.org/blog/tspl-on-docc/">announced in February</a>, The Swift Programming Language book has been converted to Swift-DocC and made <a href="https://github.com/apple/swift-book">open source</a>, and with it came some enhancements to Swift-DocC itself in the form of <a href="https://www.swift.org/documentation/docc/options">option directives</a> you can use to change the behavior of your generated documentation. Swift-DocC has also added some new directives to create more <a href="https://www.swift.org/documentation/docc/api-reference-syntax#creating-custom-page-layouts">dynamic documentation pages</a>, including <a href="https://www.swift.org/documentation/docc/row">Grid-based layouts</a> and <a href="https://www.swift.org/documentation/docc/tab">tab navigators</a>.</p>
<p>To take things even further, you can now <a href="https://www.swift.org/documentation/docc/customizing-the-appearance-of-your-documentation-pages">customize the appearance of your documentation pages</a> with color, font, and icon customizations. Navigation also took a step forward with quick navigation, allowing fuzzy in-project search:</p>
<p><img src="/assets/images/5.8-blog/docc-fuzzy-search.png" alt="A DocC documentation page showing a quick navigation overlay showing fuzzy documentation search" /></p>
<p>Swift-DocC also now supports documenting extensions to types from other modules. This is an opt-in feature and can be <a href="https://apple.github.io/swift-docc-plugin/documentation/swiftdoccplugin/generating-documentation-for-extended-types">enabled by adding the <code class="language-plaintext highlighter-rouge">--include-extended-types</code> flag when using the Swift-DocC plugin</a>.</p>
<p><img src="/assets/images/5.8-blog/docc-extended-type.png" alt="A documentation page featuring an extension to the standard library's Collection type." /></p>
<h3 id="swift-package-manager">Swift Package Manager</h3>
<p>Following are some highlights from the changes introduced to the <a href="https://github.com/apple/swift-package-manager">Swift Package Manager</a> in Swift 5.8:</p>
<ul>
<li>
<p><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0362-piecemeal-future-features.md">SE-0362</a>: Targets can now specify the upcoming language features they require. <code class="language-plaintext highlighter-rouge">Package.swift</code> manifest syntax has been expanded with an API to include setting <code class="language-plaintext highlighter-rouge">enableUpcomingFeature</code> and <code class="language-plaintext highlighter-rouge">enableExperimentalFeature</code> flags at the target level.</p>
</li>
<li>
<p><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0378-package-registry-auth.md">SE-0378</a>: Token authentication when interacting with a package registry is now supported. The <code class="language-plaintext highlighter-rouge">swift package-registry</code> command has two new subcommands <code class="language-plaintext highlighter-rouge">login</code> and <code class="language-plaintext highlighter-rouge">logout</code> for adding/removing registry credentials.</p>
</li>
<li>
<p>Exposing an executable product that consists solely of a binary target that is backed by an artifact bundle is now allowed. This allows vending binary executables as their own separate package, independently of the plugins that are using them.</p>
</li>
<li>
<p>In packages using tools version 5.8 or later, Foundation is no longer implicitly imported into package manifests. If Foundation APIs are used, the module needs to be imported explicitly.</p>
</li>
</ul>
<p>See the <a href="https://github.com/apple/swift-package-manager/blob/main/CHANGELOG.md#swift-58">Swift Package Manager changelog</a> for the complete list of changes.</p>
<h3 id="swiftsyntax">SwiftSyntax</h3>
<p>With the Swift 5.8-aligned release of <a href="https://github.com/apple/swift-syntax">SwiftSyntax</a>, SwiftSyntax contains a completely re-written parser that is implemented entirely in Swift instead of relying on the C++ parser to produce a SwiftSyntax tree. While the Swift compiler still uses the old parser implemented in C++, the eventual goal is to replace the old parser entirely. The new parser has a number of advantages:</p>
<ul>
<li>Contributing to or depending on SwiftSyntax is now as easy as any other Swift package. This greatly lowers the barrier of entry for new contributors and adopters.</li>
<li>The new parser has been designed with error recovery as a primary goal. It is more tolerant of parsing errors and produces better error messages.</li>
<li>SwiftSyntaxBuilder allows generating source code in a declarative way using a mixture of result builders and string interpolation. An example can be found <a href="https://github.com/apple/swift-syntax/blob/release/5.8/Examples/CodeGenerationUsingSwiftSyntaxBuilder.swift">here</a>.</li>
</ul>
<h3 id="windows-platform">Windows Platform</h3>
<p>Swift 5.8 continues the incremental improvements to the Windows toolchain. Some of the important work that has gone into this release cycle includes:</p>
<ul>
<li>The Windows toolchain has reduced some of its dependency on environment variables. <code class="language-plaintext highlighter-rouge">DEVELOPER_DIR</code> was previously needed to locate components and this is no longer required. This cleans up the installation and enables us to get closer to per-user installation.</li>
<li>ICU has been changed to static linking. This reduces the number of files that need to be distributed and reduces the number of dependencies that a shipping product requires. This was made possible by the removal of the ICU dependency in the Swift standard library.</li>
<li>Some of the initial work to support C++ interop on Windows has been merged and is available in the toolchain. This includes the work towards modularising the Microsoft C++ Runtime (msvcprt).</li>
<li>The <code class="language-plaintext highlighter-rouge">vcruntime</code> module has been renamed to <code class="language-plaintext highlighter-rouge">visualc</code>. This better reflects the module and paves the road for future enhancements for bridging the Windows platform libraries.</li>
<li>A significant amount of work for improving path handling in the Swift Package Manager has been merged. This should help make Swift Package Manager more robust on Windows and improve interactions with SourceKit-LSP.</li>
<li>SourceKit-LSP has benefited from several robustness improvements. Cross-module references are now more reliable and C/C++ references have been improved thanks to the enhanced path handling in SwiftPM which ensures that files are correctly identified.</li>
</ul>
<h2 id="downloads">Downloads</h2>
<p>Official binaries are <a href="https://swift.org/download/">available for download</a> from <a href="http://swift.org/">Swift.org</a> for Xcode, Windows, and Linux. The Swift 5.8 compiler is also included in <a href="https://apps.apple.com/app/xcode/id497799835">Xcode 14.3</a>.</p>
<h2 id="swift-evolution-appendix">Swift Evolution Appendix</h2>
<p>The following language, standard library, and Swift Package Manager proposals were accepted through the <a href="https://github.com/apple/swift-evolution">Swift Evolution</a> process and <a href="https://apple.github.io/swift-evolution/#?version=5.8">implemented in Swift 5.8</a>.</p>
<p><strong>Language and Standard Library</strong></p>
<ul>
<li>SE-0274: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0274-magic-file.md">Concise magic file names</a></li>
<li>SE-0362: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0362-piecemeal-future-features.md">Piecemeal adoption of upcoming language improvements</a></li>
<li>SE-0365: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0365-implicit-self-weak-capture.md">Allow implicit self for weak self captures, after self is unwrapped</a></li>
<li>SE-0367: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0367-conditional-attributes.md">Conditional compilation for attributes</a></li>
<li>SE-0368: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0368-staticbigint.md">StaticBigInt</a></li>
<li>SE-0369: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0369-add-customdebugdescription-conformance-to-anykeypath.md">Add CustomDebugStringConvertible conformance to AnyKeyPath</a></li>
<li>SE-0370: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0370-pointer-family-initialization-improvements.md">Pointer Family Initialization Improvements and Better Buffer Slices</a></li>
<li>SE-0372: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0372-document-sorting-as-stable.md">Document Sorting as Stable</a></li>
<li>SE-0373: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0373-vars-without-limits-in-result-builders.md">Lift all limitations on variables in result builders</a></li>
<li>SE-0375: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0375-opening-existential-optional.md">Opening existential arguments to optional parameters</a></li>
<li>SE-0376: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0376-function-back-deployment.md">Function Back Deployment</a></li>
</ul>
<p><strong>Swift Package Manager</strong></p>
<ul>
<li>SE-0362: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0362-piecemeal-future-features.md">Piecemeal adoption of upcoming language improvements</a></li>
<li>SE-0378: <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0378-package-registry-auth.md">Package Registry Authentication</a></li>
</ul>
"""
@MainActor
static let content: NSAttributedString = HTMLTools.convert(Self.htmlString, baseURL: URL(string: "https://swift.org"))!
static let feed: ParsedFeed = {
let filePath = URL(string: #filePath)!
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("Tests/RSSTests/HTMLToolsTests/swift-org--atom.xml")
.absoluteString
let string = try! String(contentsOfFile: filePath)
return try! FeedParser.parse(
ParserData(url: "https://www.swift.org/atom.xml",
data: string.data(using: .utf8)!)
)!
}()
static let item: ParsedItem = { Self.feed.items.first! }()
}
#endif

View file

@ -0,0 +1,105 @@
//
// RSSUITextView.swift
// IceCubesApp
//
// Created by Duong Thai on 28/02/2024.
//
import SwiftUI
import DesignSystem
@MainActor
struct RSSItemDetailView: UIViewControllerRepresentable {
let content: NSAttributedString
func makeUIViewController(context: Context) -> RSSUITextViewController {
RSSUITextViewController(content)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
}
class RSSUITextViewController: UIViewController {
private var imageCache: [(attachment: NSTextAttachment, originalSize: CGSize)]?
private let textView: UITextView
init(_ content: NSAttributedString) {
let padding: CGFloat = 20
let textView = UITextView()
textView.isEditable = false
textView.textContainer.lineBreakMode = .byWordWrapping
textView.textContainer.widthTracksTextView = true
textView.textContainerInset = UIEdgeInsets(top: 0, left: padding, bottom: 0, right: padding)
textView.setContentHuggingPriority(.defaultHigh, for: .vertical)
textView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
textView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textView.isEditable = false
textView.attributedText = content
self.textView = textView
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
view = textView
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
resizeImage()
}
private func calculateDisplaySize(from originalSize: CGSize) -> CGSize {
let viewWidth = self.textView.frame.width - self.textView.textContainerInset.left - self.textView.textContainerInset.right
let viewHeight = self.textView.frame.height
guard viewWidth > 0, viewHeight > 0 else { return originalSize }
return if originalSize.width == 0 {
CGSize.zero
} else if originalSize.width < 100 {
// TODO: some authors use small use images to display special characters
// still don't know how to deal with it
originalSize
} else {
CGSize(
width: viewWidth,
height: originalSize.height * viewWidth / originalSize.width
)
}
}
private func resizeImage() {
if let imageCache {
for cache in imageCache {
let size = calculateDisplaySize(from: cache.originalSize)
cache.attachment.bounds = CGRect(origin: cache.attachment.bounds.origin, size: size)
}
} else {
imageCache = []
self.textView.attributedText.enumerateAttribute(.attachment, in: NSRange(location: 0, length: self.textView.attributedText.length)) { value, _, _ in
if let attachment = value as? NSTextAttachment,
let data = attachment.fileWrapper?.regularFileContents,
let image = UIImage(data: data)
{
let size = calculateDisplaySize(from: image.size)
imageCache?.append((attachment, size))
attachment.bounds = CGRect(origin: attachment.bounds.origin, size: size)
}
}
}
}
}
#Preview {
RSSItemDetailView(content: RSSExampleData.content)
.environment(Theme.shared)
}

View file

@ -0,0 +1,120 @@
//
// SwiftUIView.swift
//
//
// Created by Duong Thai on 03/03/2024.
//
import SwiftUI
import StatusKit
import DesignSystem
@MainActor
public struct RSSItemView: View {
@Environment(Theme.self) private var theme
private let viewModel: RSSItem
private var contentPadding: CGFloat {
theme.avatarPosition == .top
? 0
: AvatarView.FrameConfig.status.width + .statusColumnsSpacing
}
public init(_ viewModel: RSSItem) {
self.viewModel = viewModel
}
public var body: some View {
VStack(alignment: .leading, spacing: .statusComponentSpacing) {
HStack {
AvatarView(viewModel.feed?.iconURL ?? viewModel.feed?.faviconURL, config: .status)
.accessibility(addTraits: .isButton)
.contentShape(Circle())
.hoverEffect()
HStack(alignment: .bottom) {
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 2) {
Text(viewModel.feed?.feedURL?.host() ?? "")
.foregroundColor(theme.labelColor)
Image(systemName: "dot.radiowaves.up.forward")
.foregroundColor(theme.tintColor)
}
.font(.scaledSubheadline)
.fontWeight(.semibold)
.lineLimit(1)
Text(viewModel.authorsAsString ?? viewModel.feed?.feedURL?.host() ?? "")
.font(.scaledFootnote)
.foregroundStyle(.secondary)
.lineLimit(1)
}
Spacer()
Text(viewModel.date ?? .now, style: .offset)
.font(.scaledFootnote)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
.padding(.bottom, .statusComponentSpacing)
VStack(alignment: .leading, spacing: .statusComponentSpacing) {
if
let previewImageURL = viewModel.previewImageURL
{
let width = viewModel.previewImageWidth
let height = viewModel.previewImageHeight
RSSPreviewImage(url: previewImageURL, originalSize: CGSize(width: width, height: height))
}
Text(viewModel.title ?? "no title")
.font(.scaledHeadline)
.lineLimit(2)
.padding(.vertical, .statusComponentSpacing)
Text(viewModel.summary ?? "")
.font(.scaledBody)
.lineSpacing(CGFloat(theme.lineSpacing))
.lineLimit(10)
/*
Copy Link ???
Copy Text ???
Post this
*/
if let url = viewModel.url {
ShareLink(
item: url,
subject: Text(viewModel.title ?? ""),
message: Text(viewModel.summary ?? "")
)
.buttonStyle(.borderless)
.foregroundColor(Color(UIColor.secondaryLabel))
.padding(.vertical, .statusComponentSpacing)
// .padding(.horizontal, 8)
.contentShape(Rectangle())
#if targetEnvironment(macCatalyst)
.font(.scaledBody)
#else
.font(.body)
.dynamicTypeSize(.large)
#endif
}
}
.padding(.leading, contentPadding)
}
}
}
// FIXME: example data
//#Preview {
// Theme.shared.avatarShape = .circle
// Theme.shared.tintColor = .purple
// Theme.shared.avatarPosition = .top
//
// return RSSItemView(RSSExampleData.itemViewModel)
// .frame(width: 430)
// .environment(Theme.shared)
//}

View file

@ -0,0 +1,74 @@
//
// RSSPreviewImage.swift
// IceCubesApp
//
// Created by Duong Thai on 02/03/2024.
//
import SwiftUI
import NukeUI
struct RSSPreviewImage: View, Sendable {
private let url: URL
private let originalSize: CGSize
init(url: URL, originalSize: CGSize) {
self.url = url
self.originalSize = originalSize
}
public var body: some View {
_Layout(originalWidth: originalSize.width, originalHeight: originalSize.height) {
Rectangle()
.overlay {
LazyImage(url: url) { state in
if let image = state.image {
image.resizable().scaledToFill()
}
}
}
.cornerRadius(10)
.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(.gray.opacity(0.35), lineWidth: 1)
}
}
}
private struct _Layout: Layout {
let originalWidth: CGFloat
let originalHeight: CGFloat
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) -> CGSize {
guard !subviews.isEmpty else { return CGSize.zero }
return calculateSize(proposal)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache _: inout ()) {
guard let view = subviews.first else { return }
let size = calculateSize(proposal)
view.place(at: bounds.origin, proposal: ProposedViewSize(size))
}
private func calculateSize(_ proposal: ProposedViewSize) -> CGSize {
var size = switch (proposal.width, proposal.height) {
case (nil, nil):
CGSize(width: originalWidth, height: originalWidth)
case let (nil, .some(height)):
CGSize(width: originalWidth, height: min(height, originalWidth))
case (0, _):
CGSize.zero
case let (.some(width), _):
if originalWidth == 0 {
CGSize(width: width, height: width / 2)
} else {
CGSize(width: width, height: width / originalWidth * originalHeight)
}
}
size.height = min(size.height, 450)
return size
}
}
}

View file

@ -0,0 +1,151 @@
//
// GetMetadataTests.swift
//
//
// Created by Duong Thai on 02/03/2024.
//
import XCTest
@testable import RSS
final class GetMetadataTests: XCTestCase {
func test_Get_Metadata() throws {
let fileName = "wadetregaskis-com--a-brief-introduction-to-type-memory-layout-in-swift.html"
let sourceURL = URL(string: "https://wadetregaskis.com/a-brief-introduction-to-type-memory-layout-in-swift/")!
let content = Self.getStringFrom(fileName: fileName)
XCTAssertNil(HTMLTools.getTitleOf(html: content))
XCTAssertNil(HTMLTools.getContentTypeOf(html: content))
XCTAssertNil(HTMLTools.getPreviewImageOf(html: content))
XCTAssertNil(HTMLTools.getURLOf(html: content))
XCTAssertEqual(
HTMLTools.getFaviconOf(html: content, sourceURL: sourceURL)!,
URL(string: "https://wadetregaskis.com/wp-content/uploads/2016/03/Stitch-512x512-1-256x256.png")!
)
XCTAssertEqual(
HTMLTools.getIconOf(html: content, sourceURL: sourceURL)!,
URL(string: "https://wadetregaskis.com/wp-content/uploads/2016/03/Stitch-512x512-1-256x256.png")!
)
XCTAssertNil(HTMLTools.getSiteNameOf(html: content))
}
func test_Get_Metadata_1() throws {
let fileName = "swift-org--blog-summer-of-code-2023-summary.html"
let sourceURL = URL(string: "https://www.swift.org/blog/summer-of-code-2023-summary/")!
let content = Self.getStringFrom(fileName: fileName)
XCTAssertEqual(
HTMLTools.getTitleOf(html: content)!.string,
NonEmptyString("Swift Summer of Code 2023 Summary")!.string
)
XCTAssertEqual(
HTMLTools.getContentTypeOf(html: content)!.string,
NonEmptyString("article")!.string
)
XCTAssertEqual(
HTMLTools.getPreviewImageOf(html: content)!,
URL(string: "https://swift.org/apple-touch-icon-180x180.png")!
)
XCTAssertEqual(
HTMLTools.getURLOf(html: content)!,
URL(string: "https://swift.org/blog/summer-of-code-2023-summary/")!
)
XCTAssertEqual(
HTMLTools.getFaviconOf(html: content, sourceURL: sourceURL)!,
URL(string: "https://www.swift.org/favicon.ico")!
)
XCTAssertNil(HTMLTools.getIconOf(html: content, sourceURL: sourceURL))
XCTAssertEqual(
HTMLTools.getSiteNameOf(html: content)!.string,
NonEmptyString("Swift.org")!.string
)
}
func test_Get_Metadata_2() throws {
let fileName = "iso-500px-com--10-chilly-new-photos-from-500px-licensing.html"
let sourceURL = URL(string: "https://iso.500px.com/10-chilly-new-photos-from-500px-licensing/")!
let content = Self.getStringFrom(fileName: fileName)
XCTAssertEqual(
HTMLTools.getTitleOf(html: content)!.string,
NonEmptyString("10 chilly new photos from 500px Licensing Contributors")!.string
)
XCTAssertEqual(
HTMLTools.getContentTypeOf(html: content)!.string,
NonEmptyString("article")!.string
)
XCTAssertEqual(
HTMLTools.getPreviewImageOf(html: content)!,
URL(string: "https://iso.500px.com/wp-content/uploads/2024/01/Intense-pt.-2-By-Jagoda-Matejczuk-2-1500x1000.jpeg")!
)
XCTAssertEqual(
HTMLTools.getURLOf(html: content)!,
URL(string: "https://iso.500px.com/10-chilly-new-photos-from-500px-licensing/")!
)
XCTAssertEqual(
HTMLTools.getFaviconOf(html: content, sourceURL: sourceURL)!,
URL(string: "https://iso.500px.com/wp-content/themes/photoform/favicon.ico")!
)
XCTAssertEqual(
HTMLTools.getIconOf(html: content, sourceURL: sourceURL)!,
URL(string: "https://iso.500px.com/wp-content/uploads/2019/04/cropped-500px-logo-photography-social-media-design-thumb-192x192.jpg")!
)
XCTAssertEqual(
HTMLTools.getSiteNameOf(html: content)!.string,
NonEmptyString("500px")!.string
)
}
func test_Get_First_Image() throws {
let fileName = "wadetregaskis-com--a-brief-introduction-to-type-memory-layout-in-swift.html"
let content = Self.getStringFrom(fileName: fileName)
XCTAssertEqual(
HTMLTools.getFirstImageOf(html: content),
URL(string: "https://wadetregaskis.com/wp-content/uploads/2023/12/Blank-pixel.png")
)
let fileName1 = "swift-org--blog-summer-of-code-2023-summary.html"
let content1 = Self.getStringFrom(fileName: fileName1)
XCTAssertEqual(
HTMLTools.getFirstImageOf(html: content1),
URL(string: "https://www.gravatar.com/avatar/03cb20b97f6a14701c24c4e088b6af87?s=64&d=mp")
)
let fileName2 = "iso-500px-com--10-chilly-new-photos-from-500px-licensing.html"
let content2 = Self.getStringFrom(fileName: fileName2)
XCTAssertEqual(
HTMLTools.getFirstImageOf(html: content2),
URL(string: "https://www.facebook.com/tr?id=324942534599956&ev=PageView&noscript=1")
)
}
static private func getStringFrom(fileName: String) -> String {
/*
can be broken if moving related files
*/
let filePath = URL(string: #filePath)!
.deletingLastPathComponent()
.appendingPathComponent("HTMLFiles/\(fileName)")
.absoluteString
return try! String(contentsOfFile: filePath)
}
}

View file

@ -0,0 +1,774 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Swift.org - Swift Summer of Code 2023 Summary</title>
<meta name="author" content="Apple Inc." />
<meta name="viewport" content="width=device-width initial-scale=1" />
<meta name="description" content="The Swift project regularly participates in Google Summer of Code in order to help people new to the open source ecosystem dip their toes in contributing to Swift and its growing ecosystem.
">
<link rel="license" href="/LICENSE.txt" />
<link rel="stylesheet" media="all" href="/assets/stylesheets/application.css" />
<link rel="shortcut icon" sizes="16x16 24x24 32x32 48x48 64x64" type="image/vnd.microsoft.icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114x114.png" />
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png" />
<link rel="mask-icon" href="/assets/images/icon-swift.svg" color="#F05339" />
<link rel="canonical" href="https://swift.org/blog/summer-of-code-2023-summary/" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@SwiftLang" />
<meta name="twitter:title" content="Swift Summer of Code 2023 Summary" />
<meta name="twitter:description" content="The Swift project regularly participates in Google Summer of Code in order to help people new to the open source ecosystem dip their toes in contributing to Swift and its growing ecosystem.
" />
<meta property="og:site_name" content="Swift.org" />
<meta property="og:image" content="https://swift.org/apple-touch-icon-180x180.png" />
<meta property="og:type" content="article" />
<meta property="og:title" content="Swift Summer of Code 2023 Summary" />
<meta property="og:url" content="https://swift.org/blog/summer-of-code-2023-summary/" />
<meta property="og:description" content="The Swift project regularly participates in Google Summer of Code in order to help people new to the open source ecosystem dip their toes in contributing to Swift and its growing ecosystem.
" />
<meta property="article:published_time" content="2024-02-13T06:00:00-04:00" />
<meta property="article:modified_time" content="2024-03-02T07:00:07-04:00" />
</head>
<body>
<script src="/assets/javascripts/color-scheme-toggle.js"></script>
<header class="site-navigation">
<div class="wrapper">
<h1 id="logo">
<a href="/" title="Swift.org">
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 191.186 59.391"><path fill="#F05138" d="M59.387 16.45a82.463 82.463 0 0 0-.027-1.792c-.035-1.301-.112-2.614-.343-3.9-.234-1.307-.618-2.523-1.222-3.71a12.464 12.464 0 0 0-5.453-5.452C51.156.992 49.941.609 48.635.374c-1.288-.232-2.6-.308-3.902-.343a85.714 85.714 0 0 0-1.792-.027C42.23 0 41.52 0 40.813 0H18.578c-.71 0-1.419 0-2.128.004-.597.004-1.195.01-1.792.027-.325.009-.651.02-.978.036-.978.047-1.959.133-2.924.307-.98.176-1.908.436-2.811.81A12.503 12.503 0 0 0 3.89 3.89a12.46 12.46 0 0 0-2.294 3.158C.992 8.235.61 9.45.374 10.758c-.231 1.286-.308 2.599-.343 3.9a85.767 85.767 0 0 0-.027 1.792C-.001 17.16 0 17.869 0 18.578v22.235c0 .71 0 1.418.004 2.128.004.597.01 1.194.027 1.791.035 1.302.112 2.615.343 3.901.235 1.307.618 2.523 1.222 3.71a12.457 12.457 0 0 0 5.453 5.453c1.186.603 2.401.986 3.707 1.22 1.287.232 2.6.31 3.902.344.597.016 1.195.023 1.793.027.709.005 1.417.004 2.127.004h22.235c.709 0 1.418 0 2.128-.004.597-.004 1.194-.011 1.792-.027 1.302-.035 2.614-.112 3.902-.343 1.306-.235 2.521-.618 3.707-1.222a12.461 12.461 0 0 0 5.453-5.452c.604-1.187.987-2.403 1.222-3.71.231-1.286.308-2.6.343-3.9.016-.598.023-1.194.027-1.792.004-.71.004-1.419.004-2.129V18.578c0-.71 0-1.419-.004-2.128z"/><path fill="#FFF" d="m47.06 36.66-.004-.004c.066-.224.134-.446.191-.675 2.465-9.821-3.55-21.432-13.731-27.546 4.461 6.048 6.434 13.374 4.681 19.78-.156.571-.344 1.12-.552 1.653-.225-.148-.51-.316-.89-.527 0 0-10.127-6.252-21.103-17.312-.288-.29 5.852 8.777 12.822 16.14-3.284-1.843-12.434-8.5-18.227-13.802.712 1.187 1.558 2.33 2.489 3.43C17.573 23.932 23.882 31.5 31.44 37.314c-5.31 3.25-12.814 3.502-20.285.003a30.646 30.646 0 0 1-5.193-3.098c3.162 5.058 8.033 9.423 13.96 11.97 7.07 3.039 14.1 2.833 19.336.05l-.004.007c.024-.016.055-.032.08-.047.214-.116.428-.234.636-.358 2.516-1.306 7.485-2.63 10.152 2.559.654 1.27 2.041-5.46-3.061-11.74z"/><path id="logotype" d="M81.93 38.542c.465 4.12 4.394 6.822 9.852 6.822 5.185 0 8.924-2.701 8.924-6.44 0-3.22-2.265-5.185-7.478-6.495l-5.048-1.282c-7.26-1.801-10.534-5.077-10.534-10.48 0-6.658 5.813-11.27 14.082-11.27 8.022 0 13.726 4.639 13.917 11.325h-5.32c-.41-4.093-3.74-6.604-8.734-6.604-4.94 0-8.378 2.538-8.378 6.249 0 2.892 2.13 4.612 7.369 5.95l4.202 1.09c8.133 1.993 11.462 5.159 11.462 10.863 0 7.259-5.759 11.816-14.928 11.816-8.514 0-14.327-4.53-14.763-11.543h5.376zM140.049 49.43h-5.35l-6.249-21.776h-.109L122.12 49.43h-5.348l-7.914-28.518h5.184l5.513 22.896h.11l6.221-22.896h5.021l6.277 22.896h.11l5.512-22.896h5.13L140.05 49.43zM151.39 13.244c0-1.718 1.419-3.11 3.138-3.11 1.746 0 3.165 1.392 3.165 3.11 0 1.72-1.419 3.139-3.165 3.139a3.157 3.157 0 0 1-3.139-3.139zm.545 7.669h5.213V49.43h-5.213V20.913zM191.186 25.116v-4.204h-5.513v-6.821h-5.185v6.821h-9.964v-2.51c.027-2.538 1.01-3.603 3.357-3.603.764 0 1.528.083 2.156.192v-4.094a18.193 18.193 0 0 0-2.756-.218c-5.568 0-7.915 2.32-7.915 7.642v2.591h-3.983v4.204h3.983V49.43h5.185V25.116H180.488v16.838c0 5.512 2.101 7.64 7.559 7.64 1.174 0 2.51-.082 3.111-.218v-4.257c-.355.055-1.392.137-1.965.137-2.428 0-3.52-1.147-3.52-3.712V25.116h5.513z"/></svg>
</a>
</h1>
<nav role="navigation">
<ul class="navigation-links">
<li class="nav-item">
<span>
<a href="/getting-started/">Get Started</a>
</span>
</li>
<li class="nav-item">
<span>
<a href="/blog/">Blog</a>
</span>
</li>
<li class="nav-item">
<span>
<a href="/documentation/">Documentation</a>
</span>
</li>
<li class="nav-item">
<span>
<a href="/packages/">Packages</a>
</span>
</li>
<li class="nav-item">
<span>
<a href="/community/">Community</a>
<i>&#9663;</i>
</span>
<ul class="nav-submenu" role="menu">
<li role="presentation">
<a href="/community/" role="menuitem">Overview</a>
</li>
<li role="presentation">
<a href="/swift-evolution/" role="menuitem">Swift Evolution</a>
</li>
<li role="presentation">
<a href="/diversity/" role="menuitem">Diversity</a>
</li>
<li role="presentation">
<a href="/mentorship/" role="menuitem">Mentorship</a>
</li>
<li role="presentation">
<a href="/contributing/" role="menuitem">Contributing</a>
</li>
<li class="nav-section">Workgroups</li>
<li role="presentation">
<a href="/contributor-experience-workgroup/" role="menuitem">Contributor Experience</a>
</li>
<li role="presentation">
<a href="/sswg/" role="menuitem">Server</a>
</li>
<li role="presentation">
<a href="/website/" role="menuitem">Website</a>
</li>
<li role="presentation">
<a href="/language-steering-group/" role="menuitem">Language Steering Group</a>
</li>
<li role="presentation">
<a href="/cxx-interop-workgroup/" role="menuitem">C++ Interoperability</a>
</li>
<li role="presentation">
<a href="/documentation-workgroup/" role="menuitem">Documentation</a>
</li>
<li class="nav-section">Governance</li>
<li role="presentation">
<a href="/code-of-conduct/" role="menuitem">Code of Conduct</a>
</li>
<li role="presentation">
<a href="/legal/license.html" role="menuitem">License</a>
</li>
<li role="presentation">
<a href="/support/security.html" role="menuitem">Security</a>
</li>
</ul>
</li>
<li class="cta">
<a href="/install">Download</span></a>
</li>
</ul>
<button id="menu-toggle" class="menu-item menu-toggle open" aria-expanded="false" aria-label="Toggle Navigation Menu"></button>
</nav>
</div>
<nav class="mobile-navigation" role="navigation">
<ul class="mobile-navigation-links">
<li class="nav-item">
<div class="link-container">
<a href="/getting-started/">Get Started</a>
</div>
</li>
<li class="nav-item">
<div class="link-container">
<a href="/blog/">Blog</a>
</div>
</li>
<li class="nav-item">
<div class="link-container">
<a href="/documentation/">Documentation</a>
</div>
</li>
<li class="nav-item">
<div class="link-container">
<a href="/packages/">Packages</a>
</div>
</li>
<li class="nav-item">
<div class="link-container">
<a href="/community/">Community</a>
<button class="section-toggle" aria-expanded="false" aria-label="Toggle Community Section">
&#9663;
</button>
</div>
<ul class="section-menu">
<li>
<a href="/community/">Overview</a>
</li>
<li>
<a href="/swift-evolution/">Swift Evolution</a>
</li>
<li>
<a href="/diversity/">Diversity</a>
</li>
<li>
<a href="/mentorship/">Mentorship</a>
</li>
<li>
<a href="/contributing/">Contributing</a>
</li>
<li class="nav-section">Workgroups</li>
<li>
<a href="/contributor-experience-workgroup/">Contributor Experience</a>
</li>
<li>
<a href="/sswg/">Server</a>
</li>
<li>
<a href="/website/">Website</a>
</li>
<li>
<a href="/language-steering-group/">Language Steering Group</a>
</li>
<li>
<a href="/cxx-interop-workgroup/">C++ Interoperability</a>
</li>
<li>
<a href="/documentation-workgroup/">Documentation</a>
</li>
<li class="nav-section">Governance</li>
<li>
<a href="/code-of-conduct/">Code of Conduct</a>
</li>
<li>
<a href="/legal/license.html">License</a>
</li>
<li>
<a href="/support/security.html">Security</a>
</li>
</ul>
</li>
<li class="cta">
<a href="/install">Download <span>5.9.2</span></a>
</li>
</ul>
</nav>
</header>
<main role="main">
<article class="post">
<header>
<h1>Swift Summer of Code 2023 Summary</h1>
<time pubdate datetime="2024-02-13T06:00:00-04:00">February 13, 2024</time>
<div class="authors">
<div class="byline">
<img src="https://www.gravatar.com/avatar/03cb20b97f6a14701c24c4e088b6af87?s=64&d=mp" alt="Konrad ktoso Malawski"/>
<span class="author">
<a href="https://github.com/ktoso/" rel="nofollow" title="Konrad ktoso Malawski (@ktoso) on GitHub">Konrad ktoso Malawski</a>
</span>
</div>
<div class="about">Konrad Malawski is a member of a team developing foundational server-side Swift libraries at Apple, with focus on distributed systems and concurrency.</div>
<div class="byline">
<img src="https://www.gravatar.com/avatar/3b34dbe9f3870b0e9bf1f4cb0750fa3d?s=64&d=mp" alt="Franz Busch"/>
<span class="author">
<a href="https://github.com/FranzBusch/" rel="nofollow" title="Franz Busch (@FranzBusch) on GitHub">Franz Busch</a>
</span>
</div>
<div class="about">Franz Busch is a member of a team developing foundational server-side Swift libraries at Apple, and is a member of the SSWG.</div>
<div class="byline">
<img src="https://www.github.com/ahoppen.png?size=64" alt="Alex Hoppen"/>
<span class="author">
<a href="https://github.com/ahoppen/" rel="nofollow" title="Alex Hoppen (@ahoppen) on GitHub">Alex Hoppen</a>
</span>
</div>
<div class="about">Alex Hoppen works on the Swift team at Apple, focusing on parsing-related technologies like SourceKit, swift-syntax and code completion.</div>
<div class="byline">
<img src="https://www.github.com/xedin.png?size=64" alt="Pavel Yaskevich"/>
<span class="author">
<a href="https://github.com/xedin/" rel="nofollow" title="Pavel Yaskevich (@xedin) on GitHub">Pavel Yaskevich</a>
</span>
</div>
<div class="about">Pavel Yaskevich works on the Swift team at Apple, focusing on semantic analysis.</div>
</div>
</header>
<p>The Swift project regularly participates in <a href="https://summerofcode.withgoogle.com">Google Summer of Code</a> in order to help people new to the open source ecosystem dip their toes in contributing to Swift and its growing ecosystem.</p>
<p>During the 2023 edition of the program, we ran three projects, all of which completed their assigned projects successfully.</p>
<p>The projects in this edition were:</p>
<ul>
<li>Swift Memcache Library</li>
<li>Incremental Parsing in SwiftParser</li>
<li>Key Path Inference and Diagnostic Improvements</li>
</ul>
<p>Wed like to extend our sincere thanks to the participants and mentors who poured their time and passion into these projects, and use this post to highlight their work to the wider community. Below, each project is described in a small summary.</p>
<p>Lets take a look at each project, in the words of the mentees and mentors themselves:</p>
<h3 id="swift-memcache">Swift Memcache</h3>
<ul>
<li>Mentee: <a href="https://github.com/dkz2">Delkhaz Ibrahimi</a></li>
<li>Mentor: <a href="https://github.com/FranzBusch">Franz Busch</a></li>
</ul>
<p>The goal of the project was to develop a native <a href="https://memcached.org">Memcached</a> connection abstraction for the Swift on Server ecosystem. This connection was implemented using <code class="language-plaintext highlighter-rouge">SwiftNIO,</code> offers native Swift Concurrency APIs and integrates well with the rest of the server ecosystem. The benefit of using Swift Concurrency for building such client is that it can make use of <a href="https://developer.apple.com/videos/play/wwdc2023/10170/">structured concurrency</a> which brings cancellation, executor awareness, and simple integration with <a href="https://github.com/apple/swift-distributed-tracing">distributed tracing</a> in the future.</p>
<p>The focus during the project was implementing the Memcache <a href="https://github.com/memcached/memcached/wiki/MetaCommands">meta command protocol</a> and offering basic <code class="language-plaintext highlighter-rouge">get</code> and <code class="language-plaintext highlighter-rouge">set</code> functionalities.</p>
<p>Below is a short example how the new <code class="language-plaintext highlighter-rouge">MemcacheConnection</code> type can be used to <code class="language-plaintext highlighter-rouge">set</code> and <code class="language-plaintext highlighter-rouge">get</code> a value for a given key:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Instantiate a new MemcacheConnection with host, port, and event loop group</span>
<span class="k">let</span> <span class="nv">memcacheConnection</span> <span class="o">=</span> <span class="kt">MemcacheConnection</span><span class="p">(</span><span class="nv">host</span><span class="p">:</span> <span class="s">"127.0.0.1"</span><span class="p">,</span> <span class="nv">port</span><span class="p">:</span> <span class="mi">11211</span><span class="p">,</span> <span class="nv">eventLoopGroup</span><span class="p">:</span> <span class="o">.</span><span class="n">singleton</span><span class="p">)</span>
<span class="c1">// Initialize the service group</span>
<span class="k">let</span> <span class="nv">serviceGroup</span> <span class="o">=</span> <span class="kt">ServiceGroup</span><span class="p">(</span><span class="nv">services</span><span class="p">:</span> <span class="p">[</span><span class="n">memcacheConnection</span><span class="p">],</span> <span class="nv">logger</span><span class="p">:</span> <span class="n">logger</span><span class="p">)</span>
<span class="k">try</span> <span class="k">await</span> <span class="nf">withThrowingTaskGroup</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="kt">Void</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span> <span class="n">group</span> <span class="k">in</span>
<span class="n">group</span><span class="o">.</span><span class="n">addTask</span> <span class="p">{</span> <span class="k">try</span> <span class="k">await</span> <span class="n">serviceGroup</span><span class="o">.</span><span class="nf">run</span><span class="p">()</span> <span class="p">}</span>
<span class="c1">// Set a value for a key.</span>
<span class="k">let</span> <span class="nv">setValue</span> <span class="o">=</span> <span class="s">"bar"</span>
<span class="k">try</span> <span class="k">await</span> <span class="n">memcacheConnection</span><span class="o">.</span><span class="nf">set</span><span class="p">(</span><span class="s">"foo"</span><span class="p">,</span> <span class="nv">value</span><span class="p">:</span> <span class="n">setValue</span><span class="p">)</span>
<span class="c1">// Get the value for a key.</span>
<span class="c1">// Specify the expected type for the value returned from Memcache.</span>
<span class="k">let</span> <span class="nv">getValue</span> <span class="o">=</span> <span class="k">try</span> <span class="k">await</span> <span class="n">memcacheConnection</span><span class="o">.</span><span class="nf">get</span><span class="p">(</span><span class="s">"foo"</span><span class="p">,</span> <span class="nv">as</span><span class="p">:</span> <span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>After finishing the foundational work to implement the <code class="language-plaintext highlighter-rouge">get</code> and <code class="language-plaintext highlighter-rouge">set</code> commands, support for <code class="language-plaintext highlighter-rouge">delete</code>, <code class="language-plaintext highlighter-rouge">append</code>, <code class="language-plaintext highlighter-rouge">prepend</code>, <code class="language-plaintext highlighter-rouge">increment</code> and <code class="language-plaintext highlighter-rouge">decrement</code> was added. Lastly, support for checking and updating the time-to-live for keys was added.</p>
<p>The new <code class="language-plaintext highlighter-rouge">MemcacheConnection</code> type lays the ground work to implement a higher-level <code class="language-plaintext highlighter-rouge">MemcacheClient</code> that offers additional functionality such as connection pooling, retries, key distribution across nodes and more. However, implementing such a client was out of scope for this years GSoC project.</p>
<p>If youd like to learn more about this project and Delkhazs experience, see <a href="https://forums.swift.org/t/gsoc-2023-swift-memcache-gsoc-project-kickoff/64932">this thread</a> on the Swift forums.</p>
<h3 id="implement-incremental-re-parsing-in-swiftparser">Implement Incremental Re-Parsing in SwiftParser</h3>
<ul>
<li>Mentee: <a href="https://github.com/StevenWong12">Ziyang Huang</a></li>
<li>Mentor: <a href="https://github.com/ahoppen">Alex Hoppen</a></li>
</ul>
<p>This project aimed to improve the performance of the <a href="https://github.com/apple/swift-syntax/tree/main/Sources/SwiftParser">SwiftParser</a> for scenarios like editor syntax highlighting, where minor edits are applied to the file. By adding incremental parsing and re-using parts of the syntax tree that remain unchanged, large performance improvements can be made.</p>
<p>The most challenging part of this project was to make sure we parse the source files correctly. Considering the code snippet below:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">foo</span><span class="p">()</span> <span class="p">{}</span>
<span class="nv">someLabel</span><span class="p">:</span> <span class="k">switch</span> <span class="n">x</span> <span class="p">{</span>
<span class="k">default</span><span class="p">:</span> <span class="k">break</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This source is parsed as a <code class="language-plaintext highlighter-rouge">FunctionCallExprSyntax</code> and a <code class="language-plaintext highlighter-rouge">LabeledStmtSyntax</code>. When we remove the “<code class="language-plaintext highlighter-rouge">switch x</code>” part of this code, a naïve implementation could reuse <code class="language-plaintext highlighter-rouge">foo() {}</code> as a function call since the edit didnt touch it. But this is not correct since the <code class="language-plaintext highlighter-rouge">someLabel</code> block now became a labeled trailing closure of <code class="language-plaintext highlighter-rouge">foo() {}</code>.</p>
<p>To solve this problem, we collect some additional information for every syntax node during the initial parse to mark the potentially affected range for each node. That information is used to correctly re-parse the <code class="language-plaintext highlighter-rouge">foo()</code> function call and include <code class="language-plaintext highlighter-rouge">someLabel</code> as a labeled trailing closure in the call.</p>
<p>The implementation speeds up parsing by about <strong>10x</strong> when we parse source incrementally while <strong>only incurring a 2~3% of performance loss</strong> during normal parsing.</p>
<p>If youd like to read more about this project and Ziyangs experience in his own words, head over to <a href="https://forums.swift.org/t/gsoc-2023-my-gsoc-experience/67340">his GSoC experience post</a> on the Swift forums.</p>
<h3 id="key-path-inference-and-diagnostic-improvements">Key Path Inference and Diagnostic Improvements</h3>
<ul>
<li>Mentee: <a href="https://github.com/amritpan">Amritpan Kaur</a></li>
<li>Mentor: <a href="https://github.com/xedin?tab=repositories">Pavel Yaskevich</a></li>
</ul>
<p>This project was focused on performance and diagnostic improvements to type-checking of key path literal expressions as well as improvements to new features such as keypaths-as-functions introduced to the language by <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0249-key-path-literal-function-expressions.md">SE-0249</a>.</p>
<p>During compilation, the key path expression root and value were type-checked sequentially to resolve a key path type from this context. However, the design of the type-checkers evaluation of key path component types, their relationships to each other, and key path capabilities results in hard to understand compiler errors and even failures to type-check some valid Swift code.</p>
<p>Some of the problems with this approach can be illustrated by the following code example:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">User</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">Name</span>
<span class="p">}</span>
<span class="kd">struct</span> <span class="kt">Name</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">firstName</span><span class="p">:</span> <span class="kt">String</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">test</span><span class="p">(</span><span class="nv">_</span><span class="p">:</span> <span class="kt">WritableKeyPath</span><span class="o">&lt;</span><span class="kt">User</span><span class="p">,</span> <span class="kt">String</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{}</span>
<span class="nf">test</span><span class="p">(\</span><span class="o">.</span><span class="n">name</span><span class="o">.</span><span class="n">firstName</span><span class="p">)</span>
</code></pre></div></div>
<p>The compiler produces the following error:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">error</span><span class="p">:</span> <span class="n">key</span> <span class="n">path</span> <span class="n">value</span> <span class="n">type</span> <span class="err">'</span><span class="kt">WritableKeyPath</span><span class="o">&lt;</span><span class="kt">User</span><span class="p">,</span> <span class="kt">String</span><span class="o">&gt;</span><span class="err">'</span> <span class="n">cannot</span> <span class="n">be</span> <span class="n">converted</span> <span class="n">to</span> <span class="n">contextual</span> <span class="n">type</span> <span class="err">'</span><span class="kt">KeyPath</span><span class="o">&lt;</span><span class="kt">User</span><span class="p">,</span> <span class="kt">String</span><span class="o">&gt;</span><span class="err">'</span>
<span class="nf">test</span><span class="p">(\</span><span class="o">.</span><span class="n">name</span><span class="o">.</span><span class="n">firstName</span><span class="p">)</span>
<span class="o">^</span>
</code></pre></div></div>
<p>There are multiple issues with this error diagnostic: contextual type is actually <code class="language-plaintext highlighter-rouge">WritableKeyPath</code> and key path should be inferred as read-only, the source information is lost and the compiler is unenable to point out a problem with an argument to a call to the function <code class="language-plaintext highlighter-rouge">test</code>.</p>
<p>To address these and other issues, we explored a different design for key path literal expression type-checking: infer the root type of the key path first and propagate that information to the components and infer capability based on the components before setting the type for the key path expression. This made it a lot easier to diagnose contextual type failures and support previously failing conversions. This approach improves the performance as well because the literal is resolved only if the context expects a key path and root type is either provided by the developer explicitly or is able to be inferred from the context.</p>
<p>With the new approach implemented the compiler now produces the following diagnostic:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">error</span><span class="p">:</span> <span class="n">cannot</span> <span class="n">convert</span> <span class="n">value</span> <span class="n">of</span> <span class="n">type</span> <span class="err">'</span><span class="kt">KeyPath</span><span class="o">&lt;</span><span class="kt">User</span><span class="p">,</span> <span class="kt">String</span><span class="o">&gt;</span><span class="err">'</span> <span class="n">to</span> <span class="n">expected</span> <span class="n">argument</span> <span class="n">type</span> <span class="err">'</span><span class="kt">WritableKeyPath</span><span class="o">&lt;</span><span class="kt">User</span><span class="p">,</span> <span class="kt">String</span><span class="o">&gt;</span><span class="err">'</span>
<span class="nf">test</span><span class="p">(\</span><span class="o">.</span><span class="n">name</span><span class="o">.</span><span class="n">firstName</span><span class="p">)</span>
<span class="o">^</span>
</code></pre></div></div>
<p>If youd like to learn more about the details of this project, we recommend having a look at this excellent and <a href="https://forums.swift.org/t/key-path-inference-and-diagnostic-improvements-an-update/69632">very detailed writeup written by Amritpan</a> on the forums.</p>
<h2 id="mentee-impressions">Mentee Impressions</h2>
<p>Its great to see what projects our GSoC participants accomplished this year! Even better though is seeing participants enjoy their time working with the Swift project, as a big part of Summer of Code is giving mentees (and mentors) a space to grow and learn about open source development.</p>
<p>Here are a few impressions of the mentees about their experience:</p>
<p><strong>Delkhaz (Swift Memcache):</strong></p>
<blockquote>
<p>As we wrap up this technical update, its a bittersweet moment for me to share that my formal journey as a Google Summer of Code student has reached its conclusion, but what an incredible journey its been. This transformative experience has enriched my understanding of open source development and honed my skills as a developer. I want to extend a heartfelt thank you to everyone who has been a part of this adventure with me.</p>
<p>While this chapter may be closing, Im thrilled to announce that I will remain actively involved in the project. Im particularly excited about contributing toward taking this project to its v1 or production stage. So, this isnt a goodbye; its just the beginning of a new chapter. Looking forward to staying in touch and continuing this journey with all of you.</p>
<p>Special thanks are in order for my mentor Franz, for his exceptional mentorship during my time in the program. His wisdom and insights in our weekly sessions have been nothing short of transformative for me as a developer. Whether it was navigating the complexities of open-source contributions or striving for the highest standards in our API development, Franzs guidance has been indispensable. Ive gained a multifaceted understanding of software development thanks to his continued support. I couldnt have asked for a more impactful mentorship experience, and for that, I am deeply grateful.</p>
</blockquote>
<p><strong>Ziyang (Implement Incremental Re-Parsing in SwiftParser):</strong></p>
<blockquote>
<p>After measurement, our implementation speed up parsing about 10 times when we parse source incrementally while only bring 2~3% of performance loss to normal parsing. 🎉 🎉</p>
<p>I also bring this feature to sourcekit-lsp and swift-stress-tester, it is really exciting to see my work can be actually put into use.</p>
<p>Special thanks to my mentor Alex for the quick response, detailed reviews and inspiring ideas.</p>
</blockquote>
<p><strong>Amritpan (Key Path inference and diagnostic improvements):</strong></p>
<blockquote>
<p>I enjoyed working on this project this year, primarily because it was a challenge that allowed me to deepen my understanding of the solver implementation, utilize improvements I made last year to the debug output, and make more impactful changes to the compiler codebase.</p>
<p>Refactoring the debug output last year helped me understand how the various pieces of information that the type checker collected were then evaluated to assign types from context. Taking a look at key path expression type checking failures revealed some of the fallibilities of the constraint system and solver and how various design decision choices could solve certain issues while causing others.</p>
</blockquote>
<p>We hope this writeup inspires you to either apply to work with us or become a mentor on the Swift project in a future!</p>
<p>We are always looking for ideas, mentors, and general input about how we can make Swifts ecosystem more inclusive and easier to participate in. If you have some ideas, would like to become a mentor in future editions, or if youre just curious about other project ideas that were floated during previous editions, visit the <a href="https://forums.swift.org/c/development/gsoc/98">dedicated GSoC category</a> on the Swift forums. You can also check out <a href="https://www.swift.org/blog/swift-summer-of-code-2022-summary/">last years GSoC summary blog post</a>, where we highlighted last years projects.</p>
<p>If youre interested in participating as a mentee, keep an eye on the official <a href="https://summerofcode.withgoogle.com/">GSoC schedule</a> and on the Swift forums <a href="https://forums.swift.org/c/development/gsoc/98">GSoC category</a>.</p>
<footer>
<nav>
<a href="/blog/swift-openapi-generator-1.0/" rel="prev" title="Previous: Swift OpenAPI Generator 1.0 Released">Swift OpenAPI Generator 1.0 Released</a>
<a href="/blog/mlx-swift/" rel="next" title="Next: On-device ML research with MLX and Swift">On-device ML research with MLX and Swift</a>
</nav>
</footer>
</article>
</main>
<footer role="contentinfo">
<div class="footer-content">
<p>Except where otherwise noted, all content on this blog is licensed under a <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 International license</a>.</p>
<p class="copyright">Copyright © 2024 Apple Inc. All rights reserved.</p>
<p class="trademark">Swift and the Swift logo are trademarks of Apple Inc.</p>
<p class="privacy">
<a href="//www.apple.com/privacy/privacy-policy/">Privacy Policy</a>
<a href="//www.apple.com/legal/privacy/en-ww/cookies/">Cookies</a>
</p>
</div>
<div class="footer-other">
<form
class="color-scheme-toggle"
role="radiogroup"
tabindex="0"
id="color-scheme-toggle"
>
<legend class="visuallyhidden">Color scheme preference</legend>
<label for="scheme-light">
<input id="scheme-light" type="radio" name="color-scheme-preference" value="light">
<span class="color-scheme-toggle-label">Light</span>
</label>
<label for="scheme-dark">
<input id="scheme-dark" type="radio" name="color-scheme-preference" value="dark">
<span class="color-scheme-toggle-label">Dark</span>
</label>
<label for="scheme-auto" id="scheme-auto-wrapper">
<input id="scheme-auto" type="radio" name="color-scheme-preference" value="auto">
<span class="color-scheme-toggle-label">Auto</span>
</label>
</form>
<aside>
<a href="https://twitter.com/swiftlang" rel="nofollow" title="Follow @SwiftLang on Twitter"><i class="twitter"></i></a>
<a href="/atom.xml" title="Subscribe to Site Updates"><i class="feed"></i></a>
</aside>
</div>
</footer>
<script src="/assets/javascripts/application.js"></script>
<!-- metrics -->
<script>
/* RSID: */
var s_account="awdswiftorg"
</script>
<script src="https://developer.apple.com/assets/metrics/scripts/analytics.js"></script>
<script>
s.pageName= AC && AC.Tracking && AC.Tracking.pageName();
/************* DO NOT ALTER ANYTHING BELOW THIS LINE ! **************/
var s_code=s.t();if(s_code)document.write(s_code)
</script>
<!-- /metrics -->
</body>
</html>

View file

@ -0,0 +1,66 @@
//
// HTMLToolsPerformanceTests.swift
//
//
// Created by Duong Thai on 03/03/2024.
//
import XCTest
@testable import RSS
import SwiftSoup
final class HTMLToolsPerformanceTests: XCTestCase {
func test_Performance_SwiftSoup() {
let fileName = "iso-500px-com--10-chilly-new-photos-from-500px-licensing.html"
let content = Self.getStringFrom(fileName: fileName)
measure {
_ = try! SwiftSoup.parse(content)
}
}
func test_Performance_Regex() {
let fileName = "iso-500px-com--10-chilly-new-photos-from-500px-licensing.html"
let sourceURL = URL(string: "https://iso.500px.com/10-chilly-new-photos-from-500px-licensing/")!
let content = Self.getStringFrom(fileName: fileName)
measure {
_ = HTMLTools.getTitleOf(html: content)!.string
_ = HTMLTools.getContentTypeOf(html: content)!.string
_ = HTMLTools.getPreviewImageOf(html: content)!
_ = HTMLTools.getURLOf(html: content)!
_ = HTMLTools.getFaviconOf(html: content, sourceURL: sourceURL)!
_ = HTMLTools.getSiteNameOf(html: content)!.string
}
}
func test_Performance_Convert_HTML_To_NSAttributedString_Without_Media() {
let fileName = "iso-500px-com--10-chilly-new-photos-from-500px-licensing.html"
let content = Self.getStringFrom(fileName: fileName)
measure {
_ = HTMLTools.convert(content, baseURL: nil)
}
}
func test_Performance_Convert_HTML_To_NSAttributedString_With_Media() {
let fileName = "iso-500px-com--10-chilly-new-photos-from-500px-licensing.html"
let content = Self.getStringFrom(fileName: fileName)
measure {
_ = HTMLTools.convert(content, baseURL: nil, withMedia: true)
}
}
static private func getStringFrom(fileName: String) -> String {
/*
can be broken if moving related files
*/
let filePath = URL(string: #filePath)!
.deletingLastPathComponent()
.appendingPathComponent("HTMLFiles/\(fileName)")
.absoluteString
return try! String(contentsOfFile: filePath)
}
}

File diff suppressed because it is too large Load diff