123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347(*---------------------------------------------------------------------------
Copyright (c) 2024 The webs programmers. All rights reserved.
SPDX-License-Identifier: ISC
---------------------------------------------------------------------------*)letstring_subrange?(first=0)?lasts=letmax=String.lengths-1inletlast=matchlastwith|None->max|Somelwhenl>max->max|Somel->linletfirst=iffirst<0then0elsefirstiniffirst>lastthen""elseString.subsfirst(last-first+1)letwhite=function' '|'\t'..'\r'->true|_->falseletalpha=function'A'..'Z'|'a'..'z'->true|_->falseletdigit=function'0'..'9'->true|_->false(* Urls *)typescheme=stringtypeauthority=stringtypepath=stringtypequery=stringtypefragment=stringtypet=stringletscheme_charc=alphac||digitc||Char.equalc'+'||Char.equalc'-'||Char.equal'.'cletfind_scheme_colonu=ifu=""||not(alphau.[0])thenNoneelseletmax=String.lengthu-1inleti=ref1inwhile!i<=max&&scheme_charu.[!i]doincridone;if!i>max||u.[!i]<>':'thenNoneelseSome!iletfind_authority_last~startu=letmax=String.lengthu-1inifstart>maxthenNoneelseifstart+1>maxthenSome(start-1)elseifnot(u.[start]='/'&&u.[start+1]='/')thenSome(start-1)elseleti=ref(start+2)inwhile(!i<=max&&u.[!i]<>'/'&&u.[!i]<>'?'&&u.[!i]<>'#')doincridone;Some(!i-1)letschemeu=matchfind_scheme_colonuwith|None->None|Somei->Some(String.subu0i)letauthorityu=letstart=matchfind_scheme_colonuwith|None->0|Somei->i+1inletfirst=start+2inmatchfind_authority_last~startuwith|None->None|Somelastwhenlast>=first->Some(string_subrange~first~lastu)|Some_->Noneletpath_firstu=letstart=matchfind_scheme_colonuwith|None->0|Somei->i+1inletfirst=matchfind_authority_last~startuwith|None->start|Somelast->last+1inletmax=String.lengthu-1iniffirst>max||u.[first]='#'||u.[first]='?'thenNoneelseSomefirstletpath_lastu~first=letmax=String.lengthu-1inleti=ref(first+1)inwhile(!i<=max&&u.[!i]<>'?'&&u.[!i]<>'#')doincridone;!i-1letpathu=matchpath_firstuwith|None->None|Somefirst->Some(string_subrange~first~last:(path_lastu~first)u)letqueryu=letmax=String.lengthu-1inleti=ref0inwhile(!i<=max&&u.[!i]<>'?'&&u.[!i]<>'#')doincridone;if!i>max||u.[!i]='#'thenNoneelsebeginincri;letfirst=!iinwhile(!i<=max&&u.[!i]<>'#')doincridone;letlast=!i-1inSome(string_subrange~first~lastu)endletfragmentu=letmax=String.lengthu-1inleti=ref0inwhile(!i<=max&&u.[!i]<>'#')doincridone;if!i>maxthenNoneelseSome(string_subrange~first:(!i+1)u)(* Kinds *)typerelative_kind=[`Scheme|`Absolute_path|`Relative_path|`Empty]typekind=[`Absolute|`Relativeofrelative_kind]letrelative_kinds=letlen=String.lengthsiniflen=0then`Emptyelseifs.[0]='/'then(iflen>1&&s.[1]='/'then`Schemeelse`Absolute_path)else`Relative_pathletkinds=matchfind_scheme_colonswith|Some_->`Absolute|None->`Relative(relative_kinds)(* Operations *)letof_urlu?scheme:s?authority:a?path:p?query:q?fragment:f()=letadd_schemesu=matchswithNone->u|Somes->s::":"::uinletadd_authorityau=matchawithNone->u|Somea->"//"::a::uinletadd_pathpu=matchpwithNone->u|Somep->p::uinletadd_queryqu=matchqwithNone->u|Someq->"?"::q::uinletadd_fragfu=matchfwithNone->u|Somef->"#"::f::uinlets=matchswithNone->schemeu|Somes->sinleta=matchawithNone->authorityu|Somea->ainletp=matchpwithNone->pathu|Somep->pinletq=matchqwithNone->queryu|Someq->qinletf=matchfwithNone->fragmentu|Somef->finString.concat""@@add_schemes@@add_authoritya@@add_pathp@@add_queryq@@add_fragf@@[]letpath_and_restu=matchpath_firstuwith|None->None|Somefirst->Some(string_subrange~firstu)letdrop_path_and_restu=matchpath_firstuwith|None->u|Somefirst->string_subrange~last:(first-1)uletappendrootu=matchkinduwith|`Absolute->u|`Relative`Scheme->beginmatchschemerootwith|None->u|Somescheme->String.concat":"[scheme;u]end|`Relative`Absolute_path->String.concat""[drop_path_and_restroot;u]|`Relative`Relative_path->ifroot<>""&&root.[String.lengthroot-1]='/'thenString.concat""[root;u]elsebeginmatchString.rindexroot'/'with|exceptionNot_found->String.concat"/"[root;u]|i->matchfind_scheme_colonrootwith|None->String.concat""[string_subrange~last:iroot;u]|Somejwhenj+2=i->String.concat"/"[root;u]|Some_->String.concat""[string_subrange~last:iroot;u]end|`Relative`Empty->rootletto_absolute~scheme~root_pathu=matchkinduwith|`Absolute->u|`Relative`Scheme->String.concat":"[scheme;u]|`Relative`Absolute_path->String.concat"://"[scheme;u]|`Relative`Relative_path->letroot=matchroot_pathwith|Some""|None->""|Somerootwhenroot.[String.lengthroot-1]='/'->root|Someroot->root^"/"inString.concat""[scheme;"://";root;u]|`Relative`Empty->letroot=Option.value~default:""root_pathinString.concat""[scheme;"://";root](* Authority *)moduleAuthority=structtypet=authorityletfind_userinfo_ata=String.index_opta'@'letfind_port_colona=ifa=""thenNoneelseletmax=String.lengtha-1inleti=refmaxinwhile!i>=0&&digita.[!i]dodecridone;if!i<0thenNoneelseifa.[!i]=':'thenSome!ielseNoneletuserinfoa=matchfind_userinfo_atawith|None->None|Somei->Some(String.suba0i)lethosta=letfirst=matchfind_userinfo_atawithNone->0|Someat->at+1inletlast=matchfind_port_colonawith|None->String.lengtha-1|Somecolon->colon-1instring_subrangea~first~lastletporta=matchfind_port_colonawith|None->None|Somec->int_of_string_opt(string_subrangea~first:(c+1))end(* Scraping *)letlist_of_text_scrape?roots=(* See .mli to understand what it does *)letrecfind_stopsimaxstop=ifi>maxthenielseifstops.[i]thenielsefind_stops(i+1)maxstopinletparse_attsimax=letj=find_stopsimax(Fun.negatewhite)inifnot(j<max&&s.[j]='=')thenNoneelseletk=find_stops(j+1)max(Fun.negatewhite)inifnot(k<max&&(s.[k]='\''||s.[k]='\"'))thenNoneelseletl=find_stops(k+1)max(Char.equals.[k])inifnot(l<=max)thenNoneelseleturl=String.trim(string_subrange~first:(k+1)~last:(l-1)s)inifurl=""thenNoneelseSome(url,l+1)inletrecfind_nextaccsimax=letadd_urlurlacc=matchrootwith|None->url::acc|Someroot->(appendrooturl)::accinifi>maxthenList.revaccelsematchs.[i]with|'s'wheni+5<=max&&s.[i+1]='r'&&s.[i+2]='c'->beginmatchparse_atts(i+3)maxwith|None->find_nextaccs(i+3)max|Some(url,next)->find_next(add_urlurlacc)snextmaxend|'h'wheni+6<=max&&s.[i+1]='r'&&s.[i+2]='e'&&s.[i+3]='f'->beginmatchparse_atts(i+4)maxwith|None->find_nextaccs(i+4)max|Some(url,next)->find_next(add_urlurlacc)snextmaxend|'h'wheni+7<=max&&s.[i+1]='t'&&s.[i+2]='t'&&s.[i+3]='p'->letstop=ifi=0thenSomewhiteelsematchs.[i-1]with|'\"'|'\''asc->Some(Char.equalc)|'<'->Some(Char.equal'>')|cwhenwhitec->Somewhite|_->Noneinbeginmatchstopwith|None->find_nextaccs(i+1)max|Somestop->letstop=find_stopsimaxstopinleturl=string_subrange~first:i~last:(stop-1)sinifnot(String.starts_with~prefix:"http://"url||String.starts_with~prefix:"https://"url)thenfind_nextaccs(i+1)maxelsefind_next(add_urlurlacc)sstopmaxend|_->find_nextaccs(i+1)maxinfind_next[]s0(String.lengths-1)(* Formatting *)letpp=Format.pp_print_stringletpp_kindppfk=Format.pp_print_stringppf@@matchkwith|`Absolute->"abs"|`Relative`Scheme->"rel-scheme"|`Relative`Absolute_path->"rel-abs-path"|`Relative`Relative_path->"rel-rel-path"|`Relative`Empty->"rel-empty"(* Percent encoding *)modulePercent=struct(* See https://tools.ietf.org/html/rfc3986 *)typekind=[`Uri_component|`Uri]letis_char_verbatim_in_uri_component=function(* unreserved *)|'A'..'Z'|'a'..'z'|'0'..'9'|'-'|'.'|'_'|'~'(* sub-delims *)|'!'|'$'|'&'|'\''|'('|')'|'*'|'+'|','|';'|'='->true|_->falseletis_char_verbatim_in_uri=function(* unreserved *)|'A'..'Z'|'a'..'z'|'0'..'9'|'-'|'.'|'_'|'~'(* sub-delims *)|'!'|'$'|'&'|'\''|'('|')'|'*'|'+'|','|';'|'='(* gen-delims *)|':'|'/'|'?'|'#'|'['|']'|'@'->true|_->falseletis_hexdig=function|'0'..'9'|'A'..'F'|'a'..'f'->true|_->falselethexdig_to_int=function|'0'..'9'asc->Char.codec-0x30|'A'..'F'asc->Char.codec-0x37|'a'..'f'asc->Char.codec-0x57|_->assertfalseletunsafe_hexdig_of_inti=ifi<10thenChar.unsafe_chr(i+0x30)elseChar.unsafe_chr(i+0x37)letencode_to_bufferis_verbatimbs=fork=0toString.lengths-1domatchs.[k]with|cwhenis_verbatimc->Buffer.add_charbc|c->lethi=(Char.codeclsr4)land0xFinletlo=(Char.codec)land0xFinBuffer.add_charb'%';Buffer.add_charb(unsafe_hexdig_of_inthi);Buffer.add_charb(unsafe_hexdig_of_intlo)doneletdecode_to_bufferbs~first~last=leti=reffirstinwhile(!i<=last)domatchs.[!i]with|'+'->Buffer.add_charb' ';incri;|'%'when!i+2<=last->lethi=s.[!i+1]andlo=s.[!i+2]inbeginmatchis_hexdighi&&is_hexdiglowith|false->Buffer.add_charb'%';incri|true->letc=(hexdig_to_inthilsl4)lor(hexdig_to_intlo)inBuffer.add_charb(Char.unsafe_chrc);i:=!i+3end|c->Buffer.add_charbc;incridoneletencodekinds=(* One day we should benchmark whether the scan first to determine
length and then use Bytes directly is faster – see
Query.pct_encode_space_as_plus – one day. *)letis_verbatim=matchkindwith|`Uri_component->is_char_verbatim_in_uri_component|`Uri->is_char_verbatim_in_uriinletb=Buffer.create(String.lengths*2)inencode_to_bufferis_verbatimbs;Buffer.contentsbletdecodes=letb=Buffer.create(String.lengths)indecode_to_bufferbs~first:0~last:(String.lengths-1);Buffer.contentsbend