123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170(* Claude Code
*
* Copyright (C) 2026 Yoann Padioleau
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public License
* (LGPL) as published by the Free Software Foundation; either version
* 2 of the License, or (at your option) any later version.
*)(* See Html_tree.mli *)(*****************************************************************************)(* Elements being built *)(*****************************************************************************)(* an element while its children still come: they are kept last first,
* and the tree is frozen into Dom values at the end *)typeopen_element={name:string;origin:Dtd.origin;mutableattributes:(string*string)list;mutableextensions:(string*string)list;(* Netscape's, apart *)mutablechildren:childlist;(* the last first *)}andchild=Eofopen_element|Tofstring(* a start tag's name and attributes, with their origins (Html_lexer) *)typetag={tag_name:string;origin:Dtd.origin;attributes:(string*string)list;extensions:(string*string)list}letcore(name:string):tag={tag_name=name;origin=Core;attributes=[];extensions=[]}letmake(tag:tag):open_element={name=tag.tag_name;origin=tag.origin;attributes=tag.attributes;extensions=tag.extensions;children=[]}letrecfreeze(e:open_element):Dom.element={name=e.name;attributes=e.attributes;extensions=e.extensions;origin=e.origin;children=List.rev_map(func->matchcwithEe->Dom.Element(freezee)|Ts->Dom.Texts)e.children;}(* attributes given again (a second <body bgcolor=...>): the new ones
* added, the old ones kept *)letadd_attributes(e:open_element)(tag:tag):unit=letaddold(n,v)=ifList.mem_assocnoldthenoldelseold@[(n,v)]ine.attributes<-List.fold_leftadde.attributestag.attributes;e.extensions<-List.fold_leftadde.extensionstag.extensions(*****************************************************************************)(* The stack of open elements *)(*****************************************************************************)(* its top first; [body_started] tells the head from the body *)typet={html:open_element;head:open_element;body:open_element;mutablestack:open_elementlist;mutablebody_started:bool;(* a newline right after <pre> is not content (so that the first line
* can start on a line of its own in the source) *)mutableskip_newline:bool;}lettop(t:t):open_element=List.hdt.stackletappend_text(t:t)(s:string):unit=lete=toptinmatche.childrenwithTbefore::rest->e.children<-T(before^s)::rest|_->e.children<-Ts::e.childrenletinsert(t:t)(tag:tag):unit=lete=maketagin(topt).children<-Ee::(topt).children;ifnot(Dtd.is_voidtag.tag_name)thent.stack<-e::t.stack(* pop down to the first element satisfying [found], looking no further
* than one satisfying [stop]; false if none was found *)letpop_to(t:t)~(found:string->bool)~(stop:string->bool):bool=letrecsearch(stack:open_elementlist)=matchstackwith|[]->None|e::rest->iffounde.namethenSomerestelseifstope.namethenNoneelsesearchrestinmatchsearcht.stackwith|Somerest->t.stack<-rest;true|None->falseletstart_body(t:t):unit=ifnott.body_startedthen(t.body_started<-true;t.stack<-[t.body;t.html])(*****************************************************************************)(* The tokens *)(*****************************************************************************)letis_blank(s:string):bool=String.for_all(func->c=' '||c='\n'||c='\t')sletstart_tag?(self_closing=false)(t:t)(tag:tag):unit=letname=tag.tag_nameinmatchnamewith(* inside <svg>, "foreign content": XML's rules, not HTML's -- nothing
* closes what is open, and <path/> closes itself *)|_whent.body_started&&List.exists(fun(e:open_element)->e.name="svg")t.stack->insertttag;ifself_closingthent.stack<-List.tlt.stack|"svg"whenself_closing->start_bodyt;insertttag;t.stack<-List.tlt.stack|"html"->add_attributest.htmltag|"head"->()|"body"->start_bodyt;add_attributest.bodytag|_whenDtd.is_head_elementname&¬t.body_started->insertttag|_->start_bodyt;(* 1. what x closes, nearest first, as long as some is found *)whilepop_tot~found:(Dtd.closesname)~stop:(Dtd.stopsname)do()done;(* 2. and 3. *)insertttag;t.skip_newline<-List.memname["pre";"listing";"textarea"]letend_tag(t:t)(name:string):unit=matchnamewith|"html"|"body"|"head"->()|"p"->ifnot(pop_tot~found:((=)"p")~stop:(Dtd.stops"p"))then((* </p> with no <p>: an empty one, as the spec says *)start_bodyt;insertt(core"p");ignore(pop_tot~found:((=)"p")~stop:(fun_->false)))|_->ignore(pop_tot~found:((=)name)~stop:(funy->List.memy["html";"table";"td";"th";"caption"]))letparse(tokens:Html_lexer.tokenlist):Dom.element=lethtml=make(core"html")andhead=make(core"head")andbody=make(core"body")inhtml.children<-[Ebody;Ehead];lett={html;head;body;stack=[head;html];body_started=false;skip_newline=false}inList.iter(fun(token:Html_lexer.token)->lettoken:Html_lexer.token=matchtokenwith|Textswhent.skip_newline&&String.lengths>0&&s.[0]='\n'->Text(String.subs1(String.lengths-1))|_->tokenint.skip_newline<-false;matchtokenwith|Text""->()|Doctype_|Comment_->()|Start_tag{name;attributes;extensions;origin;self_closing}->start_tag~self_closingt{tag_name=name;origin;attributes;extensions}|End_tagname->end_tagtname|Texts->(* in the head itself, only spaces are allowed: other text
* starts the body; inside a <title>, it is the title *)ift.body_started||topt!=t.headthenappend_texttselseifnot(is_blanks)then(start_bodyt;append_textts))tokens;freezehtmlletof_string(s:string):Dom.element=parse(Html_lexer.tokenizes)