@@ -169,7 +169,7 @@ function usage(c: Colors): string {
169169 "" ,
170170 ` ${ c . green ( "iiif-parser" ) } ${ c . yellow ( "upgrade" ) } ${ c . dim ( "<input.json> <output.json>" ) } ` ,
171171 ` ${ c . green ( "iiif-parser" ) } ${ c . yellow ( "download" ) } ${ c . dim ( "<manifest-url> <output.json>" ) } ${ c . dim ( "[--version 3|4]" ) } ` ,
172- ` ${ c . green ( "iiif-parser" ) } ${ c . yellow ( "validate-p4" ) } ${ c . dim ( "<input-path...>" ) } ${ c . dim ( "[--strict] [--json] [--show-warnings]" ) } ` ,
172+ ` ${ c . green ( "iiif-parser" ) } ${ c . yellow ( "validate-p4" ) } ${ c . dim ( "<input-path-or-url ...>" ) } ${ c . dim ( "[--strict] [--json] [--show-warnings]" ) } ` ,
173173 "" ,
174174 ` ${ c . bold ( c . cyan ( "Commands:" ) ) } ` ,
175175 "" ,
@@ -179,7 +179,7 @@ function usage(c: Colors): string {
179179 ` ${ c . yellow ( "download" ) } Download a manifest and save as Presentation 3` ,
180180 ` ${ c . dim ( "(default)" ) } or Presentation 4.` ,
181181 "" ,
182- ` ${ c . yellow ( "validate-p4" ) } Validate one or more files/folders of Presentation 4` ,
182+ ` ${ c . yellow ( "validate-p4" ) } Validate one or more files/folders/URLs of Presentation 4` ,
183183 ` manifests.` ,
184184 "" ,
185185 ` ${ c . bold ( c . cyan ( "Options:" ) ) } ` ,
@@ -241,6 +241,15 @@ function parseJson(contents: string, source: string): unknown {
241241 }
242242}
243243
244+ function isHttpUrl ( value : string ) : boolean {
245+ try {
246+ const url = new URL ( value ) ;
247+ return url . protocol === "http:" || url . protocol === "https:" ;
248+ } catch {
249+ return false ;
250+ }
251+ }
252+
244253function toSerializedPresentation4 ( input : unknown ) : unknown {
245254 const upgraded = upgradeToPresentation4 ( input ) ;
246255 const normalized = normalize ( upgraded ) ;
@@ -339,7 +348,7 @@ async function runValidateP4(
339348 if ( positionals . length < 2 ) {
340349 deps . stderr ( `\n ${ c . red ( `${ SYM . cross } Missing arguments` ) } \n` ) ;
341350 deps . stderr (
342- ` Usage: ${ c . green ( "iiif-parser" ) } ${ c . yellow ( "validate-p4" ) } ${ c . dim ( "<input-path...> [--strict] [--json] [--show-warnings]" ) } \n`
351+ ` Usage: ${ c . green ( "iiif-parser" ) } ${ c . yellow ( "validate-p4" ) } ${ c . dim ( "<input-path-or-url ...> [--strict] [--json] [--show-warnings]" ) } \n`
343352 ) ;
344353 return 2 ;
345354 }
@@ -348,12 +357,17 @@ async function runValidateP4(
348357 const strict = options . strict === true ;
349358 const jsonOutput = options . json === true ;
350359 const showWarnings = options [ "show-warnings" ] === true ;
351- const expandedPaths : string [ ] = [ ] ;
360+ const expandedInputs : Array < { type : "file" | "url" ; path: string } > = [ ] ;
352361
353362 async function collectJsonFiles ( path : string ) : Promise < void > {
363+ if ( isHttpUrl ( path ) ) {
364+ expandedInputs . push ( { type : "url" , path } ) ;
365+ return ;
366+ }
367+
354368 const pathInfo = await stat ( path ) ;
355369 if ( ! pathInfo . isDirectory ( ) ) {
356- expandedPaths . push ( path ) ;
370+ expandedInputs . push ( { type : "file" , path } ) ;
357371 return ;
358372 }
359373
@@ -365,7 +379,7 @@ async function runValidateP4(
365379 continue ;
366380 }
367381 if ( entry . isFile ( ) && entry . name . toLowerCase ( ) . endsWith ( ".json" ) ) {
368- expandedPaths . push ( entryPath ) ;
382+ expandedInputs . push ( { type : "file" , path : entryPath } ) ;
369383 }
370384 }
371385 }
@@ -374,10 +388,10 @@ async function runValidateP4(
374388 await collectJsonFiles ( inputPath ) ;
375389 }
376390
377- expandedPaths . sort ( ) ;
391+ expandedInputs . sort ( ( a , b ) => a . path . localeCompare ( b . path ) ) ;
378392
379393 const summary = {
380- scanned : expandedPaths . length ,
394+ scanned : expandedInputs . length ,
381395 validated : 0 ,
382396 skipped : 0 ,
383397 valid : 0 ,
@@ -397,14 +411,18 @@ async function runValidateP4(
397411 deps . stdout (
398412 ` ${ c . bold ( c . cyan ( "Presentation 4 Validation" ) ) } ${ c . dim ( `(${ strict ? "strict" : "tolerant" } mode)` ) } `
399413 ) ;
400- deps . stdout ( ` ${ c . dim ( `Scanning ${ expandedPaths . length } file ${ expandedPaths . length === 1 ? "" : "s" } ...` ) } ` ) ;
414+ deps . stdout ( ` ${ c . dim ( `Scanning ${ expandedInputs . length } input ${ expandedInputs . length === 1 ? "" : "s" } ...` ) } ` ) ;
401415 deps . stdout ( "" ) ;
402416 }
403417
404418 // ── First pass: compact one-line-per-file results ──────────────
405419
406- for ( const inputPath of expandedPaths ) {
407- const input = parseJson ( await deps . readFileText ( inputPath ) , inputPath ) ;
420+ for ( const inputRef of expandedInputs ) {
421+ const inputPath = inputRef . path ;
422+ const input =
423+ inputRef . type === "url"
424+ ? await deps . fetchJson ( inputPath )
425+ : parseJson ( await deps . readFileText ( inputPath ) , inputPath ) ;
408426 const resourceType = ( input as { type ?: string ; "@type" ?: string } ) ?. type ?? ( input as any ) ?. [ "@type" ] ;
409427 if ( resourceType !== "Manifest" ) {
410428 summary . skipped ++ ;
0 commit comments