Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/adapters/default/discover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ describe('discoverDefault', () => {
document.querySelector('#root').innerHTML = '<header><nav>' +
'<a href="/">Home</a><a href="/platform">Platform</a>' +
'<a href="/solutions">Solutions</a><a href="/ai">AI</a>' +
'</nav><a href="/resources">Resources</a><a href="/contact">Contact</a></header>';
'<a href="/resume.PDF?download=1">Resume</a>' +
'</nav><a href="/resources">Resources</a><a href="/contact">Contact</a><a href="/guide.pdf">Guide</a></header>';
</script>`);
});
await new Promise<void>((resolve) => server!.listen(0, '127.0.0.1', resolve));
Expand Down Expand Up @@ -76,7 +77,7 @@ describe('discoverDefault', () => {
}
response.setHeader('content-type', 'text/html');
response.end(`<!doctype html><title>Example</title>
<nav><a href="/one">One</a><a href="/two">Two</a></nav>
<nav><a href="/one">One</a><a href="/two">Two</a><a href="/resume.pdf">Resume</a></nav>
<div class="dmFooter"><a href="/aviso-legal">Aviso legal</a><a href="/three?ref=footer">Three</a></div>`);
});
await new Promise<void>((resolve) => server!.listen(0, '127.0.0.1', resolve));
Expand All @@ -95,5 +96,6 @@ describe('discoverDefault', () => {
`${origin}/five`,
`${origin}/aviso-legal`,
]);
expect(inventory.navigation).toContainEqual({ text: 'Resume', href: `${origin}/resume.pdf` });
});
});
11 changes: 5 additions & 6 deletions src/adapters/default/discover.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fetchSitemapWithDiagnostics, classifyUrl, extractSameOriginLinks, routeKey } from '../../lib/extraction/sitemap.js';
import { fetchSitemapWithDiagnostics, classifyUrl, extractSameOriginLinks, resolvePageLink, routeKey } from '../../lib/extraction/sitemap.js';
import { extractMeta, extractTitle, extractNavLinks } from '../../lib/html-extract/index.js';
import { sourceContextOptions, getPlaywright } from '../../lib/browser-kit/browser-kit.js';
import type { InventoryUrl } from '../shared.js';
Expand Down Expand Up @@ -78,13 +78,12 @@ export async function discoverDefault(url: string, _opts: Record<string, unknown
...renderedHeaderUrls,
...extractSameOriginLinks(homepageHtml, normalized),
]) {
const linkUrl = new URL(href);
if (linkUrl.origin !== origin || !['http:', 'https:'].includes(linkUrl.protocol)) continue;
linkUrl.hash = '';
const key = routeKey(linkUrl.href);
const pageUrl = resolvePageLink(href, normalized, origin);
if (!pageUrl) continue;
const key = routeKey(pageUrl);
if (knownRoutes.has(key)) continue;
knownRoutes.add(key);
discoveredUrls.add(linkUrl.href);
discoveredUrls.add(pageUrl);
}
for (const u of discoveredUrls) {
const type = classifyUrl(u);
Expand Down
6 changes: 3 additions & 3 deletions src/lib/extraction/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ export function extractSameOriginLinks(html: string, baseUrl: string, baseOrigin
$('a[href]').each((_, el) => {
const href = $(el).attr('href')?.trim();
if (!href || href.startsWith('#')) return;
const resolved = resolveAndFilter(href, baseUrl, baseOrigin);
const resolved = resolvePageLink(href, baseUrl, baseOrigin);
if (resolved && !seen.has(resolved)) {
seen.add(resolved);
urls.push(resolved);
Expand Down Expand Up @@ -363,7 +363,7 @@ async function crawlRenderedNavLinks(baseUrl: string, baseOrigin: string): Promi
);
const seen = new Set<string>();
return hrefs.flatMap((href) => {
const resolved = resolveAndFilter(href, baseUrl, baseOrigin);
const resolved = resolvePageLink(href, baseUrl, baseOrigin);
if (!resolved || seen.has(resolved)) return [];
seen.add(resolved);
return [resolved];
Expand All @@ -376,7 +376,7 @@ async function crawlRenderedNavLinks(baseUrl: string, baseOrigin: string): Promi
}
}

function resolveAndFilter(href: string, baseUrl: string, baseOrigin: string): string | null {
export function resolvePageLink(href: string, baseUrl: string, baseOrigin: string): string | null {
try {
const resolved = new URL(href, baseUrl);
if (resolved.protocol !== 'http:' && resolved.protocol !== 'https:') return null;
Expand Down
11 changes: 7 additions & 4 deletions src/lib/screenshot/resource-capture.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,23 +68,26 @@ describe( 'CapturedResourceStore', () => {
expect( manifest.icons[ 0 ].src ).toBe( identity.logo.url );
} );

it( 'exports a linked DOCX as portable bytes beside a missing HTML route', async () => {
it.each( [
[ 'docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'PK\x03\x04word document fixture' ],
[ 'PDF', 'application/pdf', '%PDF-1.7\npdf document fixture' ],
] )( 'exports a linked %s as portable bytes beside a missing HTML route', async ( extension, contentType, content ) => {
const outputDir = mkdtempSync( join( tmpdir(), 'dla-linked-docx-' ) );
dirs.push( outputDir );
mkdirSync( join( outputDir, 'html' ) );
mkdirSync( join( outputDir, 'screenshots' ) );
const sourceUrl = 'https://example.com/';
const download = 'https://example.com/_files/ugd/flyer.docx?dn=Fly%20fishing.docx';
const download = `https://example.com/_files/ugd/flyer.${ extension }?dn=Fly%20fishing.${ extension }`;
const missing = 'https://example.com/missing-page';
const bytes = Buffer.from( 'PK\x03\x04word document fixture' );
const bytes = Buffer.from( content );
const html = `<html><body><a id="flyer" href="${ download }">Flyer</a><a id="missing" href="/missing-page">Missing</a></body></html>`;
writeFileSync( join( outputDir, 'html', 'home.html' ), html );
writeFileSync( join( outputDir, 'screenshots', 'manifest.json' ), JSON.stringify( {
version: 1, entries: { [ sourceUrl ]: { html: 'html/home.html' }, [ missing ]: {} },
} ) );
const fetchMedia = vi.fn( async ( url: string ) => ( {
finalUrl: url, status: 200,
headers: new Headers( { 'content-type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' } ),
headers: new Headers( { 'content-type': contentType } ),
body: bytes,
} ) );
const store = new CapturedResourceStore( outputDir, sourceUrl, fetchMedia );
Expand Down
6 changes: 3 additions & 3 deletions src/lib/screenshot/resource-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,11 +305,11 @@ export function isAudioLink( reference: string, documentUrl: string ): boolean {
}
}

/** Linked Word files are downloads, not navigable HTML routes. */
/** Linked documents are downloads, not navigable HTML routes. */
export function isDocumentDownloadLink( reference: string, documentUrl: string ): boolean {
try {
const url = new URL( reference.replace( /&amp;/g, '&' ), documentUrl );
return /^https?:$/.test( url.protocol ) && /\.docx?$/i.test( url.pathname );
return /^https?:$/.test( url.protocol ) && /\.(?:pdf|docx?)$/i.test( url.pathname );
} catch {
return false;
}
Expand Down Expand Up @@ -662,7 +662,7 @@ export class CapturedResourceStore {
if ( fetched.body.length === 0 && contentType !== 'text/css' )
throw new Error( 'render dependency response body is empty' );
if (
! /^(?:text\/css|image\/|audio\/|video\/|font\/|application\/(?:json|manifest\+json|font|x-font|font-woff|octet-stream|msword|vnd\.openxmlformats-officedocument\.wordprocessingml\.document))/i.test(
! /^(?:text\/css|image\/|audio\/|video\/|font\/|application\/(?:json|manifest\+json|font|x-font|font-woff|octet-stream|pdf|msword|vnd\.openxmlformats-officedocument\.wordprocessingml\.document))/i.test(
contentType
)
)
Expand Down
Loading