@@ -32,7 +32,8 @@ struct EpsilonUpdateChecker {
3232 )
3333 }
3434
35- // Parses NumWorks' official download page, finds all simulator zip URLs, and returns the highest version.
35+ // Parses NumWorks' download page for cdn.numworks.com zip URLs (e.g. numworks-simulator-*.zip or numworks-graphing-emulator-*.zip).
36+ // Picks the URL whose filename contains the highest X.Y.Z version.
3637 static func fetchLatestRemoteURL( ) async throws -> URL {
3738 let page = URL ( string: " https://www.numworks.com/simulator/download/ " ) !
3839 var req = URLRequest ( url: page)
@@ -48,24 +49,19 @@ struct EpsilonUpdateChecker {
4849 throw Error . invalidResponse
4950 }
5051
51- let pattern = #"https:\/\/cdn\.numworks\.com\/[A-Za-z0-9_-]+\/numworks-simulator-(\d+\.\d+\.\d+)\.zip"#
52- let r = try NSRegularExpression ( pattern: pattern)
52+ // Match any cdn.numworks.com ... .zip URL (e.g. .../numworks-graphing-emulator-25.2.2.zip or .../26.1.zip).
53+ let urlPattern = #"https://cdn\.numworks\.com/[^"'\s<>]+\.zip"#
54+ let urlRegex = try NSRegularExpression ( pattern: urlPattern)
5355 let range = NSRange ( html. startIndex..< html. endIndex, in: html)
54- let matches = r. matches ( in: html, range: range)
55- guard !matches. isEmpty else {
56- throw Error . couldNotExtractRemoteURL
57- }
56+ let urlMatches = urlRegex. matches ( in: html, range: range)
5857
5958 var best : ( url: URL , ver: SemVer ) ?
6059
61- for m in matches {
60+ for m in urlMatches {
6261 guard let urlRange = Range ( m. range ( at: 0 ) , in: html) else { continue }
6362 let urlString = String ( html [ urlRange] )
6463 guard let url = URL ( string: urlString) else { continue }
65-
66- guard let verRange = Range ( m. range ( at: 1 ) , in: html) else { continue }
67- let verString = String ( html [ verRange] )
68- guard let ver = SemVer ( verString) else { continue }
64+ guard let ver = parseVersionFromFilename ( url. lastPathComponent) else { continue }
6965
7066 if let currentBest = best {
7167 if ver > currentBest. ver { best = ( url, ver) }
@@ -90,13 +86,34 @@ struct EpsilonUpdateChecker {
9086 extractVersionString ( from: url. lastPathComponent)
9187 }
9288
89+ /// Extracts a version string (X.Y or X.Y.Z) from a filename and normalizes to X.Y.Z for SemVer.
9390 static func extractVersionString( from filename: String ) -> String ? {
94- let pattern = #"(\d+)\.(\d+)\.(\d+)"#
95- guard let r = try ? NSRegularExpression ( pattern: pattern) else { return nil }
91+ parseVersionFromFilename ( filename) ? . string
92+ }
93+
94+ /// Parses X.Y or X.Y.Z from a filename (e.g. "26.1.zip" or "numworks-simulator-25.2.2.zip"). Returns nil if no valid version.
95+ private static func parseVersionFromFilename( _ filename: String ) -> SemVer ? {
96+ // Prefer X.Y.Z then X.Y (treat as X.Y.0).
97+ let threePart = #"(\d+)\.(\d+)\.(\d+)"#
98+ // X.Y only when not part of X.Y.Z (e.g. 26.1 in "26.1.zip" yes; 26.1 in "26.1.2.zip" no)
99+ let twoPart = #"(\d+)\.(\d+)(?=\.zip|[^.\d]|$)"#
96100 let range = NSRange ( filename. startIndex..< filename. endIndex, in: filename)
97- guard let m = r. firstMatch ( in: filename, range: range) else { return nil }
98- guard let rr = Range ( m. range ( at: 0 ) , in: filename) else { return nil }
99- return String ( filename [ rr] )
101+ if let r3 = try ? NSRegularExpression ( pattern: threePart) ,
102+ let m3 = r3. firstMatch ( in: filename, range: range) ,
103+ let rr = Range ( m3. range ( at: 0 ) , in: filename) {
104+ let s = String ( filename [ rr] )
105+ return SemVer ( s)
106+ }
107+ if let r2 = try ? NSRegularExpression ( pattern: twoPart) ,
108+ let m2 = r2. firstMatch ( in: filename, range: range) ,
109+ let r0 = Range ( m2. range ( at: 0 ) , in: filename) ,
110+ let r1 = Range ( m2. range ( at: 1 ) , in: filename) ,
111+ let r2g = Range ( m2. range ( at: 2 ) , in: filename) {
112+ let major = Int ( filename [ r1] ) ?? 0
113+ let minor = Int ( filename [ r2g] ) ?? 0
114+ return SemVer ( " \( major) . \( minor) .0 " )
115+ }
116+ return nil
100117 }
101118}
102119
0 commit comments