( listingUrl: string, rootUrl: string, html: string )
| 189 | } |
| 190 | |
| 191 | export function getDirectoryListingLinks( |
| 192 | listingUrl: string, |
| 193 | rootUrl: string, |
| 194 | html: string |
| 195 | ): DirectoryListingLink[] { |
| 196 | const root = getDirectoryRootUrl(rootUrl); |
| 197 | if (!root) { |
| 198 | return []; |
| 199 | } |
| 200 | |
| 201 | let current: URL; |
| 202 | try { |
| 203 | current = new URL(listingUrl); |
| 204 | } catch { |
| 205 | return []; |
| 206 | } |
| 207 | |
| 208 | const links: DirectoryListingLink[] = []; |
| 209 | const seen = new Set<string>(); |
| 210 | const hrefPattern = /<a\b[^>]*\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi; |
| 211 | |
| 212 | for (const match of html.matchAll(hrefPattern)) { |
| 213 | const rawHref = match[1] ?? match[2] ?? match[3] ?? ''; |
| 214 | const href = decodeHtmlAttribute(rawHref.trim()); |
| 215 | if (shouldSkipDirectoryHref(href)) { |
| 216 | continue; |
| 217 | } |
| 218 | |
| 219 | let target: URL; |
| 220 | try { |
| 221 | target = new URL(href, current); |
| 222 | } catch { |
| 223 | continue; |
| 224 | } |
| 225 | |
| 226 | target.hash = ''; |
| 227 | if (target.toString() === current.toString()) { |
| 228 | continue; |
| 229 | } |
| 230 | |
| 231 | const relativePath = getRelativeUrlPath(root, target); |
| 232 | if (!relativePath) { |
| 233 | continue; |
| 234 | } |
| 235 | |
| 236 | const isDirectory = href.endsWith('/') || target.pathname.endsWith('/'); |
| 237 | const isSplat = isSplatFilePath(target.pathname); |
| 238 | if (!isDirectory && !isSplat) { |
| 239 | continue; |
| 240 | } |
| 241 | |
| 242 | const key = target.toString(); |
| 243 | if (seen.has(key)) { |
| 244 | continue; |
| 245 | } |
| 246 | |
| 247 | seen.add(key); |
| 248 | links.push({ |
no test coverage detected