Skip to content

Commit 04f5495

Browse files
wpmobilebotkeancrazytonyli
authored
Merge release/26.5 into trunk (#25037)
* Fix regex for apple-touch-icon (#25025) * Merge release_notes/26.5 into release/26.5 (#25036) * Update Jetpack release notes * Update metadata strings --------- Co-authored-by: Tony Li <[email protected]> * Update app translations – `Localizable.strings` * Update Jetpack metadata translations * Bump version number --------- Co-authored-by: Alex Grebenyuk <[email protected]> Co-authored-by: Tony Li <[email protected]>
1 parent 6a50a4d commit 04f5495

File tree

27 files changed

+940
-86
lines changed

27 files changed

+940
-86
lines changed

Modules/Sources/AsyncImageKit/Helpers/FaviconService.swift

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public actor FaviconService {
3232
let task = tasks[siteURL] ?? FaviconTask { [session] in
3333
let (data, response) = try await session.data(from: siteURL)
3434
try validate(response: response)
35-
return await makeFavicon(from: data, siteURL: siteURL)
35+
return FaviconService.makeFavicon(from: data, siteURL: siteURL)
3636
}
3737
let subscriptionID = UUID()
3838
task.subscriptions.insert(subscriptionID)
@@ -55,6 +55,20 @@ public actor FaviconService {
5555
task.task.cancel()
5656
tasks[key] = nil
5757
}
58+
59+
/// Parses HTML data to extract the apple-touch-icon URL or falls back to /favicon.icon
60+
public static func makeFavicon(from data: Data, siteURL: URL) -> URL {
61+
let html = String(data: data, encoding: .utf8) ?? ""
62+
let range = NSRange(location: 0, length: html.utf16.count)
63+
if let match = regex?.firstMatch(in: html, options: [], range: range),
64+
let matchRange = Range(match.range(at: 1), in: html),
65+
let faviconURL = URL(string: String(html[matchRange]), relativeTo: siteURL) {
66+
return faviconURL
67+
}
68+
// Fallback to standard favicon path. It has low quality, but
69+
// it's better than nothing.
70+
return siteURL.appendingPathComponent("favicon.icon")
71+
}
5872
}
5973

6074
public enum FaviconError: Error, Sendable {
@@ -74,23 +88,10 @@ private final class FaviconCache: @unchecked Sendable {
7488
}
7589

7690
private let regex: NSRegularExpression? = {
77-
let pattern = "<link[^>]*rel=\"apple-touch-icon(?:-precomposed)\"[^>]*href=\"([^\"]+)\"[^>]*>"
91+
let pattern = "<link[^>]*rel=\"apple-touch-icon(?:-precomposed)?\"[^>]*href=\"([^\"]+)\"[^>]*>"
7892
return try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
7993
}()
8094

81-
private func makeFavicon(from data: Data, siteURL: URL) async -> URL {
82-
let html = String(data: data, encoding: .utf8) ?? ""
83-
let range = NSRange(location: 0, length: html.utf16.count)
84-
if let match = regex?.firstMatch(in: html, options: [], range: range),
85-
let matchRange = Range(match.range(at: 1), in: html),
86-
let faviconURL = URL(string: String(html[matchRange]), relativeTo: siteURL) {
87-
return faviconURL
88-
}
89-
// Fallback to standard favicon path. It has low quality, but
90-
// it's better than nothing.
91-
return siteURL.appendingPathComponent("favicon.icon")
92-
}
93-
9495
private func validate(response: URLResponse) throws {
9596
guard let response = response as? HTTPURLResponse else {
9697
return
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import UIKit
2+
import Testing
3+
import AsyncImageKit
4+
5+
@Suite final class FaviconServiceTests {
6+
@Test func appleTouchIcon() throws {
7+
// GIVEN
8+
let siteURL = try #require(URL(string: "https://example.com"))
9+
let html = """
10+
<html>
11+
<head>
12+
<link rel="apple-touch-icon" href="/apple-icon.png">
13+
</head>
14+
</html>
15+
"""
16+
let data = Data(html.utf8)
17+
18+
// WHEN
19+
let faviconURL = FaviconService.makeFavicon(from: data, siteURL: siteURL)
20+
21+
// THEN
22+
#expect(faviconURL.absoluteString == "https://example.com/apple-icon.png")
23+
}
24+
25+
@Test func appleTouchIconPrecomposed() throws {
26+
// GIVEN
27+
let siteURL = try #require(URL(string: "https://example.com"))
28+
let html = """
29+
<html>
30+
<head>
31+
<link rel="apple-touch-icon-precomposed" href="/apple-icon-precomposed.png">
32+
</head>
33+
</html>
34+
"""
35+
let data = Data(html.utf8)
36+
37+
// WHEN
38+
let faviconURL = FaviconService.makeFavicon(from: data, siteURL: siteURL)
39+
40+
// THEN
41+
#expect(faviconURL.absoluteString == "https://example.com/apple-icon-precomposed.png")
42+
}
43+
44+
@Test func appleTouchIconWithAbsoluteURL() throws {
45+
// GIVEN
46+
let siteURL = try #require(URL(string: "https://example.com"))
47+
let html = """
48+
<html>
49+
<head>
50+
<link rel="apple-touch-icon" href="https://cdn.example.com/icon.png">
51+
</head>
52+
</html>
53+
"""
54+
let data = Data(html.utf8)
55+
56+
// WHEN
57+
let faviconURL = FaviconService.makeFavicon(from: data, siteURL: siteURL)
58+
59+
// THEN
60+
#expect(faviconURL.absoluteString == "https://cdn.example.com/icon.png")
61+
}
62+
63+
@Test func appleTouchIconWithRelativePath() throws {
64+
// GIVEN
65+
let siteURL = try #require(URL(string: "https://example.com"))
66+
let html = """
67+
<html>
68+
<head>
69+
<link rel="apple-touch-icon" href="assets/icon.png">
70+
</head>
71+
</html>
72+
"""
73+
let data = Data(html.utf8)
74+
75+
// WHEN
76+
let faviconURL = FaviconService.makeFavicon(from: data, siteURL: siteURL)
77+
78+
// THEN
79+
#expect(faviconURL.absoluteString == "https://example.com/assets/icon.png")
80+
}
81+
82+
@Test func appleTouchIconWithAdditionalAttributes() throws {
83+
// GIVEN
84+
let siteURL = try #require(URL(string: "https://example.com"))
85+
let html = """
86+
<html>
87+
<head>
88+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png">
89+
</head>
90+
</html>
91+
"""
92+
let data = Data(html.utf8)
93+
94+
// WHEN
95+
let faviconURL = FaviconService.makeFavicon(from: data, siteURL: siteURL)
96+
97+
// THEN
98+
#expect(faviconURL.absoluteString == "https://example.com/apple-icon-180x180.png")
99+
}
100+
101+
@Test func appleTouchIconPrecomposedWithSizes() throws {
102+
// GIVEN
103+
let siteURL = try #require(URL(string: "https://example.com"))
104+
let html = """
105+
<html>
106+
<head>
107+
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="/apple-icon-precomposed-152.png">
108+
</head>
109+
</html>
110+
"""
111+
let data = Data(html.utf8)
112+
113+
// WHEN
114+
let faviconURL = FaviconService.makeFavicon(from: data, siteURL: siteURL)
115+
116+
// THEN
117+
#expect(faviconURL.absoluteString == "https://example.com/apple-icon-precomposed-152.png")
118+
}
119+
120+
@Test func fallbackToFaviconIcon() throws {
121+
// GIVEN
122+
let siteURL = try #require(URL(string: "https://example.com"))
123+
let html = """
124+
<html>
125+
<head>
126+
<link rel="icon" href="/favicon.ico">
127+
</head>
128+
</html>
129+
"""
130+
let data = Data(html.utf8)
131+
132+
// WHEN
133+
let faviconURL = FaviconService.makeFavicon(from: data, siteURL: siteURL)
134+
135+
// THEN
136+
#expect(faviconURL.absoluteString == "https://example.com/favicon.icon")
137+
}
138+
139+
@Test func fallbackWhenNoFaviconFound() throws {
140+
// GIVEN
141+
let siteURL = try #require(URL(string: "https://example.com"))
142+
let html = "<html><head></head></html>"
143+
let data = Data(html.utf8)
144+
145+
// WHEN
146+
let faviconURL = FaviconService.makeFavicon(from: data, siteURL: siteURL)
147+
148+
// THEN
149+
#expect(faviconURL.absoluteString == "https://example.com/favicon.icon")
150+
}
151+
152+
@Test func appleTouchIconCaseInsensitive() throws {
153+
// GIVEN
154+
let siteURL = try #require(URL(string: "https://example.com"))
155+
let html = """
156+
<html>
157+
<head>
158+
<link rel="APPLE-TOUCH-ICON" href="/apple-icon.png">
159+
</head>
160+
</html>
161+
"""
162+
let data = Data(html.utf8)
163+
164+
// WHEN
165+
let faviconURL = FaviconService.makeFavicon(from: data, siteURL: siteURL)
166+
167+
// THEN
168+
#expect(faviconURL.absoluteString == "https://example.com/apple-icon.png")
169+
}
170+
171+
@Test func multipleAppleTouchIconsUsesFirst() throws {
172+
// GIVEN
173+
let siteURL = try #require(URL(string: "https://example.com"))
174+
let html = """
175+
<html>
176+
<head>
177+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180.png">
178+
<link rel="apple-touch-icon" sizes="152x152" href="/apple-icon-152.png">
179+
<link rel="apple-touch-icon-precomposed" href="/apple-icon-precomposed.png">
180+
</head>
181+
</html>
182+
"""
183+
let data = Data(html.utf8)
184+
185+
// WHEN
186+
let faviconURL = FaviconService.makeFavicon(from: data, siteURL: siteURL)
187+
188+
// THEN - Uses the first match
189+
#expect(faviconURL.absoluteString == "https://example.com/apple-icon-180.png")
190+
}
191+
192+
@Test func emptyData() throws {
193+
// GIVEN
194+
let siteURL = try #require(URL(string: "https://example.com"))
195+
let data = Data()
196+
197+
// WHEN
198+
let faviconURL = FaviconService.makeFavicon(from: data, siteURL: siteURL)
199+
200+
// THEN - Falls back to standard favicon path
201+
#expect(faviconURL.absoluteString == "https://example.com/favicon.icon")
202+
}
203+
204+
@Test func siteURLWithPath() throws {
205+
// GIVEN
206+
let siteURL = try #require(URL(string: "https://example.com/blog"))
207+
let html = """
208+
<html>
209+
<head>
210+
<link rel="apple-touch-icon" href="/apple-icon.png">
211+
</head>
212+
</html>
213+
"""
214+
let data = Data(html.utf8)
215+
216+
// WHEN
217+
let faviconURL = FaviconService.makeFavicon(from: data, siteURL: siteURL)
218+
219+
// THEN
220+
#expect(faviconURL.absoluteString == "https://example.com/apple-icon.png")
221+
}
222+
223+
@Test func siteURLWithPathFallback() throws {
224+
// GIVEN
225+
let siteURL = try #require(URL(string: "https://example.com/blog"))
226+
let html = "<html><head></head></html>"
227+
let data = Data(html.utf8)
228+
229+
// WHEN
230+
let faviconURL = FaviconService.makeFavicon(from: data, siteURL: siteURL)
231+
232+
// THEN
233+
#expect(faviconURL.absoluteString == "https://example.com/blog/favicon.icon")
234+
}
235+
}

WordPress/Jetpack/Resources/AppStoreStrings.po

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,16 @@ msgid ""
9595
"\n"
9696
"- On the “Publishing” screen, you can toggle “Email to Subscribers” to automatically notify your subscribers when a post is published. Start the presses.\n"
9797
"- The slug editor has a permalink preview to show what a post’s final URL will look like.\n"
98-
"- We added “Taxonomies” to Site Settings so you can manage your content groups at the site level.\n"
98+
"- We added “Taxonomies” to Site Settings so you can manage content groups at the site level.\n"
9999
"- Thanks to some design fixes in the “Categories” picker, it’s more obvious that you can pick multiple categories for a post.\n"
100100
"- File sizes are now visible in the Site Media Details area. No more guesswork.\n"
101+
"\n"
102+
"More of a reader than a publisher? Don’t worry, we made changes in the Reader, too.\n"
103+
"\n"
104+
"- When related post titles are overly long, you’ll see a shorter, truncated version instead.\n"
105+
"- Post tiles can now have three lines of text instead of just two, giving you 50% more content goodness.\n"
106+
"- Articles have slightly wider horizontal margins.\n"
107+
"- We squashed several bugs that affected the way articles load.\n"
101108
msgstr ""
102109

103110
#. translators: This is a promo message that will be attached on top of the first screenshot in the App Store.

WordPress/Jetpack/Resources/release_notes.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ While we were at it, we made several other helpful updates in various areas of t
1010

1111
- On the “Publishing” screen, you can toggle “Email to Subscribers” to automatically notify your subscribers when a post is published. Start the presses.
1212
- The slug editor has a permalink preview to show what a post’s final URL will look like.
13-
- We added “Taxonomies” to Site Settings so you can manage your content groups at the site level.
13+
- We added “Taxonomies” to Site Settings so you can manage content groups at the site level.
1414
- Thanks to some design fixes in the “Categories” picker, it’s more obvious that you can pick multiple categories for a post.
1515
- File sizes are now visible in the Site Media Details area. No more guesswork.
16+
17+
More of a reader than a publisher? Don’t worry, we made changes in the Reader, too.
18+
19+
- When related post titles are overly long, you’ll see a shorter, truncated version instead.
20+
- Post tiles can now have three lines of text instead of just two, giving you 50% more content goodness.
21+
- Articles have slightly wider horizontal margins.
22+
- We squashed several bugs that affected the way articles load.

WordPress/Resources/ar.lproj/Localizable.strings

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10965,9 +10965,6 @@ Example: 27 social shares remaining in the next 30 days */
1096510965
/* Error message title informing the user that a search for sites in the Reader could not be loaded. */
1096610966
"reader.blog.search.loading.error" = "مشكلة في أثناء تحميل المدونات";
1096710967

10968-
/* A brief prompt when the user is searching for blogs in the Reader. */
10969-
"reader.blog.search.loading.title" = "جارٍ إحضار المدونات...";
10970-
1097110968
/* Message shown when the reader finds no blogs for the specified search phrase. The %@ is a placeholder for the search phrase. */
1097210969
"reader.blog.search.no.results.message.format" = "لم يتم العثور على تدوينات مطابقة لـ %@ بلغتك.";
1097310970

WordPress/Resources/bg.lproj/Localizable.strings

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Translation-Revision-Date: 2025-11-27 18:06:49+0000 */
1+
/* Translation-Revision-Date: 2025-11-30 08:04:12+0000 */
22
/* Plural-Forms: nplurals=2; plural=n != 1; */
33
/* Generator: GlotPress/4.0.3 */
44
/* Language: bg */
@@ -11007,9 +11007,6 @@ Example: 27 social shares remaining in the next 30 days */
1100711007
/* Error message title informing the user that a search for sites in the Reader could not be loaded. */
1100811008
"reader.blog.search.loading.error" = "Проблем при зареждането на блогове";
1100911009

11010-
/* A brief prompt when the user is searching for blogs in the Reader. */
11011-
"reader.blog.search.loading.title" = "Зареждане на блогове...";
11012-
1101311010
/* Message shown when the reader finds no blogs for the specified search phrase. The %@ is a placeholder for the search phrase. */
1101411011
"reader.blog.search.no.results.message.format" = "Не са открити блогове за %@ на вашия език.";
1101511012

@@ -11351,8 +11348,14 @@ Feel free to replace it with other bracket types that you think looks better for
1135111348
/* Accessibility label for a list of tags in the preview section. */
1135211349
"reader.preferences.preview.tagsList.a11y" = "Примерни етикети";
1135311350

11351+
/* Accessibility label for the Extra Extra Extra Large size option, used in the Reader's reading preferences. */
11352+
"reader.preferences.size.extraExtraExtraLarge" = "XXXL";
11353+
11354+
/* Accessibility label for the Extra Extra Large size option, used in the Reader's reading preferences. */
11355+
"reader.preferences.size.extraExtraLarge" = "XXL";
11356+
1135411357
/* Accessibility label for the Extra Large size option, used in the Reader's reading preferences. */
11355-
"reader.preferences.size.extraLarge" = "Много голям";
11358+
"reader.preferences.size.extraLarge" = "XL";
1135611359

1135711360
/* Accessibility label for the Extra Small size option, used in the Reader's reading preferences. */
1135811361
"reader.preferences.size.extraSmall" = "Много малък";

WordPress/Resources/de.lproj/Localizable.strings

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Translation-Revision-Date: 2025-11-26 09:54:09+0000 */
1+
/* Translation-Revision-Date: 2025-11-30 02:01:43+0000 */
22
/* Plural-Forms: nplurals=2; plural=n != 1; */
33
/* Generator: GlotPress/4.0.3 */
44
/* Language: de */
@@ -11007,9 +11007,6 @@ Example: 27 social shares remaining in the next 30 days */
1100711007
/* Error message title informing the user that a search for sites in the Reader could not be loaded. */
1100811008
"reader.blog.search.loading.error" = "Es gab ein Problem beim Laden der Blogs";
1100911009

11010-
/* A brief prompt when the user is searching for blogs in the Reader. */
11011-
"reader.blog.search.loading.title" = "Blogs werden abgerufen …";
11012-
1101311010
/* Message shown when the reader finds no blogs for the specified search phrase. The %@ is a placeholder for the search phrase. */
1101411011
"reader.blog.search.no.results.message.format" = "Es wurden keine Blogs gefunden, die %@ in deiner Sprache entsprechen.";
1101511012

@@ -11351,6 +11348,12 @@ Feel free to replace it with other bracket types that you think looks better for
1135111348
/* Accessibility label for a list of tags in the preview section. */
1135211349
"reader.preferences.preview.tagsList.a11y" = "Beispielschlagwörter";
1135311350

11351+
/* Accessibility label for the Extra Extra Extra Large size option, used in the Reader's reading preferences. */
11352+
"reader.preferences.size.extraExtraExtraLarge" = "Überaus groß";
11353+
11354+
/* Accessibility label for the Extra Extra Large size option, used in the Reader's reading preferences. */
11355+
"reader.preferences.size.extraExtraLarge" = "Extrem groß";
11356+
1135411357
/* Accessibility label for the Extra Large size option, used in the Reader's reading preferences. */
1135511358
"reader.preferences.size.extraLarge" = "Extra Groß";
1135611359

0 commit comments

Comments
 (0)