| 1 | # htmlparse.tcl -- |
|---|
| 2 | # |
|---|
| 3 | # This file implements a simple HTML parsing library in Tcl. |
|---|
| 4 | # It may take advantage of parsers coded in C in the future. |
|---|
| 5 | # |
|---|
| 6 | # The functionality here is a subset of the |
|---|
| 7 | # |
|---|
| 8 | # Simple HTML display library by Stephen Uhler (stephen.uhler@sun.com) |
|---|
| 9 | # Copyright (c) 1995 by Sun Microsystems |
|---|
| 10 | # Version 0.3 Fri Sep 1 10:47:17 PDT 1995 |
|---|
| 11 | # |
|---|
| 12 | # The main restriction is that all Tk-related code in the above |
|---|
| 13 | # was left out of the code here. It is expected that this code |
|---|
| 14 | # will go into a 'tklib' in the future. |
|---|
| 15 | # |
|---|
| 16 | # Copyright (c) 2001 by ActiveState Tool Corp. |
|---|
| 17 | # See the file license.terms. |
|---|
| 18 | |
|---|
| 19 | package require Tcl 8.2 |
|---|
| 20 | package require struct::stack |
|---|
| 21 | package require cmdline 1.1 |
|---|
| 22 | |
|---|
| 23 | namespace eval ::htmlparse { |
|---|
| 24 | namespace export \ |
|---|
| 25 | parse \ |
|---|
| 26 | debugCallback \ |
|---|
| 27 | mapEscapes \ |
|---|
| 28 | 2tree \ |
|---|
| 29 | removeVisualFluff \ |
|---|
| 30 | removeFormDefs |
|---|
| 31 | |
|---|
| 32 | # Table of escape characters. Maps from their names to the actual |
|---|
| 33 | # character. See http://htmlhelp.org/reference/html40/entities/ |
|---|
| 34 | |
|---|
| 35 | variable namedEntities |
|---|
| 36 | |
|---|
| 37 | # I. Latin-1 Entities (HTML 4.01) |
|---|
| 38 | array set namedEntities { |
|---|
| 39 | nbsp \xa0 iexcl \xa1 cent \xa2 pound \xa3 curren \xa4 |
|---|
| 40 | yen \xa5 brvbar \xa6 sect \xa7 uml \xa8 copy \xa9 |
|---|
| 41 | ordf \xaa laquo \xab not \xac shy \xad reg \xae |
|---|
| 42 | macr \xaf deg \xb0 plusmn \xb1 sup2 \xb2 sup3 \xb3 |
|---|
| 43 | acute \xb4 micro \xb5 para \xb6 middot \xb7 cedil \xb8 |
|---|
| 44 | sup1 \xb9 ordm \xba raquo \xbb frac14 \xbc frac12 \xbd |
|---|
| 45 | frac34 \xbe iquest \xbf Agrave \xc0 Aacute \xc1 Acirc \xc2 |
|---|
| 46 | Atilde \xc3 Auml \xc4 Aring \xc5 AElig \xc6 Ccedil \xc7 |
|---|
| 47 | Egrave \xc8 Eacute \xc9 Ecirc \xca Euml \xcb Igrave \xcc |
|---|
| 48 | Iacute \xcd Icirc \xce Iuml \xcf ETH \xd0 Ntilde \xd1 |
|---|
| 49 | Ograve \xd2 Oacute \xd3 Ocirc \xd4 Otilde \xd5 Ouml \xd6 |
|---|
| 50 | times \xd7 Oslash \xd8 Ugrave \xd9 Uacute \xda Ucirc \xdb |
|---|
| 51 | Uuml \xdc Yacute \xdd THORN \xde szlig \xdf agrave \xe0 |
|---|
| 52 | aacute \xe1 acirc \xe2 atilde \xe3 auml \xe4 aring \xe5 |
|---|
| 53 | aelig \xe6 ccedil \xe7 egrave \xe8 eacute \xe9 ecirc \xea |
|---|
| 54 | euml \xeb igrave \xec iacute \xed icirc \xee iuml \xef |
|---|
| 55 | eth \xf0 ntilde \xf1 ograve \xf2 oacute \xf3 ocirc \xf4 |
|---|
| 56 | otilde \xf5 ouml \xf6 divide \xf7 oslash \xf8 ugrave \xf9 |
|---|
| 57 | uacute \xfa ucirc \xfb uuml \xfc yacute \xfd thorn \xfe |
|---|
| 58 | yuml \xff |
|---|
| 59 | } |
|---|
| 60 | |
|---|
| 61 | # II. Entities for Symbols and Greek Letters (HTML 4.01) |
|---|
| 62 | array set namedEntities { |
|---|
| 63 | fnof \u192 Alpha \u391 Beta \u392 Gamma \u393 Delta \u394 |
|---|
| 64 | Epsilon \u395 Zeta \u396 Eta \u397 Theta \u398 Iota \u399 |
|---|
| 65 | Kappa \u39A Lambda \u39B Mu \u39C Nu \u39D Xi \u39E |
|---|
| 66 | Omicron \u39F Pi \u3A0 Rho \u3A1 Sigma \u3A3 Tau \u3A4 |
|---|
| 67 | Upsilon \u3A5 Phi \u3A6 Chi \u3A7 Psi \u3A8 Omega \u3A9 |
|---|
| 68 | alpha \u3B1 beta \u3B2 gamma \u3B3 delta \u3B4 epsilon \u3B5 |
|---|
| 69 | zeta \u3B6 eta \u3B7 theta \u3B8 iota \u3B9 kappa \u3BA |
|---|
| 70 | lambda \u3BB mu \u3BC nu \u3BD xi \u3BE omicron \u3BF |
|---|
| 71 | pi \u3C0 rho \u3C1 sigmaf \u3C2 sigma \u3C3 tau \u3C4 |
|---|
| 72 | upsilon \u3C5 phi \u3C6 chi \u3C7 psi \u3C8 omega \u3C9 |
|---|
| 73 | thetasym \u3D1 upsih \u3D2 piv \u3D6 bull \u2022 |
|---|
| 74 | hellip \u2026 prime \u2032 Prime \u2033 oline \u203E |
|---|
| 75 | frasl \u2044 weierp \u2118 image \u2111 real \u211C |
|---|
| 76 | trade \u2122 alefsym \u2135 larr \u2190 uarr \u2191 |
|---|
| 77 | rarr \u2192 darr \u2193 harr \u2194 crarr \u21B5 |
|---|
| 78 | lArr \u21D0 uArr \u21D1 rArr \u21D2 dArr \u21D3 hArr \u21D4 |
|---|
| 79 | forall \u2200 part \u2202 exist \u2203 empty \u2205 |
|---|
| 80 | nabla \u2207 isin \u2208 notin \u2209 ni \u220B prod \u220F |
|---|
| 81 | sum \u2211 minus \u2212 lowast \u2217 radic \u221A |
|---|
| 82 | prop \u221D infin \u221E ang \u2220 and \u2227 or \u2228 |
|---|
| 83 | cap \u2229 cup \u222A int \u222B there4 \u2234 sim \u223C |
|---|
| 84 | cong \u2245 asymp \u2248 ne \u2260 equiv \u2261 le \u2264 |
|---|
| 85 | ge \u2265 sub \u2282 sup \u2283 nsub \u2284 sube \u2286 |
|---|
| 86 | supe \u2287 oplus \u2295 otimes \u2297 perp \u22A5 |
|---|
| 87 | sdot \u22C5 lceil \u2308 rceil \u2309 lfloor \u230A |
|---|
| 88 | rfloor \u230B lang \u2329 rang \u232A loz \u25CA |
|---|
| 89 | spades \u2660 clubs \u2663 hearts \u2665 diams \u2666 |
|---|
| 90 | } |
|---|
| 91 | |
|---|
| 92 | # III. Special Entities (HTML 4.01) |
|---|
| 93 | array set namedEntities { |
|---|
| 94 | quot \x22 amp \x26 lt \x3C gt \x3E OElig \u152 oelig \u153 |
|---|
| 95 | Scaron \u160 scaron \u161 Yuml \u178 circ \u2C6 |
|---|
| 96 | tilde \u2DC ensp \u2002 emsp \u2003 thinsp \u2009 |
|---|
| 97 | zwnj \u200C zwj \u200D lrm \u200E rlm \u200F ndash \u2013 |
|---|
| 98 | mdash \u2014 lsquo \u2018 rsquo \u2019 sbquo \u201A |
|---|
| 99 | ldquo \u201C rdquo \u201D bdquo \u201E dagger \u2020 |
|---|
| 100 | Dagger \u2021 permil \u2030 lsaquo \u2039 rsaquo \u203A |
|---|
| 101 | euro \u20AC |
|---|
| 102 | } |
|---|
| 103 | |
|---|
| 104 | # Internal cache for the foreach variable-lists and the |
|---|
| 105 | # substitution strings used to split a HTML string into |
|---|
| 106 | # incrementally handleable scripts. This should reduce the |
|---|
| 107 | # time compute this information for repeated calls with the same |
|---|
| 108 | # split-factor. The array is indexed by a combination of the |
|---|
| 109 | # numerical split factor and the length of the command prefix and |
|---|
| 110 | # maps this to a 2-element list containing variable- and |
|---|
| 111 | # subst-string. |
|---|
| 112 | |
|---|
| 113 | variable splitdata |
|---|
| 114 | array set splitdata {} |
|---|
| 115 | |
|---|
| 116 | } |
|---|
| 117 | |
|---|
| 118 | # htmlparse::parse -- |
|---|
| 119 | # |
|---|
| 120 | # This command is the basic parser for HTML. It takes a HTML |
|---|
| 121 | # string, parses it and invokes a command prefix for every tag |
|---|
| 122 | # encountered. It is not necessary for the HTML to be valid for |
|---|
| 123 | # this parser to function. It is the responsibility of the |
|---|
| 124 | # command invoked for every tag to check this. Another |
|---|
| 125 | # responsibility of the invoked command is the handling of tag |
|---|
| 126 | # attributes and character entities (escaped characters). The |
|---|
| 127 | # parser provides the un-interpreted tag attributes to the |
|---|
| 128 | # invoked command to aid in the former, and the package at large |
|---|
| 129 | # provides a helper command, '::htmlparse::mapEscapes', to aid |
|---|
| 130 | # in the handling of the latter. The parser *does* ignore |
|---|
| 131 | # leading DOCTYPE declarations and all valid HTML comments it |
|---|
| 132 | # encounters. |
|---|
| 133 | # |
|---|
| 134 | # All information beyond the HTML string itself is specified via |
|---|
| 135 | # options, these are explained below. |
|---|
| 136 | # |
|---|
| 137 | # To help understanding the options some more background |
|---|
| 138 | # information about the parser. |
|---|
| 139 | # |
|---|
| 140 | # It is capable to detect incomplete tags in the HTML string |
|---|
| 141 | # given to it. Under normal circumstances this will cause the |
|---|
| 142 | # parser to throw an error, but if the option '-incvar' is used |
|---|
| 143 | # to specify a global (or namespace) variable the parser will |
|---|
| 144 | # store the incomplete part of the input into this variable |
|---|
| 145 | # instead. This will aid greatly in the handling of |
|---|
| 146 | # incrementally arriving HTML as the parser will handle whatever |
|---|
| 147 | # he can and defer the handling of the incomplete part until |
|---|
| 148 | # more data has arrived. |
|---|
| 149 | # |
|---|
| 150 | # Another feature of the parser are its two possible modes of |
|---|
| 151 | # operation. The normal mode is activated if the option '-queue' |
|---|
| 152 | # is not present on the command line invoking the parser. If it |
|---|
| 153 | # is present the parser will go into the incremental mode instead. |
|---|
| 154 | # |
|---|
| 155 | # The main difference is that a parser in normal mode will |
|---|
| 156 | # immediately invoke the command prefix for each tag it |
|---|
| 157 | # encounters. In incremental mode however the parser will |
|---|
| 158 | # generate a number of scripts which invoke the command prefix |
|---|
| 159 | # for groups of tags in the HTML string and then store these |
|---|
| 160 | # scripts in the specified queue. It is then the responsibility |
|---|
| 161 | # of the caller of the parser to ensure the execution of the |
|---|
| 162 | # scripts in the queue. |
|---|
| 163 | # |
|---|
| 164 | # Note: The queue objecct given to the parser has to provide the |
|---|
| 165 | # same interface as the queue defined in tcllib -> struct. This |
|---|
| 166 | # does for example mean that all queues created via that part of |
|---|
| 167 | # tcllib can be immediately used here. Still, the queue doesn't |
|---|
| 168 | # have to come from tcllib -> struct as long as the same |
|---|
| 169 | # interface is provided. |
|---|
| 170 | # |
|---|
| 171 | # In both modes the parser will return an empty string to the |
|---|
| 172 | # caller. |
|---|
| 173 | # |
|---|
| 174 | # To a parser in incremental mode the option '-split' can be |
|---|
| 175 | # given and will specify the size of the groups he creates. In |
|---|
| 176 | # other words, -split 5 means that each of the generated scripts |
|---|
| 177 | # will invoke the command prefix for 5 consecutive tags in the |
|---|
| 178 | # HTML string. A parser in normal mode will ignore this option |
|---|
| 179 | # and its value. |
|---|
| 180 | # |
|---|
| 181 | # The option '-vroot' specifies a virtual root tag. A parser in |
|---|
| 182 | # normal mode will invoke the command prefix for it immediately |
|---|
| 183 | # before and after he processes the tags in the HTML, thus |
|---|
| 184 | # simulating that the HTML string is enclosed in a <vroot> |
|---|
| 185 | # </vroot> combination. In incremental mode however the parser |
|---|
| 186 | # is unable to provide the closing virtual root as he never |
|---|
| 187 | # knows when the input is complete. In this case the first |
|---|
| 188 | # script generated by each invocation of the parser will contain |
|---|
| 189 | # an invocation of the command prefix for the virtual root as |
|---|
| 190 | # its first command. |
|---|
| 191 | # |
|---|
| 192 | # Interface to the command prefix: |
|---|
| 193 | # |
|---|
| 194 | # In normal mode the parser will invoke the command prefix with |
|---|
| 195 | # for arguments appended. See '::htmlparse::debugCallback' for a |
|---|
| 196 | # description. In incremental mode however the generated scripts |
|---|
| 197 | # will invoke the command prefix with five arguments |
|---|
| 198 | # appended. The last four of these are the same which were |
|---|
| 199 | # mentioned above. The first however is a placeholder string |
|---|
| 200 | # (\win\) for a clientdata value to be supplied later during the |
|---|
| 201 | # actual execution of the generated scripts. This could be a tk |
|---|
| 202 | # window path, for example. This allows the user of this package |
|---|
| 203 | # to preprocess HTML strings without commiting them to a |
|---|
| 204 | # specific window, object, whatever during parsing. This |
|---|
| 205 | # connection can be made later. This also means that it is |
|---|
| 206 | # possible to cache preprocessed HTML. Of course, nothing |
|---|
| 207 | # prevents the user of the parser to replace the placeholder |
|---|
| 208 | # with an empty string. |
|---|
| 209 | # |
|---|
| 210 | # Arguments: |
|---|
| 211 | # args An option/value-list followed by the string to |
|---|
| 212 | # parse. Available options are: |
|---|
| 213 | # |
|---|
| 214 | # -cmd The command prefix to invoke for every tag in |
|---|
| 215 | # the HTML string. Defaults to |
|---|
| 216 | # '::htmlparse::debugCallback'. |
|---|
| 217 | # |
|---|
| 218 | # -vroot The virtual root tag to add around the HTML in |
|---|
| 219 | # normal mode. In incremental mode it is the |
|---|
| 220 | # first tag in each chunk processed by the |
|---|
| 221 | # parser, but there will be no closing tags. |
|---|
| 222 | # Defaults to 'hmstart'. |
|---|
| 223 | # |
|---|
| 224 | # -split The size of the groups produced by an |
|---|
| 225 | # incremental mode parser. Ignored when in |
|---|
| 226 | # normal mode. Defaults to 10. Values <= 0 are |
|---|
| 227 | # not allowed. |
|---|
| 228 | # |
|---|
| 229 | # -incvar The name of the variable where to store any |
|---|
| 230 | # incomplete HTML into. Optional. |
|---|
| 231 | # |
|---|
| 232 | # -queue |
|---|
| 233 | # The handle/name of the queue objecct to store |
|---|
| 234 | # the generated scripts into. Activates |
|---|
| 235 | # incremental mode. Normal mode is used if this |
|---|
| 236 | # option is not present. |
|---|
| 237 | # |
|---|
| 238 | # After the option the command explect a single argument |
|---|
| 239 | # containing the HTML string to parse. |
|---|
| 240 | # |
|---|
| 241 | # Side Effects: |
|---|
| 242 | # In normal mode as of the invoked command. Else none. |
|---|
| 243 | # |
|---|
| 244 | # Results: |
|---|
| 245 | # None. |
|---|
| 246 | |
|---|
| 247 | proc ::htmlparse::parse {args} { |
|---|
| 248 | # Convert the HTML string into a evaluable command sequence. |
|---|
| 249 | |
|---|
| 250 | variable splitdata |
|---|
| 251 | |
|---|
| 252 | # Option processing, start with the defaults, then run through the |
|---|
| 253 | # list of arguments. |
|---|
| 254 | |
|---|
| 255 | set cmd ::htmlparse::debugCallback |
|---|
| 256 | set vroot hmstart |
|---|
| 257 | set incvar "" |
|---|
| 258 | set split 10 |
|---|
| 259 | set queue "" |
|---|
| 260 | |
|---|
| 261 | while {[set err [cmdline::getopt args {cmd.arg vroot.arg incvar.arg split.arg queue.arg} opt arg]]} { |
|---|
| 262 | if {$err < 0} { |
|---|
| 263 | return -code error "::htmlparse::parse : $arg" |
|---|
| 264 | } |
|---|
| 265 | switch -exact -- $opt { |
|---|
| 266 | cmd - |
|---|
| 267 | vroot - |
|---|
| 268 | incvar - |
|---|
| 269 | queue { |
|---|
| 270 | if {[string length $arg] == 0} { |
|---|
| 271 | return -code error "::htmlparse::parse : -$opt illegal argument (empty)" |
|---|
| 272 | } |
|---|
| 273 | # Each option has an variable with the same name associated with it. |
|---|
| 274 | # FRINK: nocheck |
|---|
| 275 | set $opt $arg |
|---|
| 276 | } |
|---|
| 277 | split { |
|---|
| 278 | if {$arg <= 0} { |
|---|
| 279 | return -code error "::htmlparse::parse : -split illegal argument (<= 0)" |
|---|
| 280 | } |
|---|
| 281 | set split $arg |
|---|
| 282 | } |
|---|
| 283 | default {# Can't happen} |
|---|
| 284 | } |
|---|
| 285 | } |
|---|
| 286 | |
|---|
| 287 | if {[llength $args] > 1} { |
|---|
| 288 | return -code error "::htmlparse::parse : to many arguments behind the options, expected one" |
|---|
| 289 | } |
|---|
| 290 | if {[llength $args] < 1} { |
|---|
| 291 | return -code error "::htmlparse::parse : html string missing" |
|---|
| 292 | } |
|---|
| 293 | |
|---|
| 294 | set html [PrepareHtml [lindex $args 0]] |
|---|
| 295 | |
|---|
| 296 | # Look for incomplete HTML from the last iteration and prepend it |
|---|
| 297 | # to the input we just got. |
|---|
| 298 | |
|---|
| 299 | if {$incvar != {}} { |
|---|
| 300 | upvar $incvar incomplete |
|---|
| 301 | } else { |
|---|
| 302 | set incomplete "" |
|---|
| 303 | } |
|---|
| 304 | |
|---|
| 305 | if {[catch {set new $incomplete$html}]} {set new $html} |
|---|
| 306 | set html $new |
|---|
| 307 | |
|---|
| 308 | # Handle incomplete HTML (Recognize incomplete tag at end, buffer |
|---|
| 309 | # it up for the next call). |
|---|
| 310 | |
|---|
| 311 | set end [lindex \{$html\} end] |
|---|
| 312 | if {[set idx [string last < $end]] > [string last > $end]} { |
|---|
| 313 | |
|---|
| 314 | if {$incvar == {}} { |
|---|
| 315 | return -code error "::htmlparse::parse : HTML is incomplete, option -incvar is missing" |
|---|
| 316 | } |
|---|
| 317 | |
|---|
| 318 | # upvar $incvar incomplete -- Already done, s.a. |
|---|
| 319 | set incomplete [string range $end $idx end] |
|---|
| 320 | incr idx -1 |
|---|
| 321 | set html [string range $end 0 $idx] |
|---|
| 322 | |
|---|
| 323 | } else { |
|---|
| 324 | set incomplete "" |
|---|
| 325 | } |
|---|
| 326 | |
|---|
| 327 | # Convert the HTML string into a script. |
|---|
| 328 | |
|---|
| 329 | set sub "\}\n$cmd {\\1} {} {\\2} \{\}\n$cmd {\\1} {/} {} \{" |
|---|
| 330 | regsub -all -- {<([^\s>]+)\s*([^>]*)/>} $html $sub html |
|---|
| 331 | |
|---|
| 332 | set sub "\}\n$cmd {\\2} {\\1} {\\3} \{" |
|---|
| 333 | regsub -all -- {<(/?)([^\s>]+)\s*([^>]*)>} $html $sub html |
|---|
| 334 | |
|---|
| 335 | # The value of queue now determines wether we process the HTML by |
|---|
| 336 | # ourselves (queue is empty) or if we generate a list of scripts |
|---|
| 337 | # each of which processes n tags, n the argument to -split. |
|---|
| 338 | |
|---|
| 339 | if {$queue == {}} { |
|---|
| 340 | # And evaluate it. This is the main parsing step. |
|---|
| 341 | |
|---|
| 342 | eval "$cmd {$vroot} {} {} \{$html\}" |
|---|
| 343 | eval "$cmd {$vroot} / {} {}" |
|---|
| 344 | } else { |
|---|
| 345 | # queue defined, generate list of scripts doing small chunks of tags. |
|---|
| 346 | |
|---|
| 347 | set lcmd [llength $cmd] |
|---|
| 348 | set key $split,$lcmd |
|---|
| 349 | |
|---|
| 350 | if {![info exists splitdata($key)]} { |
|---|
| 351 | for {set i 0; set group {}} {$i < $split} {incr i} { |
|---|
| 352 | # Use the length of the command prefix to generate |
|---|
| 353 | # additional variables before the main variable after |
|---|
| 354 | # which the placeholder will be inserted. |
|---|
| 355 | |
|---|
| 356 | for {set j 1} {$j < $lcmd} {incr j} { |
|---|
| 357 | append group "b${j}_$i " |
|---|
| 358 | } |
|---|
| 359 | |
|---|
| 360 | append group "a$i c$i d$i e$i f$i\n" |
|---|
| 361 | } |
|---|
| 362 | regsub -all -- {(a[0-9]+)} $group {{$\1} \\\\win\\\\} subgroup |
|---|
| 363 | regsub -all -- {([b-z_0-9]+[0-9]+)} $subgroup {{$\1}} subgroup |
|---|
| 364 | |
|---|
| 365 | set splitdata($key) [list $group $subgroup] |
|---|
| 366 | } |
|---|
| 367 | |
|---|
| 368 | foreach {group subgroup} $splitdata($key) break ; # lassign |
|---|
| 369 | foreach $group "$cmd {$vroot} {} {} \{$html\}" { |
|---|
| 370 | $queue put [string trimright [subst $subgroup]] |
|---|
| 371 | } |
|---|
| 372 | } |
|---|
| 373 | return |
|---|
| 374 | } |
|---|
| 375 | |
|---|
| 376 | # htmlparse::PrepareHtml -- |
|---|
| 377 | # |
|---|
| 378 | # Internal helper command of '::htmlparse::parse'. Removes |
|---|
| 379 | # leading DOCTYPE declarations and comments, protects the |
|---|
| 380 | # special characters of tcl from evaluation. |
|---|
| 381 | # |
|---|
| 382 | # Arguments: |
|---|
| 383 | # html The HTML string to prepare |
|---|
| 384 | # |
|---|
| 385 | # Side Effects: |
|---|
| 386 | # None. |
|---|
| 387 | # |
|---|
| 388 | # Results: |
|---|
| 389 | # The provided HTML string with the described modifications |
|---|
| 390 | # applied to it. |
|---|
| 391 | |
|---|
| 392 | proc ::htmlparse::PrepareHtml {html} { |
|---|
| 393 | # Remove the following items from the text: |
|---|
| 394 | # - A leading <!DOCTYPE...> declaration. |
|---|
| 395 | # - All comments <!-- ... --> |
|---|
| 396 | # |
|---|
| 397 | # Also normalize the line endings (\r -> \n). |
|---|
| 398 | |
|---|
| 399 | # Tcllib SF Bug 861287 - Processing of comments. |
|---|
| 400 | # Recognize EOC by RE, instead of fixed string. |
|---|
| 401 | |
|---|
| 402 | set html [string map [list \r \n] $html] |
|---|
| 403 | |
|---|
| 404 | regsub -- "^.*<!DOCTYPE\[^>\]*>" $html {} html |
|---|
| 405 | regsub -all -- "--(\[ \t\n\]*)>" $html "\001\\1\002" html |
|---|
| 406 | |
|---|
| 407 | # Recognize borken beginnings of a comment and convert them to PCDATA. |
|---|
| 408 | regsub -all -- "<--(\[^\001\]*)\001(\[^\002\]*)\002" $html {\<--\1--\2\>} html |
|---|
| 409 | |
|---|
| 410 | # And now recognize true comments, remove them. |
|---|
| 411 | regsub -all -- "<!--\[^\001\]*\001(\[^\002\]*)\002" $html {} html |
|---|
| 412 | |
|---|
| 413 | # Protect characters special to tcl (braces, slashes) by |
|---|
| 414 | # converting them to their escape sequences. |
|---|
| 415 | |
|---|
| 416 | return [string map [list \ |
|---|
| 417 | "\{" "{" \ |
|---|
| 418 | "\}" "}" \ |
|---|
| 419 | "\\" "\"] $html] |
|---|
| 420 | } |
|---|
| 421 | |
|---|
| 422 | |
|---|
| 423 | |
|---|
| 424 | # htmlparse::debugCallback -- |
|---|
| 425 | # |
|---|
| 426 | # The standard callback used by the parser in |
|---|
| 427 | # '::htmlparse::parse' if none was specified by the user. Simply |
|---|
| 428 | # dumps its arguments to stdout. This callback can be used for |
|---|
| 429 | # both normal and incremental mode of the calling parser. In |
|---|
| 430 | # other words, it accepts four or five arguments. The last four |
|---|
| 431 | # arguments are described below. The optional fifth argument |
|---|
| 432 | # contains the clientdata value given to the callback by a |
|---|
| 433 | # parser in incremental mode. All callbacks have to follow the |
|---|
| 434 | # signature of this command in the last four arguments, and |
|---|
| 435 | # callbacks used in incremental parsing have to follow this |
|---|
| 436 | # signature in the last five arguments. |
|---|
| 437 | # |
|---|
| 438 | # Arguments: |
|---|
| 439 | # tag The name of the tag currently |
|---|
| 440 | # processed by the parser. |
|---|
| 441 | # |
|---|
| 442 | # slash Either empty or a slash. Allows us to |
|---|
| 443 | # distinguish between opening (slash is |
|---|
| 444 | # empty) and closing tags (slash is |
|---|
| 445 | # equal to a '/'). |
|---|
| 446 | # |
|---|
| 447 | # param The un-interpreted list of parameters |
|---|
| 448 | # to the tag. |
|---|
| 449 | # |
|---|
| 450 | # textBehindTheTag The text found by the parser behind |
|---|
| 451 | # the tag named in 'tag'. |
|---|
| 452 | # |
|---|
| 453 | # Side Effects: |
|---|
| 454 | # None. |
|---|
| 455 | # |
|---|
| 456 | # Results: |
|---|
| 457 | # None. |
|---|
| 458 | |
|---|
| 459 | proc ::htmlparse::debugCallback {args} { |
|---|
| 460 | # args = ?clientData? tag slash param textBehindTheTag |
|---|
| 461 | puts "==> $args" |
|---|
| 462 | return |
|---|
| 463 | } |
|---|
| 464 | |
|---|
| 465 | # htmlparse::mapEscapes -- |
|---|
| 466 | # |
|---|
| 467 | # Takes a HTML string, substitutes all escape sequences with |
|---|
| 468 | # their actual characters and returns the resulting string. |
|---|
| 469 | # HTML not containing escape sequences or invalid escape |
|---|
| 470 | # sequences is returned unchanged. |
|---|
| 471 | # |
|---|
| 472 | # Arguments: |
|---|
| 473 | # html The string to modify |
|---|
| 474 | # |
|---|
| 475 | # Side Effects: |
|---|
| 476 | # None. |
|---|
| 477 | # |
|---|
| 478 | # Results: |
|---|
| 479 | # The argument string with all escape sequences replaced with |
|---|
| 480 | # their actual characters. |
|---|
| 481 | |
|---|
| 482 | proc ::htmlparse::mapEscapes {html} { |
|---|
| 483 | # Find HTML escape characters of the form &xxx(;|EOW) |
|---|
| 484 | |
|---|
| 485 | # Quote special Tcl chars so they pass through [subst] unharmed. |
|---|
| 486 | set new [string map [list \] \\\] \[ \\\[ \$ \\\$ \\ \\\\] $html] |
|---|
| 487 | regsub -all -- {&([[:alnum:]]{2,7})(;|\M)} $new {[DoNamedMap \1 {\2}]} new |
|---|
| 488 | regsub -all -- {&#([[:digit:]]{1,5})(;|\M)} $new {[DoDecMap \1 {\2}]} new |
|---|
| 489 | regsub -all -- {&#x([[:xdigit:]]{1,4})(;|\M)} $new {[DoHexMap \1 {\2}]} new |
|---|
| 490 | return [subst $new] |
|---|
| 491 | } |
|---|
| 492 | |
|---|
| 493 | proc ::htmlparse::DoNamedMap {name endOf} { |
|---|
| 494 | variable namedEntities |
|---|
| 495 | if {[info exist namedEntities($name)]} { |
|---|
| 496 | return $namedEntities($name) |
|---|
| 497 | } else { |
|---|
| 498 | # Put it back.. |
|---|
| 499 | return "&$name$endOf" |
|---|
| 500 | } |
|---|
| 501 | } |
|---|
| 502 | |
|---|
| 503 | proc ::htmlparse::DoDecMap {dec endOf} { |
|---|
| 504 | scan $dec %d dec |
|---|
| 505 | if {$dec <= 0xFFFD} { |
|---|
| 506 | return [format %c $dec] |
|---|
| 507 | } else { |
|---|
| 508 | # Put it back.. |
|---|
| 509 | return "&#$dec$endOf" |
|---|
| 510 | } |
|---|
| 511 | } |
|---|
| 512 | |
|---|
| 513 | proc ::htmlparse::DoHexMap {hex endOf} { |
|---|
| 514 | scan $hex %x value |
|---|
| 515 | if {$value <= 0xFFFD} { |
|---|
| 516 | return [format %c $value] |
|---|
| 517 | } else { |
|---|
| 518 | # Put it back.. |
|---|
| 519 | return "&#x$hex$endOf" |
|---|
| 520 | } |
|---|
| 521 | } |
|---|
| 522 | |
|---|
| 523 | # htmlparse::2tree -- |
|---|
| 524 | # |
|---|
| 525 | # This command is a wrapper around '::htmlparse::parse' which |
|---|
| 526 | # takes a HTML string and converts it into a tree containing the |
|---|
| 527 | # logical structure of the parsed document. The tree object has |
|---|
| 528 | # to be created by the caller. It is also expected that the tree |
|---|
| 529 | # object provides the same interface as the tree object from |
|---|
| 530 | # tcllib -> struct. It doesn't have to come from that module |
|---|
| 531 | # though. The internal callback does some basic checking of HTML |
|---|
| 532 | # validity and tries to recover from the most basic errors. |
|---|
| 533 | # |
|---|
| 534 | # Arguments: |
|---|
| 535 | # html The HTML string to parse and convert. |
|---|
| 536 | # tree The name of the tree to fill. |
|---|
| 537 | # |
|---|
| 538 | # Side Effects: |
|---|
| 539 | # Creates a tree object (see tcllib -> struct) |
|---|
| 540 | # and modifies it. |
|---|
| 541 | # |
|---|
| 542 | # Results: |
|---|
| 543 | # The contents of 'tree'. |
|---|
| 544 | |
|---|
| 545 | proc ::htmlparse::2tree {html tree} { |
|---|
| 546 | |
|---|
| 547 | # One internal datastructure is required, a stack of open |
|---|
| 548 | # tags. This stack is also provided by the 'struct' module of |
|---|
| 549 | # tcllib. As the operation of this command is synchronuous we |
|---|
| 550 | # don't have to take care against multiple running copies at the |
|---|
| 551 | # same times (Such are possible, but will be in different |
|---|
| 552 | # interpreters and true concurrency is possible only if they are |
|---|
| 553 | # in different threads too). IOW, no need for tricks to make the |
|---|
| 554 | # internal datastructure unique. |
|---|
| 555 | |
|---|
| 556 | catch {::htmlparse::tags destroy} |
|---|
| 557 | |
|---|
| 558 | ::struct::stack ::htmlparse::tags |
|---|
| 559 | ::htmlparse::tags push root |
|---|
| 560 | $tree set root type root |
|---|
| 561 | |
|---|
| 562 | parse -cmd [list ::htmlparse::2treeCallback $tree] $html |
|---|
| 563 | |
|---|
| 564 | # A bit hackish, correct the ordering of nodes for the optional |
|---|
| 565 | # tag types, over a larger area when was seen by the parser itself. |
|---|
| 566 | |
|---|
| 567 | $tree walk root -order post n { |
|---|
| 568 | ::htmlparse::Reorder $tree $n |
|---|
| 569 | } |
|---|
| 570 | |
|---|
| 571 | ::htmlparse::tags destroy |
|---|
| 572 | return $tree |
|---|
| 573 | } |
|---|
| 574 | |
|---|
| 575 | # htmlparse::2treeCallback -- |
|---|
| 576 | # |
|---|
| 577 | # Internal helper command. A special callback to |
|---|
| 578 | # '::htmlparse::parse' used by '::htmlparse::2tree' which takes |
|---|
| 579 | # the incoming stream of tags and converts them into a tree |
|---|
| 580 | # representing the inner structure of the parsed HTML |
|---|
| 581 | # document. Recovers from simple HTML errors like missing |
|---|
| 582 | # opening tags, missing closing tags and overlapping tags. |
|---|
| 583 | # |
|---|
| 584 | # Arguments: |
|---|
| 585 | # tree The name of the tree to manipulate. |
|---|
| 586 | # tag See '::htmlparse::debugCallback'. |
|---|
| 587 | # slash See '::htmlparse::debugCallback'. |
|---|
| 588 | # param See '::htmlparse::debugCallback'. |
|---|
| 589 | # textBehindTheTag See '::htmlparse::debugCallback'. |
|---|
| 590 | # |
|---|
| 591 | # Side Effects: |
|---|
| 592 | # Manipulates the tree object whose name was given as the first |
|---|
| 593 | # argument. |
|---|
| 594 | # |
|---|
| 595 | # Results: |
|---|
| 596 | # None. |
|---|
| 597 | |
|---|
| 598 | proc ::htmlparse::2treeCallback {tree tag slash param textBehindTheTag} { |
|---|
| 599 | # This could be table-driven I think but for now the switches |
|---|
| 600 | # should work fine. |
|---|
| 601 | |
|---|
| 602 | # Normalize tag information for later comparisons. Also remove |
|---|
| 603 | # superfluous whitespace. Don't forget to decode the standard |
|---|
| 604 | # entities. |
|---|
| 605 | |
|---|
| 606 | set tag [string tolower $tag] |
|---|
| 607 | set textBehindTheTag [string trim $textBehindTheTag] |
|---|
| 608 | if {$textBehindTheTag != {}} { |
|---|
| 609 | set text [mapEscapes $textBehindTheTag] |
|---|
| 610 | } |
|---|
| 611 | |
|---|
| 612 | if {"$slash" == "/"} { |
|---|
| 613 | # Handle closing tags. Standard operation is to pop the tag |
|---|
| 614 | # from the stack of open tags. We don't do this for </p> and |
|---|
| 615 | # </li>. As they were optional they were never pushed onto the |
|---|
| 616 | # stack (Well, actually they are just popped immediately after |
|---|
| 617 | # they were pusheed, see below). |
|---|
| 618 | |
|---|
| 619 | switch -exact -- $tag { |
|---|
| 620 | base - option - meta - li - p { |
|---|
| 621 | # Ignore, nothing to do. |
|---|
| 622 | } |
|---|
| 623 | default { |
|---|
| 624 | # The moment we get a closing tag which does not match |
|---|
| 625 | # the tag on the stack we have two possibilities on how |
|---|
| 626 | # this came into existence to choose from: |
|---|
| 627 | # |
|---|
| 628 | # a) A tag is now closed but was never opened. |
|---|
| 629 | # b) A tag requiring an end tag was opened but the end |
|---|
| 630 | # tag was omitted and we now are at a tag which was |
|---|
| 631 | # opened before the one with the omitted end tag. |
|---|
| 632 | |
|---|
| 633 | # NOTE: |
|---|
| 634 | # Pages delivered from the amazon.uk site contain both |
|---|
| 635 | # cases: </a> without opening, <b> & <font> without |
|---|
| 636 | # closing. Another error: <a><b></a></b>, i.e. overlapping |
|---|
| 637 | # tags. Fortunately this can be handled by the algorithm |
|---|
| 638 | # below, in two cycles, one of which is case (b), followed |
|---|
| 639 | # by case (a). It seems as if Amazon/UK believes that visual |
|---|
| 640 | # markup like <b> and <font> is an option (switch-on) instead |
|---|
| 641 | # of a region. |
|---|
| 642 | |
|---|
| 643 | # Algorithm used here to deal with these: |
|---|
| 644 | # 1) Search whole stack for the matching opening tag. |
|---|
| 645 | # If there is one assume case (b) and pop everything |
|---|
| 646 | # until and including this opening tag. |
|---|
| 647 | # 2) If no matching opening tag was found assume case |
|---|
| 648 | # (a) and ignore the tag. |
|---|
| 649 | # |
|---|
| 650 | # Part (1) also subsumes the normal case, i.e. the |
|---|
| 651 | # matching tag is at the top of the stack. |
|---|
| 652 | |
|---|
| 653 | set nodes [::htmlparse::tags peek [::htmlparse::tags size]] |
|---|
| 654 | # Note: First item is top of stack, last item is bottom of stack ! |
|---|
| 655 | # (This behaviour of tcllib stacks is not documented |
|---|
| 656 | # -> we should update the manpage). |
|---|
| 657 | |
|---|
| 658 | #foreach n $nodes {lappend tstring [p get $n -key type]} |
|---|
| 659 | #puts stderr --[join $tstring]-- |
|---|
| 660 | |
|---|
| 661 | set level 1 |
|---|
| 662 | set found 0 |
|---|
| 663 | foreach n $nodes { |
|---|
| 664 | set type [$tree get $n type] |
|---|
| 665 | if {0 == [string compare $tag $type]} { |
|---|
| 666 | # Found an earlier open tag -> (b). |
|---|
| 667 | set found 1 |
|---|
| 668 | break |
|---|
| 669 | } |
|---|
| 670 | incr level |
|---|
| 671 | } |
|---|
| 672 | if {$found} { |
|---|
| 673 | ::htmlparse::tags pop $level |
|---|
| 674 | if {$level > 1} { |
|---|
| 675 | #foreach n $nodes {lappend tstring [$tree get $n type]} |
|---|
| 676 | #puts stderr "\tdesync at <$tag> ($tstring) => pop $level" |
|---|
| 677 | } |
|---|
| 678 | } else { |
|---|
| 679 | #foreach n $nodes {lappend tstring [$tree get $n type]} |
|---|
| 680 | #puts stderr "\tdesync at <$tag> ($tstring) => ignore" |
|---|
| 681 | } |
|---|
| 682 | } |
|---|
| 683 | } |
|---|
| 684 | |
|---|
| 685 | # If there is text behind a closing tag X it belongs to the |
|---|
| 686 | # parent tag of X. |
|---|
| 687 | |
|---|
| 688 | if {$textBehindTheTag != {}} { |
|---|
| 689 | # Attach the text behind the closing tag to the reopened |
|---|
| 690 | # context. |
|---|
| 691 | |
|---|
| 692 | set pcd [$tree insert [::htmlparse::tags peek] end] |
|---|
| 693 | $tree set $pcd type PCDATA |
|---|
| 694 | $tree set $pcd data $textBehindTheTag |
|---|
| 695 | } |
|---|
| 696 | |
|---|
| 697 | } else { |
|---|
| 698 | # Handle opening tags. The standard operation for most is to |
|---|
| 699 | # push them onto the stack and thus open a nested context. |
|---|
| 700 | # This does not happen for both the optional tags (p, li) and |
|---|
| 701 | # the ones which don't have closing tags (meta, br, option, |
|---|
| 702 | # input, area, img). |
|---|
| 703 | # |
|---|
| 704 | # The text coming with the tag will be added after the tag if |
|---|
| 705 | # it is a tag without a matching close, else it will be added |
|---|
| 706 | # as a node below the tag (as it is the region between the |
|---|
| 707 | # opening and closing tag and thus nested inside). Empty text |
|---|
| 708 | # is ignored under all circcumstances. |
|---|
| 709 | |
|---|
| 710 | set node [$tree insert [::htmlparse::tags peek] end] |
|---|
| 711 | $tree set $node type $tag |
|---|
| 712 | $tree set $node data $param |
|---|
| 713 | |
|---|
| 714 | if {$textBehindTheTag != {}} { |
|---|
| 715 | switch -exact -- $tag { |
|---|
| 716 | input - area - img - br { |
|---|
| 717 | set pcd [$tree insert [::htmlparse::tags peek] end] |
|---|
| 718 | } |
|---|
| 719 | default { |
|---|
| 720 | set pcd [$tree insert $node end] |
|---|
| 721 | } |
|---|
| 722 | } |
|---|
| 723 | $tree set $pcd type PCDATA |
|---|
| 724 | $tree set $pcd data $textBehindTheTag |
|---|
| 725 | } |
|---|
| 726 | |
|---|
| 727 | ::htmlparse::tags push $node |
|---|
| 728 | |
|---|
| 729 | # Special handling: <p>, <li> may have no closing tag => pop |
|---|
| 730 | # : them immediately. |
|---|
| 731 | # |
|---|
| 732 | # Special handling: <meta>, <br>, <option>, <input>, <area>, |
|---|
| 733 | # : <img>: no closing tags for these. |
|---|
| 734 | |
|---|
| 735 | switch -exact -- $tag { |
|---|
| 736 | hr - base - meta - li - br - option - input - area - img - p - h1 - h2 - h3 - h4 - h5 - h6 { |
|---|
| 737 | ::htmlparse::tags pop |
|---|
| 738 | } |
|---|
| 739 | default {} |
|---|
| 740 | } |
|---|
| 741 | } |
|---|
| 742 | } |
|---|
| 743 | |
|---|
| 744 | # htmlparse::removeVisualFluff -- |
|---|
| 745 | # |
|---|
| 746 | # This command walks a tree as generated by '::htmlparse::2tree' |
|---|
| 747 | # and removes all the nodes which represent visual tags and not |
|---|
| 748 | # structural ones. The purpose of the command is to make the |
|---|
| 749 | # tree easier to navigate without getting bogged down in visual |
|---|
| 750 | # information not relevant to the search. |
|---|
| 751 | # |
|---|
| 752 | # Arguments: |
|---|
| 753 | # tree The name of the tree to cut down. |
|---|
| 754 | # |
|---|
| 755 | # Side Effects: |
|---|
| 756 | # Modifies the specified tree. |
|---|
| 757 | # |
|---|
| 758 | # Results: |
|---|
| 759 | # None. |
|---|
| 760 | |
|---|
| 761 | proc ::htmlparse::removeVisualFluff {tree} { |
|---|
| 762 | $tree walk root -order post n { |
|---|
| 763 | ::htmlparse::RemoveVisualFluff $tree $n |
|---|
| 764 | } |
|---|
| 765 | return |
|---|
| 766 | } |
|---|
| 767 | |
|---|
| 768 | # htmlparse::removeFormDefs -- |
|---|
| 769 | # |
|---|
| 770 | # Like '::htmlparse::removeVisualFluff' this command is here to |
|---|
| 771 | # cut down on the size of the tree as generated by |
|---|
| 772 | # '::htmlparse::2tree'. It removes all nodes representing forms |
|---|
| 773 | # and form elements. |
|---|
| 774 | # |
|---|
| 775 | # Arguments: |
|---|
| 776 | # tree The name of the tree to cut down. |
|---|
| 777 | # |
|---|
| 778 | # Side Effects: |
|---|
| 779 | # Modifies the specified tree. |
|---|
| 780 | # |
|---|
| 781 | # Results: |
|---|
| 782 | # None. |
|---|
| 783 | |
|---|
| 784 | proc ::htmlparse::removeFormDefs {tree} { |
|---|
| 785 | $tree walk root -order post n { |
|---|
| 786 | ::htmlparse::RemoveFormDefs $tree $n |
|---|
| 787 | } |
|---|
| 788 | return |
|---|
| 789 | } |
|---|
| 790 | |
|---|
| 791 | # htmlparse::RemoveVisualFluff -- |
|---|
| 792 | # |
|---|
| 793 | # Internal helper command to |
|---|
| 794 | # '::htmlparse::removeVisualFluff'. Does the actual work. |
|---|
| 795 | # |
|---|
| 796 | # Arguments: |
|---|
| 797 | # tree The name of the tree currently processed |
|---|
| 798 | # node The name of the node to look at. |
|---|
| 799 | # |
|---|
| 800 | # Side Effects: |
|---|
| 801 | # Modifies the specified tree. |
|---|
| 802 | # |
|---|
| 803 | # Results: |
|---|
| 804 | # None. |
|---|
| 805 | |
|---|
| 806 | proc ::htmlparse::RemoveVisualFluff {tree node} { |
|---|
| 807 | switch -exact -- [$tree get $node type] { |
|---|
| 808 | hmstart - html - font - center - div - sup - b - i { |
|---|
| 809 | # Removes the node, but does not affect the nodes below |
|---|
| 810 | # it. These are just made into chiildren of the parent of |
|---|
| 811 | # this node, in its place. |
|---|
| 812 | |
|---|
| 813 | $tree cut $node |
|---|
| 814 | } |
|---|
| 815 | script - option - select - meta - map - img { |
|---|
| 816 | # Removes this node and everything below it. |
|---|
| 817 | $tree delete $node |
|---|
| 818 | } |
|---|
| 819 | default { |
|---|
| 820 | # Ignore tag |
|---|
| 821 | } |
|---|
| 822 | } |
|---|
| 823 | } |
|---|
| 824 | |
|---|
| 825 | # htmlparse::RemoveFormDefs -- |
|---|
| 826 | # |
|---|
| 827 | # Internal helper command to |
|---|
| 828 | # '::htmlparse::removeFormDefs'. Does the actual work. |
|---|
| 829 | # |
|---|
| 830 | # Arguments: |
|---|
| 831 | # tree The name of the tree currently processed |
|---|
| 832 | # node The name of the node to look at. |
|---|
| 833 | # |
|---|
| 834 | # Side Effects: |
|---|
| 835 | # Modifies the specified tree. |
|---|
| 836 | # |
|---|
| 837 | # Results: |
|---|
| 838 | # None. |
|---|
| 839 | |
|---|
| 840 | proc ::htmlparse::RemoveFormDefs {tree node} { |
|---|
| 841 | switch -exact -- [$tree get $node type] { |
|---|
| 842 | form { |
|---|
| 843 | $tree delete $node |
|---|
| 844 | } |
|---|
| 845 | default { |
|---|
| 846 | # Ignore tag |
|---|
| 847 | } |
|---|
| 848 | } |
|---|
| 849 | } |
|---|
| 850 | |
|---|
| 851 | # htmlparse::Reorder -- |
|---|
| 852 | |
|---|
| 853 | # Internal helper command to '::htmlparse::2tree'. Moves the |
|---|
| 854 | # nodes between p/p, li/li and h<i> sequences below the |
|---|
| 855 | # paragraphs and items. IOW, corrects misconstructions for |
|---|
| 856 | # the optional node types. |
|---|
| 857 | # |
|---|
| 858 | # Arguments: |
|---|
| 859 | # tree The name of the tree currently processed |
|---|
| 860 | # node The name of the node to look at. |
|---|
| 861 | # |
|---|
| 862 | # Side Effects: |
|---|
| 863 | # Modifies the specified tree. |
|---|
| 864 | # |
|---|
| 865 | # Results: |
|---|
| 866 | # None. |
|---|
| 867 | |
|---|
| 868 | proc ::htmlparse::Reorder {tree node} { |
|---|
| 869 | switch -exact -- [set tp [$tree get $node type]] { |
|---|
| 870 | h1 - h2 - h3 - h4 - h5 - h6 - p - li { |
|---|
| 871 | # Look for right siblings until the next node with a |
|---|
| 872 | # similar type (or end of level) and move these below this |
|---|
| 873 | # node. |
|---|
| 874 | |
|---|
| 875 | while {1} { |
|---|
| 876 | set sibling [$tree next $node] |
|---|
| 877 | if { |
|---|
| 878 | ($sibling == {}) || |
|---|
| 879 | ([lsearch -exact {h1 h2 h3 h4 h5 h6 p li} [$tree get $sibling type]] != -1) |
|---|
| 880 | } { |
|---|
| 881 | break |
|---|
| 882 | } |
|---|
| 883 | $tree move $node end $sibling |
|---|
| 884 | } |
|---|
| 885 | } |
|---|
| 886 | default { |
|---|
| 887 | # Ignore tag |
|---|
| 888 | } |
|---|
| 889 | } |
|---|
| 890 | } |
|---|
| 891 | |
|---|
| 892 | # ### ######### ########################### |
|---|
| 893 | |
|---|
| 894 | package provide htmlparse 1.1.2 |
|---|