package tiny_languages
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
Small languages from scratch: Scheme, Lisp, Smalltalk-80, Pascal, BASIC, JavaScript, HTML, CSS and more
Install
dune-project
Dependency
Authors
Maintainers
Sources
0.3.6.tar.gz
md5=7c636383d146d30ac6f2fa234a6253c8
sha512=c79f3823c5f8f57e5038eb640d487c61168b84aa07c61999d6622ef9fd0c890e2b03b4c6a7cdbbe9352a49e25dda00ac7bb14693cee8e3d7beeed251351a2af0
doc/src/tiny_languages.lang_html/Html_lexer.ml.html
Source file Html_lexer.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295(* 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_lexer.mli *) type attribute = string * string type token = | Doctype of string | Start_tag of { name : string; attributes : attribute list; extensions : attribute list; origin : Dtd.origin; self_closing : bool; } | End_tag of string | Text of string | Comment of string (*****************************************************************************) (* The machine's memory *) (*****************************************************************************) (* the page, the tokens so far (the last first), and the text read since * the last token, raw (its entities decoded when it is emitted) *) type t = { s : string; n : int; mutable tokens : token list; text : Buffer.t } (* a tag being read *) type tag = { end_tag : bool; name : string; mutable attributes : attribute list; mutable self_closing : bool } let emit_text ?(decode = true) (t : t) : unit = if Buffer.length t.text > 0 then ( let raw = Buffer.contents t.text in t.tokens <- Text (if decode then Entities.decode raw else raw) :: t.tokens; Buffer.clear t.text) let emit (t : t) (token : token) : unit = emit_text t; t.tokens <- token :: t.tokens let peek (t : t) (i : int) : char option = if i < t.n then Some t.s.[i] else None let is_letter (c : char) : bool = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') let is_space (c : char) : bool = c = ' ' || c = '\n' || c = '\t' || c = '\012' (* the index of the first character from [i] satisfying [p], or n *) let until (t : t) (i : int) (p : char -> bool) : int = let j = ref i in while !j < t.n && not (p t.s.[!j]) do incr j done; !j let skip_spaces (t : t) (i : int) : int = until t i (fun c -> not (is_space c)) (* the elements whose content is not HTML: no tags in it until their end * tag; RAWTEXT keeps the entities too, RCDATA decodes them *) let rawtext = [ "script"; "style" ] let rcdata = [ "title"; "textarea" ] (* [prefix] at [i], ignoring case *) let at (t : t) (i : int) (prefix : string) : bool = let n = String.length prefix in i + n <= t.n && String.lowercase_ascii (String.sub t.s i n) = prefix (*****************************************************************************) (* The states *) (*****************************************************************************) (* each state is a function of where the machine is reading; a * transition is a tail call *) let rec data (t : t) (i : int) : unit = if i >= t.n then emit_text t else if t.s.[i] = '<' then tag_open t i else ( Buffer.add_char t.text t.s.[i]; data t (i + 1)) (* at a '<' *) and tag_open (t : t) (i : int) : unit = match peek t (i + 1) with | Some c when is_letter c -> tag_name t ~end_tag:false (i + 1) | Some '/' -> end_tag_open t (i + 2) | Some '!' -> markup_declaration t (i + 2) | Some '?' -> bogus_comment t (i + 1) | _ -> (* a '<' that starts nothing: text *) Buffer.add_char t.text '<'; data t (i + 1) (* after "</" *) and end_tag_open (t : t) (i : int) : unit = match peek t i with | Some c when is_letter c -> tag_name t ~end_tag:true i | Some '>' -> (* "</>": nothing *) data t (i + 1) | None -> Buffer.add_string t.text "</"; emit_text t | Some _ -> bogus_comment t i and tag_name (t : t) ~(end_tag : bool) (i : int) : unit = let stop = until t i (fun c -> is_space c || c = '/' || c = '>') in let tag = { end_tag; name = String.lowercase_ascii (String.sub t.s i (stop - i)); attributes = []; self_closing = false } in before_attribute_name t tag stop and before_attribute_name (t : t) (tag : tag) (i : int) : unit = let i = skip_spaces t i in match peek t i with | None -> (* cut off by the end: dropped *) emit_text t | Some '>' -> emit_tag t tag (i + 1) | Some '/' -> self_closing_start_tag t tag i | Some _ -> attribute_name t tag i and attribute_name (t : t) (tag : tag) (i : int) : unit = (* a '=' first is part of the name, as the spec says *) let stop = until t (i + 1) (fun c -> is_space c || c = '/' || c = '>' || c = '=') in let name = String.lowercase_ascii (String.sub t.s i (stop - i)) in after_attribute_name t tag name stop and after_attribute_name (t : t) (tag : tag) (name : string) (i : int) : unit = let i = skip_spaces t i in match peek t i with | Some '=' -> before_attribute_value t tag name (i + 1) | _ -> (* no value: <hr noshade> *) add_attribute tag name ""; before_attribute_name t tag i and before_attribute_value (t : t) (tag : tag) (name : string) (i : int) : unit = let i = skip_spaces t i in match peek t i with | Some (('"' | '\'') as quote) -> ( let stop = until t (i + 1) (fun c -> c = quote) in match peek t stop with | None -> emit_text t (* cut off: dropped *) | Some _ -> add_attribute tag name (Entities.decode (String.sub t.s (i + 1) (stop - i - 1))); before_attribute_name t tag (stop + 1)) | Some '>' -> add_attribute tag name ""; emit_tag t tag (i + 1) | _ -> (* unquoted: up to a space or '>' *) let stop = until t i (fun c -> is_space c || c = '>') in add_attribute tag name (Entities.decode (String.sub t.s i (stop - i))); before_attribute_name t tag stop (* at a '/' in a tag: "/>" ends a self-closing one, another '/' is * nothing *) and self_closing_start_tag (t : t) (tag : tag) (i : int) : unit = match peek t (i + 1) with | Some '>' -> tag.self_closing <- true; emit_tag t tag (i + 2) | _ -> before_attribute_name t tag (i + 1) (* the tag read, [i] after its '>'; the elements whose content is not * HTML switch the machine to reading it as text *) and emit_tag (t : t) (tag : tag) (i : int) : unit = if tag.end_tag then ( emit t (End_tag tag.name); data t i) else ( (* claude: the names' origins (Dtd): a Netscape element keeps all * its attributes; a core one's Netscape attributes go apart *) let origin = Dtd.element_origin tag.name in let attributes, extensions = match origin with | Netscape -> (List.rev tag.attributes, []) | Core -> List.partition (fun a -> Dtd.attribute_origin tag.name a = Core) (List.rev tag.attributes) in emit t (Start_tag { name = tag.name; attributes; extensions; origin; self_closing = tag.self_closing }); if List.mem tag.name rawtext then raw_text t tag.name ~decode:false i else if List.mem tag.name rcdata then raw_text t tag.name ~decode:true i else data t i) (* RAWTEXT and RCDATA: text up to "</name" followed by a space, '/' or * '>', ignoring case *) and raw_text (t : t) (name : string) ~(decode : bool) (i : int) : unit = let rec find j = if j >= t.n then None else if t.s.[j] = '<' && at t (j + 1) ("/" ^ name) && match peek t (j + 2 + String.length name) with Some c -> is_space c || c = '/' || c = '>' | None -> false then Some j else find (j + 1) in match find i with | Some j -> Buffer.add_substring t.text t.s i (j - i); emit_text ~decode t; end_tag_open t (j + 2) | None -> Buffer.add_substring t.text t.s i (t.n - i); emit_text ~decode t (* after "<!" *) and markup_declaration (t : t) (i : int) : unit = if at t i "--" then comment t (i + 2) else if at t i "doctype" then doctype t (i + 7) else bogus_comment t i (* after "<!--" *) and comment (t : t) (i : int) : unit = if at t i ">" then ( emit t (Comment ""); data t (i + 1)) else if at t i "->" then ( emit t (Comment ""); data t (i + 2)) else let rec close j = if j + 3 > t.n then None else if at t j "-->" then Some j else close (j + 1) in match close i with | Some j -> emit t (Comment (String.sub t.s i (j - i))); data t (j + 3) | None -> emit t (Comment (String.sub t.s i (t.n - i))) and doctype (t : t) (i : int) : unit = let stop = until t i (fun c -> c = '>') in emit t (Doctype (String.trim (String.sub t.s i (stop - i)))); data t (stop + 1) (* "<?...>", "<!...>": up to the '>', kept as a comment *) and bogus_comment (t : t) (i : int) : unit = let stop = until t i (fun c -> c = '>') in emit t (Comment (String.sub t.s i (stop - i))); data t (stop + 1) (* a name given twice keeps its first value *) and add_attribute (tag : tag) (name : string) (value : string) : unit = if not (List.mem_assoc name tag.attributes) then tag.attributes <- (name, value) :: tag.attributes (*****************************************************************************) (* Entry points *) (*****************************************************************************) (* CR LF and a lone CR as LF: the spec's preprocessing of the input *) let normalize_newlines (s : string) : string = if not (String.contains s '\r') then s else let b = Buffer.create (String.length s) in String.iteri (fun i c -> if c = '\r' then (if not (i + 1 < String.length s && s.[i + 1] = '\n') then Buffer.add_char b '\n') else Buffer.add_char b c) s; Buffer.contents b let tokenize (text : string) : token list = let s = normalize_newlines text in let t = { s; n = String.length s; tokens = []; text = Buffer.create 256 } in data t 0; List.rev t.tokens let attribute (name : string) (attributes : attribute list) : string option = List.assoc_opt name attributes (* a string between double quotes, escaped as the notes write it: only * the quote, the backslash and the control characters (UTF-8 kept) *) let quote (s : string) : string = let b = Buffer.create (String.length s + 2) in Buffer.add_char b '"'; String.iter (fun c -> match c with | '"' -> Buffer.add_string b "\\\"" | '\\' -> Buffer.add_string b "\\\\" | '\n' -> Buffer.add_string b "\\n" | '\t' -> Buffer.add_string b "\\t" | c -> Buffer.add_char b c) s; Buffer.add_char b '"'; Buffer.contents b let to_string (token : token) : string = match token with | Doctype d -> "Doctype " ^ quote d | Start_tag { name; attributes; extensions; origin; self_closing } -> let list attributes = String.concat "; " (List.map (fun (n, v) -> n ^ " = " ^ quote v) attributes) in Printf.sprintf "Start_tag %s [%s]%s%s" (quote name) (list attributes) (match (origin, extensions) with | Netscape, _ -> " {Netscape}" | Core, [] -> "" | Core, extensions -> " {Netscape: " ^ list extensions ^ "}") (if self_closing then " /" else "") | End_tag name -> "End_tag " ^ quote name | Text s -> "Text " ^ quote s | Comment s -> "Comment " ^ quote s
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>