URI: 
       webdump.c - webdump - HTML to plain-text converter for webpages
  HTML git clone git://git.codemadness.org/webdump
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
       webdump.c (71489B)
       ---
            1 #include <errno.h>
            2 #include <limits.h>
            3 #include <stdio.h>
            4 #include <stdarg.h>
            5 #include <stdlib.h>
            6 #include <string.h>
            7 #include <strings.h>
            8 #include <unistd.h>
            9 
           10 #include "arg.h"
           11 char *argv0;
           12 
           13 #include "tree.h"
           14 #include "xml.h"
           15 
           16 static XMLParser parser;
           17 
           18 #ifndef __OpenBSD__
           19 #define pledge(p1,p2) 0
           20 #endif
           21 
           22 #undef strlcat
           23 size_t strlcat(char *, const char *, size_t);
           24 #undef strlcpy
           25 size_t strlcpy(char *, const char *, size_t);
           26 
           27 /* ctype-like macros, but always compatible with ASCII / UTF-8 */
           28 #define ISALPHA(c) ((((unsigned)c) | 32) - 'a' < 26)
           29 #define ISCNTRL(c) ((c) < ' ' || (c) == 0x7f)
           30 #define ISDIGIT(c) (((unsigned)c) - '0' < 10)
           31 #define ISSPACE(c) ((c) == ' ' || ((((unsigned)c) - '\t') < 5))
           32 #define TOLOWER(c) ((((unsigned)c) - 'A' < 26) ? ((c) | 32) : (c))
           33 
           34 #define LEN(x) (sizeof(x) / sizeof(x[0]))
           35 
           36 /* URI */
           37 struct uri {
           38         char proto[48];     /* scheme including ":" or "://" */
           39         char userinfo[256]; /* username [:password] */
           40         char host[256];
           41         char port[6];       /* numeric port */
           42         char path[1024];
           43         char query[1024];
           44         char fragment[1024];
           45 };
           46 
           47 /* options */
           48 static int allowansi     = 0;  /* (-a) allow ANSI escape codes */
           49 static int uniqrefs      = 0;  /* (-d) number unique references */
           50 static int showrefinline = 0;  /* (-i) show link reference number inline */
           51 static int showurlinline = 0;  /* (-I) show full link reference inline */
           52 static int showrefbottom = 0;  /* (-l) show link references at the bottom */
           53 static int allowosc8     = 0;  /* (-o) allow OSC 8 hyperlinks in output */
           54 static int allowlinewrap = 0;  /* (-r) line-wrapping */
           55 static int termwidth     = 77; /* (-w) terminal width */
           56 static int xresources    = 0;  /* (-x) write resources line-by-line to fd 3 */
           57 
           58 enum DisplayType {
           59         DisplayUnknown     = 0,
           60         DisplayInline      = 1 << 0,
           61         DisplayInlineBlock = 1 << 1, /* unused for now */
           62         DisplayBlock       = 1 << 2,
           63         DisplayNone        = 1 << 3,
           64         DisplayPre         = 1 << 4,
           65         DisplayList        = 1 << 5,
           66         DisplayListOrdered = 1 << 6,
           67         DisplayListItem    = 1 << 7,
           68         DisplayTable       = 1 << 8,
           69         DisplayTableRow    = 1 << 9,
           70         DisplayTableCell   = 1 << 10,
           71         DisplayHeader      = 1 << 11,
           72         DisplayDl          = 1 << 12,
           73         DisplayInput       = 1 << 13,
           74         DisplayButton      = 1 << 14,
           75         DisplaySelect      = 1 << 15,
           76         DisplaySelectMulti = 1 << 16,
           77         DisplayOption      = 1 << 17,
           78         DisplayQuote       = 1 << 18
           79 };
           80 
           81 /* ANSI markup */
           82 enum MarkupType {
           83         MarkupNone        = 0,
           84         MarkupBold        = 1 << 0,
           85         MarkupItalic      = 1 << 1,
           86         MarkupUnderline   = 1 << 2,
           87         MarkupBlink       = 1 << 3, /* lol */
           88         MarkupReverse     = 1 << 4,
           89         MarkupStrike      = 1 << 5
           90 };
           91 
           92 /* String data / memory pool */
           93 typedef struct string {
           94         char   *data;   /* data */
           95         size_t  len;    /* string length */
           96         size_t  bufsiz; /* allocated size */
           97 } String;
           98 
           99 enum TagId { TagA = 1, TagAddress, TagArea, TagArticle, TagAside, TagAudio,
          100         TagB, TagBase, TagBlink, TagBlockquote, TagBody, TagBr, TagButton,
          101         TagCite, TagCol, TagColgroup, TagDatalist, TagDd, TagDel, TagDetails,
          102         TagDfn, TagDir, TagDiv, TagDl, TagDt, TagEm, TagEmbed, TagFieldset,
          103         TagFigcaption, TagFigure, TagFooter, TagForm, TagFrame, TagH1, TagH2,
          104         TagH3, TagH4, TagH5, TagH6, TagHead, TagHeader, TagHr, TagHtml, TagI,
          105         TagIframe, TagImg, TagInput, TagIns, TagLabel, TagLegend, TagLi,
          106         TagLink, TagMain, TagMark, TagMenu, TagMeta, TagNav, TagObject, TagOl,
          107         TagOption, TagP, TagParam, TagPre, TagQ, TagS, TagScript,
          108         TagSearch, TagSection, TagSelect, TagSource, TagStrike, TagStrong,
          109         TagStyle, TagSummary, TagSvg, TagTable, TagTbody, TagTd, TagTemplate,
          110         TagTextarea, TagTfoot, TagTh, TagThead, TagTitle, TagTr, TagTrack,
          111         TagU, TagUl, TagVar, TagVideo, TagWbr, TagXmp };
          112 
          113 struct tag {
          114         const char *name;
          115         enum TagId id;
          116         enum DisplayType displaytype;
          117         enum MarkupType markuptype; /* ANSI markup */
          118         enum DisplayType parenttype; /* display type belonging to element */
          119         int isvoid; /* "void" element */
          120         int isoptional; /* optional to close tag */
          121         int margintop; /* newlines when the tag starts */
          122         int marginbottom; /* newlines after the tag ends */
          123         int indent; /* indent in cells */
          124 };
          125 
          126 struct node {
          127         char tagname[128];
          128         struct tag tag;
          129         size_t nchildren; /* child node count */
          130         size_t visnchildren; /* child node count which are visible */
          131         /* attributes */
          132         char id[128];
          133         char classnames[256];
          134         int indent; /* indent per node, for formatting */
          135         int hasdata; /* tag contains some data, for formatting */
          136 };
          137 
          138 struct selectornode {
          139         char tagname[128];
          140         long index; /* index of node to match on: -1 if not matching on index */
          141         /* attributes */
          142         char id[128];
          143         char classnames[256];
          144 };
          145 
          146 struct selector {
          147         char *text;
          148         struct selectornode nodes[32];
          149         int depth;
          150 };
          151 
          152 /* list of selectors */
          153 struct selectors {
          154         struct selector **selectors;
          155         size_t count;
          156 };
          157 
          158 /* RB tree of link references */
          159 struct linkref {
          160         char *type;
          161         enum TagId tagid;
          162         char *url;
          163         int ishidden;
          164         size_t linknr;
          165         RB_ENTRY(linkref) entry;
          166 };
          167 
          168 /* link references and hidden link references */
          169 static struct linkref **visrefs;
          170 static size_t nvisrefs, ncapvisrefs; /* visible link count / capacity */
          171 static struct linkref **hiddenrefs;
          172 static size_t nhiddenrefs, ncaphiddenrefs; /* hidden link count / capacity */
          173 
          174 /* compare link by URL for link references RB-tree */
          175 static int
          176 linkrefcmp(struct linkref *r1, struct linkref *r2)
          177 {
          178         return strcmp(r1->url, r2->url);
          179 }
          180 
          181 RB_HEAD(linkreftree, linkref) linkrefhead = RB_INITIALIZER(&linkrefhead);
          182 RB_GENERATE(linkreftree, linkref, entry, linkrefcmp)
          183 
          184 static const char *str_bullet_item = "* ";
          185 static const char *str_checkbox_checked = "x";
          186 static const char *str_ruler = "-";
          187 static const char *str_radio_checked = "*";
          188 
          189 /* base href, to make URLs absolute */
          190 static char basehrefdoc[4096]; /* buffer for base href in document, if any */
          191 static int basehrefset; /* base href set and can be used? */
          192 static struct uri base; /* parsed current base href */
          193 
          194 /* buffers for some attributes of the current tag */
          195 static String attr_alt; /* alt attribute */
          196 static String attr_checked; /* checked attribute */
          197 static String attr_class; /* class attribute */
          198 static int attr_class_set; /* class attribute is set already */
          199 static String attr_data; /* data attribute */
          200 static String attr_href; /* href attribute */
          201 static String attr_id; /* id attribute */
          202 static int attr_id_set; /* class attribute is set already */
          203 static String attr_src; /* src attribute */
          204 static String attr_type; /* type attribute */
          205 static String attr_value; /* value attribute */
          206 
          207 static String htmldata; /* buffered HTML data near the current tag */
          208 
          209 /* for white-space output handling:
          210    1 = whitespace emitted (suppress repeated), 2 = other characters on this line
          211    Behaviour:
          212    * White-space data before non-whitespace data in tags are ignored on a line.
          213    * Repeated white-space are ignored: a single space (' ') is emitted.
          214 */
          215 static int whitespace_mode;
          216 static int nbytesline; /* bytes on this line */
          217 static int ncells; /* current cell/column count */
          218 static int hadnewline; /* count for repeated newlines */
          219 /* flag for skipping initial white-space in tag: for HTML white-space handling */
          220 static int skipinitialws = 1;
          221 #define DEFAULT_INDENT 2
          222 static const int defaultindent = DEFAULT_INDENT; /* default indent / margin */
          223 static int indent; /* indent for the current line, in columns */
          224 /* previous output sequential newlines, used for calculating margins between
          225    elements and reducing excessive newlines */
          226 static int currentnewlines;
          227 
          228 /* buffers for line-wrapping (buffer per word boundary) */
          229 static char rbuf[1024];
          230 static int rbuflen;
          231 static int rnbufcells; /* pending cell count to add */
          232 
          233 #define MAX_NODE_DEPTH 4096 /* absolute maximum node depth */
          234 static struct node *nodes; /* node tree (one per level is remembered) */
          235 static String *nodes_links; /* keep track of links per node */
          236 static size_t ncapnodes; /* current allocated node capacity */
          237 static int curnode; /* current node depth */
          238 
          239 /* reader / selector mode (-s) */
          240 static int reader_mode;
          241 /* flag if the tags and their children should be ignored in the current context */
          242 static int reader_ignore;
          243 
          244 static enum MarkupType curmarkup; /* current terminal markup state (bold, underline, etc) */
          245 static int linewrap; /* allow linewrap in this context */
          246 
          247 /* selector to match (for -s and -u) */
          248 static struct selectors *sel_hide, *sel_show;
          249 
          250 /* tags table: needs to be sorted like tagcmp(), alphabetically */
          251 
          252 /* tag          id             displaytype                       markup           parent           v  o  mt mb i */
          253 static struct tag tags[] = {
          254 { "a",          TagA,          DisplayInline,                    MarkupUnderline, 0,               0, 0, 0, 0, 0 },
          255 { "address",    TagAddress,    DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          256 { "area",       TagArea,       DisplayInline,                    0,               0,               1, 0, 0, 0, 0 },
          257 { "article",    TagArticle,    DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          258 { "aside",      TagAside,      DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          259 { "audio",      TagAudio,      DisplayInline,                    MarkupUnderline, 0,               0, 0, 0, 0, 0 },
          260 { "b",          TagB,          DisplayInline,                    MarkupBold,      0,               0, 0, 0, 0, 0 },
          261 { "base",       TagBase,       DisplayInline,                    0,               0,               1, 0, 0, 0, 0 },
          262 { "blink",      TagBlink,      DisplayInline,                    MarkupBlink,     0,               0, 0, 0, 0, 0 },
          263 { "blockquote", TagBlockquote, DisplayBlock,                     0,               0,               0, 0, 0, 0, 2 },
          264 { "body",       TagBody,       DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          265 { "br",         TagBr,         0,                                0,               0,               1, 0, 0, 0, 0 },
          266 { "button",     TagButton,     DisplayInline | DisplayButton,    0,               0,               0, 0, 0, 0, 0 },
          267 { "cite",       TagCite,       DisplayInline,                    MarkupItalic,    0,               0, 0, 0, 0, 0 },
          268 { "col",        TagCol,        DisplayInline,                    0,               0,               1, 0, 0, 0, 0 },
          269 { "colgroup",   TagColgroup,   DisplayInline,                    0,               0,               0, 1, 0, 0, 0 },
          270 { "datalist",   TagDatalist,   DisplayNone,                      0,               0,               0, 0, 0, 0, 0 },
          271 { "dd",         TagDd,         DisplayBlock,                     0,               0,               0, 1, 0, 0, 4 },
          272 { "del",        TagDel,        DisplayInline,                    MarkupStrike,    0,               0, 0, 0, 0, 0 },
          273 { "details",    TagDetails,    DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          274 { "dfn",        TagDfn,        DisplayInline,                    MarkupItalic,    0,               0, 0, 0, 0, 0 },
          275 { "dir",        TagDir,        DisplayList,                      0,               0,               0, 0, 1, 1, 2 },
          276 { "div",        TagDiv,        DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          277 { "dl",         TagDl,         DisplayBlock | DisplayDl,         0,               0,               0, 0, 0, 0, 0 },
          278 { "dt",         TagDt,         DisplayBlock,                     MarkupBold,      0,               0, 1, 0, 0, 0 },
          279 { "em",         TagEm,         DisplayInline,                    MarkupItalic,    0,               0, 0, 0, 0, 0 },
          280 { "embed",      TagEmbed,      DisplayInline,                    0,               0,               1, 0, 0, 0, 0 },
          281 { "fieldset",   TagFieldset,   DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          282 { "figcaption", TagFigcaption, DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          283 { "figure",     TagFigure,     DisplayBlock,                     0,               0,               0, 0, 1, 1, 4 },
          284 { "footer",     TagFooter,     DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          285 { "form",       TagForm,       DisplayBlock,                     0,               0,               0, 0, 0, 1, 0 },
          286 { "frame",      TagFrame,      DisplayInline,                    0,               0,               1, 0, 0, 0, 0 },
          287 { "h1",         TagH1,         DisplayHeader,                    MarkupBold,      0,               0, 0, 1, 1, -DEFAULT_INDENT },
          288 { "h2",         TagH2,         DisplayHeader,                    MarkupBold,      0,               0, 0, 1, 1, -DEFAULT_INDENT },
          289 { "h3",         TagH3,         DisplayHeader,                    MarkupBold,      0,               0, 0, 1, 1, -DEFAULT_INDENT },
          290 { "h4",         TagH4,         DisplayHeader,                    MarkupBold,      0,               0, 0, 1, 1, -DEFAULT_INDENT },
          291 { "h5",         TagH5,         DisplayHeader,                    MarkupBold,      0,               0, 0, 1, 1, -DEFAULT_INDENT },
          292 { "h6",         TagH6,         DisplayHeader,                    MarkupBold,      0,               0, 0, 1, 1, -DEFAULT_INDENT },
          293 { "head",       TagHead,       DisplayBlock,                     0,               0,               0, 1, 0, 0, 0 },
          294 { "header",     TagHeader,     DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          295 { "hr",         TagHr,         DisplayBlock,                     0,               0,               1, 0, 0, 0, 0 },
          296 { "html",       TagHtml,       DisplayBlock,                     0,               0,               0, 1, 0, 0, 0 },
          297 { "i",          TagI,          DisplayInline,                    MarkupItalic,    0,               0, 0, 0, 0, 0 },
          298 { "iframe",     TagIframe,     DisplayInline,                    0,               0,               0, 0, 0, 0, 0 },
          299 { "img",        TagImg,        DisplayInline,                    MarkupUnderline, 0,               1, 0, 0, 0, 0 },
          300 { "input",      TagInput,      DisplayInput,                     0,               0,               1, 0, 0, 0, 0 },
          301 { "ins",        TagIns,        DisplayInline,                    MarkupUnderline, 0,               0, 0, 0, 0, 0 },
          302 { "label",      TagLabel,      DisplayInline,                    0,               0,               0, 0, 0, 0, 0 },
          303 { "legend",     TagLegend,     DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          304 { "li",         TagLi,         DisplayListItem,                  0,               DisplayList,     0, 1, 0, 0, 0 },
          305 { "link",       TagLink,       DisplayInline,                    0,               0,               1, 0, 0, 0, 0 },
          306 { "main",       TagMain,       DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          307 { "mark",       TagMark,       DisplayInline,                    MarkupReverse,   0,               0, 0, 0, 0, 0 },
          308 { "menu",       TagMenu,       DisplayList,                      0,               0,               0, 0, 1, 1, 2 },
          309 { "meta",       TagMeta,       DisplayInline,                    0,               0,               1, 0, 0, 0, 0 },
          310 { "nav",        TagNav,        DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          311 { "object",     TagObject,     DisplayInline,                    0,               0,               0, 0, 0, 0, 0 },
          312 { "ol",         TagOl,         DisplayList | DisplayListOrdered, 0,               0,               0, 0, 1, 1, 0 },
          313 { "option",     TagOption,     DisplayInline | DisplayOption,    0,               0,               0, 1, 0, 0, 0 },
          314 { "p",          TagP,          DisplayBlock,                     0,               0,               0, 1, 1, 1, 0 },
          315 { "param",      TagParam,      DisplayInline,                    0,               0,               1, 0, 0, 0, 0 },
          316 { "pre",        TagPre,        DisplayPre,                       0,               0,               0, 0, 1, 1, 4 },
          317 { "q",          TagQ,          DisplayInline | DisplayQuote,     0,               0,               0, 0, 0, 0, 0 },
          318 { "s",          TagS,          DisplayInline,                    MarkupStrike,    0,               0, 0, 0, 0, 0 },
          319 { "script",     TagScript,     DisplayNone,                      0,               0,               0, 0, 0, 0, 0 },
          320 { "search",     TagSearch,     DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          321 { "section",    TagSection,    DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          322 { "select",     TagSelect,     DisplayInline | DisplaySelect,    0,               0,               0, 0, 0, 0, 0 },
          323 { "source",     TagSource,     DisplayInline,                    0,               0,               1, 0, 0, 0, 0 },
          324 { "strike",     TagStrike,     DisplayInline,                    MarkupStrike,    0,               0, 0, 0, 0, 0 },
          325 { "strong",     TagStrong,     DisplayInline,                    MarkupBold,      0,               0, 0, 0, 0, 0 },
          326 { "style",      TagStyle,      DisplayNone,                      0,               0,               0, 0, 0, 0, 0 },
          327 { "summary",    TagSummary,    DisplayBlock,                     0,               0,               0, 0, 0, 0, 0 },
          328 { "svg",        TagSvg,        DisplayNone,                      0,               0,               0, 0, 0, 0, 0 },
          329 { "table",      TagTable,      DisplayTable,                     0,               0,               0, 0, 0, 0, 0 },
          330 { "tbody",      TagTbody,      DisplayInline,                    0,               DisplayTable,    0, 1, 0, 0, 0 },
          331 { "td",         TagTd,         DisplayTableCell,                 0,               DisplayTableRow, 0, 1, 0, 0, 0 },
          332 { "template",   TagTemplate,   DisplayNone,                      0,               0,               0, 0, 0, 0, 0 },
          333 { "textarea",   TagTextarea,   DisplayInline | DisplayPre,       0,               0,               0, 0, 0, 0, 0 },
          334 { "tfoot",      TagTfoot,      DisplayInline,                    0,               DisplayTable,    0, 1, 0, 0, 0 },
          335 { "th",         TagTh,         DisplayTableCell,                 MarkupBold,      DisplayTableRow, 0, 1, 0, 0, 0 },
          336 { "thead",      TagThead,      DisplayInline,                    0,               DisplayTable,    0, 1, 0, 0, 0 },
          337 { "title",      TagTitle,      DisplayBlock,                     0,               0,               0, 0, 0, 0, -DEFAULT_INDENT },
          338 { "tr",         TagTr,         DisplayTableRow,                  0,               DisplayTable,    0, 1, 0, 0, 0 },
          339 { "track",      TagTrack,      DisplayInline,                    0,               0,               1, 0, 0, 0, 0 },
          340 { "u",          TagU,          DisplayInline,                    MarkupUnderline, 0,               0, 0, 0, 0, 0 },
          341 { "ul",         TagUl,         DisplayList,                      0,               0,               0, 0, 1, 1, 2 },
          342 { "var",        TagVar,        DisplayInline,                    MarkupItalic,    0,               0, 0, 0, 0, 0 },
          343 { "video",      TagVideo,      DisplayInline,                    MarkupUnderline, 0,               0, 0, 0, 0, 0 },
          344 { "wbr",        TagWbr,        DisplayInline,                    0,               0,               1, 0, 0, 0, 0 },
          345 { "xmp",        TagXmp,        DisplayPre,                       0,               0,               0, 0, 1, 1, 4 }
          346 };
          347 
          348 /* hint for compilers and static analyzers that a function exits */
          349 #ifndef __dead
          350 #define __dead
          351 #endif
          352 
          353 /* print to stderr, print error message of errno and exit(). */
          354 __dead static void
          355 err(int exitstatus, const char *fmt, ...)
          356 {
          357         va_list ap;
          358         int saved_errno;
          359 
          360         saved_errno = errno;
          361 
          362         fputs("webdump: ", stderr);
          363         if (fmt) {
          364                 va_start(ap, fmt);
          365                 vfprintf(stderr, fmt, ap);
          366                 va_end(ap);
          367                 fputs(": ", stderr);
          368         }
          369         fprintf(stderr, "%s\n", strerror(saved_errno));
          370 
          371         exit(exitstatus);
          372 }
          373 
          374 /* print to stderr and exit(). */
          375 __dead static void
          376 errx(int exitstatus, const char *fmt, ...)
          377 {
          378         va_list ap;
          379 
          380         fputs("webdump: ", stderr);
          381         if (fmt) {
          382                 va_start(ap, fmt);
          383                 vfprintf(stderr, fmt, ap);
          384                 va_end(ap);
          385         }
          386         fputs("\n", stderr);
          387 
          388         exit(exitstatus);
          389 }
          390 
          391 /* Clear string only; don't free, prevents unnecessary reallocation. */
          392 static void
          393 string_clear(String *s)
          394 {
          395         if (s->data)
          396                 s->data[0] = '\0';
          397         s->len = 0;
          398 }
          399 
          400 static void
          401 string_buffer_realloc(String *s, size_t newlen)
          402 {
          403         size_t alloclen;
          404 
          405         for (alloclen = 64; alloclen <= newlen; alloclen *= 2)
          406                 ;
          407         if (!(s->data = realloc(s->data, alloclen)))
          408                 err(1, "realloc");
          409         s->bufsiz = alloclen;
          410 }
          411 
          412 static void
          413 string_append(String *s, const char *data, size_t len)
          414 {
          415         if (!len)
          416                 return;
          417         /* check if allocation is necesary, don't shrink buffer,
          418          * should be more than bufsiz ofcourse. */
          419         if (s->len + len >= s->bufsiz)
          420                 string_buffer_realloc(s, s->len + len + 1);
          421         memcpy(s->data + s->len, data, len);
          422         s->len += len;
          423         s->data[s->len] = '\0';
          424 }
          425 
          426 static char *ignorestate;
          427 static char endtagmatch[16];
          428 static unsigned char pushbuf[32], *pushoff, *pushend; /* pushback buffer */
          429 static unsigned char *pushtagend; /* end position of tag in pushback buffer */
          430 static int (*getnext)(void);
          431 
          432 static void
          433 getnext_literal_reset(void)
          434 {
          435         pushoff = pushbuf;
          436         pushend = pushbuf;
          437         pushtagend = NULL;
          438 }
          439 
          440 static void
          441 getnext_literal_end(void)
          442 {
          443         parser.getnext = getnext; /* restore original getnext callback */
          444 }
          445 
          446 /* Collect data as literal text, used for certain HTML tags such as <script> or
          447    <title> until some case-insensitive string occurs (closing tag).
          448    This code is very ugly and I'm not proud of it. Mmmmm spaghetti. */
          449 static int
          450 getnext_literal(void)
          451 {
          452         char *p;
          453         int c;
          454 
          455         /* check if there is data in the pushback buffer: process it first. */
          456         if (pushend != pushbuf) {
          457                 if (pushoff >= pushend) {
          458                         /* reached the offset where the tag ended */
          459                         if (pushtagend) {
          460                                 c = *pushtagend;
          461                                 getnext_literal_end();
          462                                 return c; /* end tag */
          463                         }
          464                         /* otherwise reached the end of pushback buffer, proceed normally */
          465                         getnext_literal_reset();
          466                 } else {
          467                         return (int)*(pushoff++);
          468                 }
          469         }
          470 
          471         if (*ignorestate) {
          472                 for (; *ignorestate; ignorestate++) {
          473                         if ((c = getnext()) == EOF)
          474                                 return EOF;
          475                         if (TOLOWER((unsigned char)c) != TOLOWER((unsigned char)*ignorestate))
          476                                 break;
          477                 }
          478         } else {
          479                 c = EOF; /* cannot happen, endtagmatch should be set (not empty) */
          480         }
          481 
          482         /* typical case: regular character */
          483         if (ignorestate == endtagmatch)
          484                 return c;
          485 
          486         /* process a partial or full match on the tag:
          487            full: handle as end tag. partial: handle "<" as a data entity "&lt;".
          488            buffers are not checked, they always fit (size of tag + space for at least "&lt;&lt;"). */
          489         pushoff = pushbuf;
          490         pushend = pushbuf;
          491         if (*ignorestate == '\0') { /* full match */
          492                 for (p = endtagmatch; *p && p < ignorestate; p++)
          493                         *(pushend++) = *p;
          494                 pushtagend = pushend; /* offset where the tag ended */
          495         } else { /* partial match */
          496                 for (p = endtagmatch; *p && p < ignorestate; p++) {
          497                         if (*p == '<') {
          498                                 memcpy(pushend, "&lt;", 4);
          499                                 pushend += 4;
          500                         } else {
          501                                 *(pushend++) = *p;
          502                         }
          503                 }
          504                 /* append current character to end of pushback buffer */
          505                 if (c == '<') {
          506                         memcpy(pushend, "&lt;", 4);
          507                         pushend += 4;
          508                 } else {
          509                         *(pushend++) = c;
          510                 }
          511         }
          512         ignorestate = endtagmatch; /* reset state for matching to beginning */
          513 
          514         return (int)*(pushoff++); /* first character in pushback buffer, always exists */
          515 }
          516 
          517 static void
          518 getnext_literal_start(const char *t)
          519 {
          520         snprintf(endtagmatch, sizeof(endtagmatch), "</%s>", t);
          521         ignorestate = endtagmatch;
          522         getnext_literal_reset(); /* reset pushback buffer */
          523 
          524         getnext = parser.getnext; /* for restore */
          525         parser.getnext = getnext_literal;
          526 }
          527 
          528 static char *
          529 estrdup(const char *s)
          530 {
          531         char *p;
          532 
          533         if (!(p = strdup(s)))
          534                 err(1, "strdup");
          535         return p;
          536 }
          537 
          538 static char *
          539 estrndup(const char *s, size_t n)
          540 {
          541         char *p;
          542 
          543         if (!(p = strndup(s, n)))
          544                 err(1, "strndup");
          545         return p;
          546 }
          547 
          548 static void *
          549 erealloc(void *p, size_t siz)
          550 {
          551         if (!(p = realloc(p, siz)))
          552                 err(1, "realloc");
          553 
          554         return p;
          555 }
          556 
          557 static void *
          558 ecalloc(size_t nmemb, size_t size)
          559 {
          560         void *p;
          561 
          562         if (!(p = calloc(nmemb, size)))
          563                 err(1, "calloc");
          564         return p;
          565 }
          566 
          567 /* check if string has a non-empty scheme / protocol part */
          568 static int
          569 uri_hasscheme(const char *s)
          570 {
          571         const char *p = s;
          572 
          573         for (; ISALPHA((unsigned char)*p) || ISDIGIT((unsigned char)*p) ||
          574                        *p == '+' || *p == '-' || *p == '.'; p++)
          575                 ;
          576         /* scheme, except if empty and starts with ":" then it is a path */
          577         return (*p == ':' && p != s);
          578 }
          579 
          580 static int
          581 uri_parse(const char *s, struct uri *u)
          582 {
          583         const char *p = s;
          584         char *endptr;
          585         size_t i;
          586         long l;
          587 
          588         u->proto[0] = u->userinfo[0] = u->host[0] = u->port[0] = '\0';
          589         u->path[0] = u->query[0] = u->fragment[0] = '\0';
          590 
          591         /* protocol-relative */
          592         if (*p == '/' && *(p + 1) == '/') {
          593                 p += 2; /* skip "//" */
          594                 goto parseauth;
          595         }
          596 
          597         /* scheme / protocol part */
          598         for (; ISALPHA((unsigned char)*p) || ISDIGIT((unsigned char)*p) ||
          599                        *p == '+' || *p == '-' || *p == '.'; p++)
          600                 ;
          601         /* scheme, except if empty and starts with ":" then it is a path */
          602         if (*p == ':' && p != s) {
          603                 if (*(p + 1) == '/' && *(p + 2) == '/')
          604                         p += 3; /* skip "://" */
          605                 else
          606                         p++; /* skip ":" */
          607 
          608                 if ((size_t)(p - s) >= sizeof(u->proto))
          609                         return -1; /* protocol too long */
          610                 memcpy(u->proto, s, p - s);
          611                 u->proto[p - s] = '\0';
          612 
          613                 if (*(p - 1) != '/')
          614                         goto parsepath;
          615         } else {
          616                 p = s; /* no scheme format, reset to start */
          617                 goto parsepath;
          618         }
          619 
          620 parseauth:
          621         /* userinfo (username:password) */
          622         i = strcspn(p, "@/?#");
          623         if (p[i] == '@') {
          624                 if (i >= sizeof(u->userinfo))
          625                         return -1; /* userinfo too long */
          626                 memcpy(u->userinfo, p, i);
          627                 u->userinfo[i] = '\0';
          628                 p += i + 1;
          629         }
          630 
          631         /* IPv6 address */
          632         if (*p == '[') {
          633                 /* bracket not found, host too short or too long */
          634                 i = strcspn(p, "]");
          635                 if (p[i] != ']' || i < 3)
          636                         return -1;
          637                 i++; /* including "]" */
          638         } else {
          639                 /* domain / host part, skip until port, path or end. */
          640                 i = strcspn(p, ":/?#");
          641         }
          642         if (i >= sizeof(u->host))
          643                 return -1; /* host too long */
          644         memcpy(u->host, p, i);
          645         u->host[i] = '\0';
          646         p += i;
          647 
          648         /* port */
          649         if (*p == ':') {
          650                 p++;
          651                 if ((i = strcspn(p, "/?#")) >= sizeof(u->port))
          652                         return -1; /* port too long */
          653                 memcpy(u->port, p, i);
          654                 u->port[i] = '\0';
          655                 /* check for valid port: range 1 - 65535, may be empty */
          656                 errno = 0;
          657                 l = strtol(u->port, &endptr, 10);
          658                 if (i && (errno || *endptr || l <= 0 || l > 65535))
          659                         return -1;
          660                 p += i;
          661         }
          662 
          663 parsepath:
          664         /* path */
          665         if ((i = strcspn(p, "?#")) >= sizeof(u->path))
          666                 return -1; /* path too long */
          667         memcpy(u->path, p, i);
          668         u->path[i] = '\0';
          669         p += i;
          670 
          671         /* query */
          672         if (*p == '?') {
          673                 p++;
          674                 if ((i = strcspn(p, "#")) >= sizeof(u->query))
          675                         return -1; /* query too long */
          676                 memcpy(u->query, p, i);
          677                 u->query[i] = '\0';
          678                 p += i;
          679         }
          680 
          681         /* fragment */
          682         if (*p == '#') {
          683                 p++;
          684                 if ((i = strlen(p)) >= sizeof(u->fragment))
          685                         return -1; /* fragment too long */
          686                 memcpy(u->fragment, p, i);
          687                 u->fragment[i] = '\0';
          688         }
          689 
          690         return 0;
          691 }
          692 
          693 /* Transform and try to make the URI `u` absolute using base URI `b` into `a`.
          694    Follows some of the logic from "RFC 3986 - 5.2.2. Transform References".
          695    Returns 0 on success, -1 on error or truncation. */
          696 static int
          697 uri_makeabs(struct uri *a, struct uri *u, struct uri *b)
          698 {
          699         char *p;
          700         int c;
          701 
          702         strlcpy(a->fragment, u->fragment, sizeof(a->fragment));
          703 
          704         if (u->proto[0] || u->host[0]) {
          705                 strlcpy(a->proto, u->proto[0] ? u->proto : b->proto, sizeof(a->proto));
          706                 strlcpy(a->host, u->host, sizeof(a->host));
          707                 strlcpy(a->userinfo, u->userinfo, sizeof(a->userinfo));
          708                 strlcpy(a->host, u->host, sizeof(a->host));
          709                 strlcpy(a->port, u->port, sizeof(a->port));
          710                 strlcpy(a->path, u->path, sizeof(a->path));
          711                 strlcpy(a->query, u->query, sizeof(a->query));
          712                 return 0;
          713         }
          714 
          715         strlcpy(a->proto, b->proto, sizeof(a->proto));
          716         strlcpy(a->host, b->host, sizeof(a->host));
          717         strlcpy(a->userinfo, b->userinfo, sizeof(a->userinfo));
          718         strlcpy(a->host, b->host, sizeof(a->host));
          719         strlcpy(a->port, b->port, sizeof(a->port));
          720 
          721         if (!u->path[0]) {
          722                 strlcpy(a->path, b->path, sizeof(a->path));
          723         } else if (u->path[0] == '/') {
          724                 strlcpy(a->path, u->path, sizeof(a->path));
          725         } else {
          726                 a->path[0] = (b->host[0] && b->path[0] != '/') ? '/' : '\0';
          727                 a->path[1] = '\0';
          728 
          729                 if ((p = strrchr(b->path, '/'))) {
          730                         c = *(++p);
          731                         *p = '\0'; /* temporary NUL-terminate */
          732                         if (strlcat(a->path, b->path, sizeof(a->path)) >= sizeof(a->path))
          733                                 return -1;
          734                         *p = c; /* restore */
          735                 }
          736                 if (strlcat(a->path, u->path, sizeof(a->path)) >= sizeof(a->path))
          737                         return -1;
          738         }
          739 
          740         if (u->path[0] || u->query[0])
          741                 strlcpy(a->query, u->query, sizeof(a->query));
          742         else
          743                 strlcpy(a->query, b->query, sizeof(a->query));
          744 
          745         return 0;
          746 }
          747 
          748 static int
          749 uri_format(char *buf, size_t bufsiz, struct uri *u)
          750 {
          751         return snprintf(buf, bufsiz, "%s%s%s%s%s%s%s%s%s%s%s%s",
          752                 u->proto,
          753                 u->userinfo[0] ? u->userinfo : "",
          754                 u->userinfo[0] ? "@" : "",
          755                 u->host,
          756                 u->port[0] ? ":" : "",
          757                 u->port,
          758                 u->host[0] && u->path[0] && u->path[0] != '/' ? "/" : "",
          759                 u->path,
          760                 u->query[0] ? "?" : "",
          761                 u->query,
          762                 u->fragment[0] ? "#" : "",
          763                 u->fragment);
          764 }
          765 
          766 static void
          767 printuri(const char *s)
          768 {
          769         static char hex[] = "0123456789ABCDEF";
          770         unsigned char c;
          771 
          772         for (; *s; s++) {
          773                 c = (unsigned char)*s;
          774                 if (ISCNTRL((unsigned char)*s)) {
          775                         putchar('%');
          776                         putchar(hex[c >> 4]);
          777                         putchar(hex[c & 0x0f]);
          778                 } else {
          779                         putchar(*s);
          780                 }
          781         }
          782 }
          783 
          784 /* compare tag name (case-insensitive) */
          785 static int
          786 tagcmp(const char *s1, const char *s2)
          787 {
          788         return strcasecmp(s1, s2);
          789 }
          790 
          791 /* compare attribute name (case-insensitive) */
          792 static int
          793 attrcmp(const char *s1, const char *s2)
          794 {
          795         return strcasecmp(s1, s2);
          796 }
          797 
          798 static void
          799 rindent(void)
          800 {
          801         int i, total;
          802 
          803         total = indent + defaultindent;
          804         if (total < 0)
          805                 total = 0;
          806         for (i = 0; i < total; i++)
          807                 putchar(' ');
          808 
          809         nbytesline += total;
          810         ncells += total;
          811 }
          812 
          813 static void
          814 emitmarkup(int markuptype)
          815 {
          816         if (!allowansi)
          817                 return;
          818 
          819         if (!markuptype)
          820                 fputs("\033[0m", stdout); /* reset all attributes */
          821 
          822         /* set */
          823         if (markuptype & MarkupBold)
          824                 fputs("\033[1m", stdout);
          825         if (markuptype & MarkupItalic)
          826                 fputs("\033[3m", stdout);
          827         if (markuptype & MarkupUnderline)
          828                 fputs("\033[4m", stdout);
          829         if (markuptype & MarkupBlink)
          830                 fputs("\033[5m", stdout);
          831         if (markuptype & MarkupReverse)
          832                 fputs("\033[7m", stdout);
          833         if (markuptype & MarkupStrike)
          834                 fputs("\033[9m", stdout);
          835 }
          836 
          837 /* flush remaining buffer (containing a word): used for word-wrap handling */
          838 static void
          839 hflush(void)
          840 {
          841         int i;
          842 
          843         if (!rbuflen)
          844                 return;
          845 
          846         if (!nbytesline) {
          847                 if (curmarkup)
          848                         emitmarkup(0);
          849                 rindent();
          850                 /* emit code again per line, needed for GNU/less -R */
          851                 if (curmarkup)
          852                         emitmarkup(curmarkup);
          853         }
          854 
          855         for (i = 0; i < rbuflen; i++)
          856                 putchar(rbuf[i]);
          857 
          858         nbytesline += rbuflen;
          859         ncells += rnbufcells;
          860         rbuflen = 0;
          861         rnbufcells = 0;
          862 }
          863 
          864 static void
          865 printansi(const char *s)
          866 {
          867         size_t len;
          868 
          869         if (!allowansi)
          870                 return;
          871 
          872         if (linewrap) {
          873                 len = strlen(s);
          874                 if (rbuflen + len + 1 >= sizeof(rbuf))
          875                         hflush();
          876                 if (rbuflen + len + 1 < sizeof(rbuf)) {
          877                         memcpy(rbuf + rbuflen, s, len);
          878                         rbuflen += len;
          879                         /* NOTE: nbytesline and ncells are not counted for markup */
          880                 }
          881         } else {
          882                 fputs(s, stdout);
          883         }
          884 }
          885 
          886 static void
          887 setmarkup(int markuptype)
          888 {
          889         if (!allowansi)
          890                 return;
          891 
          892         /* need change? */
          893         if (curmarkup == markuptype)
          894                 return;
          895 
          896         if (!markuptype) {
          897                 printansi("\033[0m"); /* reset all attributes */
          898                 curmarkup = markuptype;
          899                 return;
          900         }
          901 
          902         /* set */
          903         if (!(curmarkup & MarkupBold) && (markuptype & MarkupBold))
          904                 printansi("\033[1m");
          905         if (!(curmarkup & MarkupItalic) && (markuptype & MarkupItalic))
          906                 printansi("\033[3m");
          907         if (!(curmarkup & MarkupUnderline) && (markuptype & MarkupUnderline))
          908                 printansi("\033[4m");
          909         if (!(curmarkup & MarkupBlink) && (markuptype & MarkupBlink))
          910                 printansi("\033[5m");
          911         if (!(curmarkup & MarkupReverse) && (markuptype & MarkupReverse))
          912                 printansi("\033[7m");
          913         if (!(curmarkup & MarkupStrike) && (markuptype & MarkupStrike))
          914                 printansi("\033[9m");
          915 
          916         /* unset */
          917         if ((curmarkup & MarkupBold) && !(markuptype & MarkupBold))
          918                 printansi("\033[22m"); /* reset bold or faint */
          919         if ((curmarkup & MarkupItalic) && !(markuptype & MarkupItalic))
          920                 printansi("\033[23m"); /* reset italic */
          921         if ((curmarkup & MarkupUnderline) && !(markuptype & MarkupUnderline))
          922                 printansi("\033[24m"); /* reset underline */
          923         if ((curmarkup & MarkupBlink) && !(markuptype & MarkupBlink))
          924                 printansi("\033[25m"); /* reset blink */
          925         if ((curmarkup & MarkupReverse) && !(markuptype & MarkupReverse))
          926                 printansi("\033[27m"); /* reset reverse */
          927         if ((curmarkup & MarkupStrike) && !(markuptype & MarkupStrike))
          928                 printansi("\033[29m"); /* reset strike */
          929 
          930         curmarkup = markuptype;
          931 }
          932 
          933 static void
          934 startmarkup(int markuptype)
          935 {
          936         setmarkup(curmarkup | markuptype);
          937 }
          938 
          939 static void
          940 endmarkup(int markuptype)
          941 {
          942         setmarkup(curmarkup & ~markuptype);
          943 }
          944 
          945 /* rough cell width of a unicode codepoint by counting a unicode codepoint as 1
          946    cell in general.
          947    NOTE: this is of course incorrect since characters can be 2 width aswell,
          948    in the future maybe replace this with wcwidth() or similar */
          949 static int
          950 utfwidth(int c)
          951 {
          952         /* not the start of a codepoint */
          953         if ((c & 0xc0) == 0x80)
          954                 return 0;
          955         /* count TAB as 8 */
          956         if (c == '\t')
          957                 return 8;
          958         return 1;
          959 }
          960 
          961 /* write a character, handling state of repeated newlines, some HTML
          962    white-space rules, indentation and word-wrapping */
          963 static void
          964 hputchar(int c)
          965 {
          966         struct node *cur = &nodes[curnode];
          967         cur->hasdata = 1;
          968 
          969         if (c == '\n') {
          970                 /* previous line had characters, so not a repeated newline */
          971                 if (nbytesline > 0)
          972                         hadnewline = 0;
          973 
          974                 /* start a new line, no chars on this line yet */
          975                 whitespace_mode &= ~2; /* no chars on this line yet */
          976                 nbytesline = 0;
          977                 ncells = 0;
          978 
          979                 if (hadnewline)
          980                         currentnewlines++; /* repeating newlines */
          981                 hadnewline = 1;
          982         } else {
          983                 hadnewline = 0;
          984                 currentnewlines = 0;
          985         }
          986 
          987         /* skip initial/leading white-space */
          988         if (ISSPACE((unsigned char)c)) {
          989                 if (skipinitialws)
          990                         return;
          991         } else {
          992                 skipinitialws = 0;
          993         }
          994 
          995         if (!(c == '\n' || c == '\t' || !ISCNTRL((unsigned char)c)))
          996                 return;
          997 
          998         if (!linewrap) {
          999                 if (c == '\n') {
         1000                         putchar('\n');
         1001                         nbytesline = 0;
         1002                         ncells = 0;
         1003                 } else {
         1004                         if (!nbytesline) {
         1005                                 if (curmarkup)
         1006                                         emitmarkup(0);
         1007                                 rindent();
         1008                                 /* emit code again per line, needed for GNU/less -R */
         1009                                 if (curmarkup)
         1010                                         emitmarkup(curmarkup);
         1011                         }
         1012                         putchar(c);
         1013                         nbytesline++;
         1014                         ncells += utfwidth(c);
         1015                 }
         1016                 return;
         1017         }
         1018 
         1019         /* really too long: the whole word doesn't even fit, flush it */
         1020         if (ncells + rnbufcells >= termwidth || rbuflen >= sizeof(rbuf) - 1) {
         1021                 putchar('\n');
         1022                 nbytesline = 0;
         1023                 ncells = 0;
         1024                 hflush();
         1025         }
         1026 
         1027         if (c == '\n') {
         1028                 putchar('\n');
         1029                 hflush();
         1030                 return;
         1031         } else if (ISSPACE((unsigned char)c) || c == '-') {
         1032                 if (ncells + rnbufcells >= termwidth) {
         1033                         putchar('\n');
         1034                         nbytesline = 0;
         1035                         ncells = 0;
         1036                 }
         1037                 rbuf[rbuflen++] = c;
         1038                 rnbufcells += utfwidth(c);
         1039                 hflush();
         1040                 return;
         1041         }
         1042 
         1043         rbuf[rbuflen++] = c;
         1044         rnbufcells += utfwidth(c);
         1045 }
         1046 
         1047 /* calculate indentation of current node depth, using the sum of each
         1048    indentation per node */
         1049 static int
         1050 calcindent(void)
         1051 {
         1052         int i, n = 0;
         1053 
         1054         for (i = curnode; i >= 0; i--)
         1055                 n += nodes[i].indent;
         1056 
         1057         return n;
         1058 }
         1059 
         1060 static void
         1061 hprint(const char *s)
         1062 {
         1063         for (; *s; ++s)
         1064                 hputchar(*s);
         1065 }
         1066 
         1067 /* printf(), max 256 bytes for now */
         1068 static void
         1069 hprintf(const char *fmt, ...)
         1070 {
         1071         va_list ap;
         1072         char buf[256];
         1073 
         1074         va_start(ap, fmt);
         1075         vsnprintf(buf, sizeof(buf), fmt, ap);
         1076         va_end(ap);
         1077 
         1078         /* use hprint() formatting logic. */
         1079         hprint(buf);
         1080 }
         1081 
         1082 static void
         1083 hprinturi(const char *s)
         1084 {
         1085         static char hex[] = "0123456789ABCDEF";
         1086         unsigned char c;
         1087 
         1088         for (; *s; s++) {
         1089                 c = (unsigned char)*s;
         1090                 if (ISCNTRL((unsigned char)*s)) {
         1091                         hputchar('%');
         1092                         hputchar(hex[c >> 4]);
         1093                         hputchar(hex[c & 0x0f]);
         1094                 } else {
         1095                         hputchar(*s);
         1096                 }
         1097         }
         1098 }
         1099 
         1100 static void
         1101 newline(void)
         1102 {
         1103         if (skipinitialws)
         1104                 return;
         1105         hputchar('\n');
         1106 }
         1107 
         1108 static int
         1109 parentcontainerhasdata(int curtype, int n)
         1110 {
         1111         int i;
         1112 
         1113         for (i = n; i >= 0; i--) {
         1114                 if (nodes[i].tag.displaytype & (DisplayList|DisplayTable))
         1115                         break;
         1116                 if (nodes[i].hasdata)
         1117                         return 1;
         1118         }
         1119 
         1120         return 0;
         1121 }
         1122 
         1123 /* start on a newline for the start of a block element or not */
         1124 static void
         1125 startblock(void)
         1126 {
         1127         hflush();
         1128         whitespace_mode &= ~2; /* no characters on this line yet */
         1129         if (nbytesline <= 0)
         1130                 return;
         1131         if (!hadnewline && curnode >= 0 && nodes[curnode - 1].hasdata)
         1132                 hputchar('\n');
         1133 }
         1134 
         1135 /* start on a newline for the end of a block element or not */
         1136 static void
         1137 endblock(void)
         1138 {
         1139         hflush();
         1140         whitespace_mode &= ~2; /* no characters on this line yet */
         1141         if (nbytesline <= 0)
         1142                 return;
         1143         if (!hadnewline)
         1144                 hputchar('\n');
         1145 }
         1146 
         1147 /* print one character safely: no control characters,
         1148    handle HTML white-space rules */
         1149 static void
         1150 printc(int c)
         1151 {
         1152         if (ISSPACE((unsigned char)c)) {
         1153                 /* no white-space previously emitted and there are other
         1154                    characters on this line. */
         1155                 if (whitespace_mode == 2)
         1156                         hputchar(' ');
         1157                 whitespace_mode |= 1; /* whitespace emitted */
         1158         } else {
         1159                 whitespace_mode = 2;
         1160                 if (!ISCNTRL((unsigned char)c))
         1161                         hputchar(c);
         1162         }
         1163 }
         1164 
         1165 static void
         1166 printpre(const char *s, size_t len)
         1167 {
         1168         struct node *cur;
         1169         size_t i;
         1170 
         1171         /* reset state of newlines because this data is printed literally */
         1172         hadnewline = 0;
         1173         currentnewlines = 0;
         1174 
         1175         /* skip leading newline */
         1176         i = 0;
         1177         if (skipinitialws) {
         1178                 if (*s == '\n' && i < len) {
         1179                         s++;
         1180                         i++;
         1181                 }
         1182         }
         1183 
         1184         hflush();
         1185 
         1186         skipinitialws = 0;
         1187 
         1188         if (*s) {
         1189                 cur = &nodes[curnode];
         1190                 cur->hasdata = 1;
         1191         }
         1192 
         1193         for (; i < len; s++, i++) {
         1194                 switch (*s) {
         1195                 case '\n':
         1196                         putchar('\n');
         1197                         nbytesline = 0;
         1198                         ncells = 0;
         1199                         break;
         1200                 case '\t':
         1201                         hadnewline = 0;
         1202                         if (!nbytesline) {
         1203                                 if (curmarkup)
         1204                                         emitmarkup(0);
         1205                                 rindent();
         1206                                 /* emit code again per line, needed for GNU/less -R */
         1207                                 if (curmarkup)
         1208                                         emitmarkup(curmarkup);
         1209                         }
         1210 
         1211                         /* TAB to 8 spaces */
         1212                         fputs("        ", stdout);
         1213                         nbytesline += 8;
         1214                         ncells += 8;
         1215                         break;
         1216                 default:
         1217                         if (ISCNTRL((unsigned char)*s))
         1218                                 continue;
         1219 
         1220                         if (!nbytesline) {
         1221                                 if (curmarkup)
         1222                                         emitmarkup(0);
         1223                                 rindent();
         1224                                 /* emit code again per line, needed for GNU/less -R */
         1225                                 if (curmarkup)
         1226                                         emitmarkup(curmarkup);
         1227                         }
         1228 
         1229                         putchar(*s);
         1230                         nbytesline++;
         1231                         /* start of rune: incorrectly assume 1 rune is 1 cell for now */
         1232                         ncells += utfwidth((unsigned char)*s);
         1233                 }
         1234         }
         1235 }
         1236 
         1237 static struct node *
         1238 findparenttype(int cur, int findtype)
         1239 {
         1240         int i;
         1241 
         1242         for (i = cur; i >= 0; i--) {
         1243                 if ((nodes[i].tag.displaytype & findtype))
         1244                         return &nodes[i];
         1245         }
         1246         return NULL;
         1247 }
         1248 
         1249 static int
         1250 isclassmatch(const char *haystack, const char *needle)
         1251 {
         1252         const char *p;
         1253         size_t needlelen;
         1254         size_t matched = 0;
         1255 
         1256         needlelen = strlen(needle);
         1257         for (p = haystack; *p; p++) {
         1258                 if (ISSPACE((unsigned char)*p)) {
         1259                         matched = 0;
         1260                         continue;
         1261                 }
         1262                 if (needle[matched] == *p)
         1263                         matched++;
         1264                 else
         1265                         matched = 0;
         1266                 if (matched == needlelen) {
         1267                         if (*(p + 1) == '\0' || ISSPACE((unsigned char)*(p + 1)))
         1268                                 return 1;
         1269                 }
         1270         }
         1271 
         1272         return 0;
         1273 }
         1274 
         1275 /* very limited CSS-like selector, supports: main, main#id, main.class,
         1276    ".class", "#id", "ul li a" */
         1277 static int
         1278 compileselector(const char *sel, struct selectornode *nodes, size_t maxnodes)
         1279 {
         1280         int depth = 0, len;
         1281         long l;
         1282         const char *s, *start;
         1283         char tmp[256];
         1284         int nameset = 0;
         1285 
         1286         memset(&nodes[0], 0, sizeof(nodes[0]));
         1287         nodes[0].index = -1;
         1288 
         1289         s = sel;
         1290         for (; *s && ISSPACE((unsigned char)*s); s++)
         1291                 ;
         1292 
         1293         start = s;
         1294         for (; ; s++) {
         1295                 /* end of tag */
         1296                 if (!nameset &&
         1297                     (*s == '#' || *s == '.' || *s == '@' ||
         1298                      *s == '\0' || ISSPACE((unsigned char)*s))) {
         1299                         nameset = 1;
         1300                         len = s - start; /* tag name */
         1301                         if (len >= sizeof(tmp))
         1302                                 return 0;
         1303                         if (len)
         1304                                 memcpy(tmp, start, len);
         1305                         tmp[len] = '\0';
         1306 
         1307                         memcpy(nodes[depth].tagname, tmp, len + 1);
         1308                 }
         1309 
         1310                 /* end */
         1311                 if (*s == '\0' || ISSPACE((unsigned char)*s)) {
         1312                         for (; ISSPACE((unsigned char)*s); s++)
         1313                                 ;
         1314                         start = s; /* start of a new tag */
         1315                         depth++;
         1316                         if (depth >= maxnodes)
         1317                                 return 0;
         1318 
         1319                         nameset = 0;
         1320                         memset(&nodes[depth], 0, sizeof(nodes[depth]));
         1321                         nodes[depth].index = -1;
         1322 
         1323                         /* end of selector */
         1324                         if (*s == '\0')
         1325                                 break;
         1326                 }
         1327 
         1328                 /* index */
         1329                 if (*s == '@') {
         1330                         len = strcspn(s + 1, ".#@ \t\n");
         1331                         if (len >= sizeof(tmp))
         1332                                 return 0;
         1333                         memcpy(tmp, s + 1, len);
         1334                         tmp[len] = '\0';
         1335 
         1336                         l = strtol(tmp, NULL, 10);
         1337                         if (l >= 0)
         1338                                 nodes[depth].index = l;
         1339                         s += len;
         1340                         start = s + 1;
         1341                         continue;
         1342                 }
         1343 
         1344                 /* id */
         1345                 if (*s == '#') {
         1346                         len = strcspn(s + 1, ".#@ \t\n");
         1347                         if (len >= sizeof(tmp))
         1348                                 return 0;
         1349                         memcpy(tmp, s + 1, len);
         1350                         tmp[len] = '\0';
         1351                         memcpy(nodes[depth].id, tmp, len + 1);
         1352                         s += len;
         1353                         start = s + 1;
         1354                         continue;
         1355                 }
         1356 
         1357                 /* class */
         1358                 if (*s == '.') {
         1359                         len = strcspn(s + 1, ".#@ \t\n");
         1360                         if (len >= sizeof(tmp))
         1361                                 return 0;
         1362                         memcpy(tmp, s + 1, len);
         1363                         tmp[len] = '\0';
         1364                         /* allow only one classname for now */
         1365                         memcpy(nodes[depth].classnames, tmp, len + 1);
         1366                         s += len;
         1367                         start = s + 1;
         1368                         continue;
         1369                 }
         1370         }
         1371 
         1372         return depth;
         1373 }
         1374 
         1375 static struct selector *
         1376 newselector(const char *q)
         1377 {
         1378         struct selector *sel;
         1379         int r;
         1380 
         1381         sel = ecalloc(1, sizeof(*sel));
         1382         sel->text = estrdup(q);
         1383 
         1384         r = compileselector(sel->text, sel->nodes, LEN(sel->nodes));
         1385         if (r <= 0) {
         1386                 free(sel->text);
         1387                 free(sel);
         1388                 return NULL;
         1389         }
         1390         sel->depth = r;
         1391 
         1392         return sel;
         1393 }
         1394 
         1395 static struct selectors *
         1396 compileselectors(const char *q)
         1397 {
         1398         struct selectors *sels = NULL;
         1399         struct selector *sel;
         1400         const char *start;
         1401         char *qe;
         1402         int count = 0;
         1403         size_t i, siz;
         1404 
         1405         sels = ecalloc(1, sizeof(*sels));
         1406 
         1407         start = q;
         1408         for (; ; q++) {
         1409                 if (*q == ',' || *q == '\0') {
         1410                         qe = estrndup(start, q - start);
         1411                         sel = newselector(qe);
         1412                         free(qe);
         1413 
         1414                         /* invalid selector */
         1415                         if (!sel)
         1416                                 goto invalid;
         1417 
         1418                         /* add new selector */
         1419                         siz = (count + 1) * sizeof(struct selector *);
         1420                         sels->selectors = erealloc(sels->selectors, siz);
         1421                         sels->selectors[count] = sel;
         1422                         count++;
         1423 
         1424                         if (*q == '\0')
         1425                                 break;
         1426                         start = q + 1;
         1427                 }
         1428         }
         1429         sels->count = count;
         1430 
         1431         return sels;
         1432 
         1433 invalid:
         1434         for (i = 0; i < count; i++) {
         1435                 sel = sels->selectors[i];
         1436                 free(sel->text);
         1437                 free(sel);
         1438         }
         1439         free(sels->selectors);
         1440         free(sels);
         1441 
         1442         return NULL;
         1443 }
         1444 
         1445 /* very limited CSS-like matcher, supports: main, main#id, main.class,
         1446    ".class", "#id", "ul li a" */
         1447 static int
         1448 iscssmatch(struct selector *sel, struct node *root, int maxdepth)
         1449 {
         1450         int d, md = 0;
         1451 
         1452         for (d = 0; d <= maxdepth; d++) {
         1453                 /* tag matched? */
         1454                 if (sel->nodes[md].tagname[0] &&
         1455                     strcasecmp(sel->nodes[md].tagname, root[d].tagname))
         1456                         continue; /* no */
         1457 
         1458                 /* id matched? */
         1459                 if (sel->nodes[md].id[0] && strcmp(sel->nodes[md].id, root[d].id))
         1460                         continue; /* no */
         1461 
         1462                 /* class matched, for now allow only one classname in the selector,
         1463                    matching multiple classnames */
         1464                 if (sel->nodes[md].classnames[0] &&
         1465                     !isclassmatch(root[d].classnames, sel->nodes[md].classnames))
         1466                         continue; /* no */
         1467 
         1468                 /* index matched */
         1469                 if (sel->nodes[md].index != -1 &&
         1470                     (d == 0 ||
         1471                     root[d - 1].nchildren == 0 ||
         1472                     sel->nodes[md].index != root[d - 1].nchildren - 1))
         1473                         continue;
         1474 
         1475                 md++;
         1476                 /* all matched of one selector */
         1477                 if (md == sel->depth)
         1478                         return 1;
         1479         }
         1480 
         1481         return 0;
         1482 }
         1483 
         1484 static int
         1485 iscssmatchany(struct selectors *sels, struct node *root, int maxdepth)
         1486 {
         1487         struct selector *sel;
         1488         int i;
         1489 
         1490         for (i = 0; i < sels->count; i++) {
         1491                 sel = sels->selectors[i];
         1492                 if (iscssmatch(sel, root, maxdepth))
         1493                         return 1;
         1494         }
         1495         return 0;
         1496 }
         1497 
         1498 static void
         1499 handleinlinealt(void)
         1500 {
         1501         struct node *cur;
         1502         char *start, *s, *e;
         1503 
         1504         /* do not show the alt text if the element is hidden */
         1505         cur = &nodes[curnode];
         1506         if (reader_ignore || (cur->tag.displaytype & DisplayNone))
         1507                 return;
         1508 
         1509         /* show img alt attribute as text. */
         1510         if (attr_alt.len) {
         1511                 start = attr_alt.data;
         1512                 e = attr_alt.data + attr_alt.len;
         1513 
         1514                 for (s = start; s < e; s++)
         1515                         printc((unsigned char)*s);
         1516                 hflush();
         1517         } else if (cur->tag.id == TagImg && !showurlinline) {
         1518                 /* if there is no alt text and no URL is shown inline, then
         1519                    show "[IMG]" to indicate there was an image there */
         1520                 hprint("[IMG]");
         1521         }
         1522 }
         1523 
         1524 /* lookup a link reference by URL in the red-black tree */
         1525 static struct linkref *
         1526 findlinkref(const char *url)
         1527 {
         1528         struct linkref find;
         1529 
         1530         find.url = (char *)url;
         1531 
         1532         return RB_FIND(linkreftree, &linkrefhead, &find);
         1533 }
         1534 
         1535 /* add a link reference. Returns the added link reference, or the existing link
         1536    reference if links are deduplicated */
         1537 static struct linkref *
         1538 addlinkref(const char *url, const char *_type, enum TagId tagid, int ishidden)
         1539 {
         1540         struct linkref *link;
         1541         size_t linknr;
         1542 
         1543         /* if links are deduplicates return the existing link */
         1544         if (uniqrefs && (link = findlinkref(url)))
         1545                 return link;
         1546 
         1547         if (tagid == TagA)
         1548                 _type = "link";
         1549 
         1550         link = ecalloc(1, sizeof(*link));
         1551 
         1552         if (!ishidden) {
         1553                 linknr = ++nvisrefs;
         1554                 if (nvisrefs >= ncapvisrefs) {
         1555                         ncapvisrefs += 256; /* greedy alloc */
         1556                         visrefs = erealloc(visrefs, sizeof(*visrefs) * ncapvisrefs);
         1557                 }
         1558                 visrefs[linknr - 1] = link; /* add pointer to list */
         1559         } else {
         1560                 linknr = ++nhiddenrefs;
         1561                 if (nhiddenrefs >= ncaphiddenrefs) {
         1562                         ncaphiddenrefs += 256; /* greedy alloc */
         1563                         hiddenrefs = erealloc(hiddenrefs, sizeof(*hiddenrefs) * ncaphiddenrefs);
         1564                 }
         1565                 hiddenrefs[linknr - 1] = link; /* add pointer to list */
         1566         }
         1567 
         1568         link->url = estrdup(url);
         1569         link->type = estrdup(_type);
         1570         link->tagid = tagid;
         1571         link->ishidden = ishidden;
         1572         link->linknr = linknr;
         1573 
         1574         /* add to tree: the tree is only used for checking unique link references */
         1575         if (uniqrefs)
         1576                 RB_INSERT(linkreftree, &linkrefhead, link);
         1577 
         1578         return link;
         1579 }
         1580 
         1581 static void
         1582 handleinlinelink(void)
         1583 {
         1584         struct uri newuri, olduri;
         1585         struct node *cur;
         1586         char buf[4096], *url;
         1587         int r;
         1588 
         1589         if (!showrefbottom && !showrefinline && !showurlinline && !xresources && !allowosc8)
         1590                 return; /* there is no need to collect the reference */
         1591 
         1592         if (!attr_href.len && !attr_src.len && !attr_data.len)
         1593                 return; /* there is no reference */
         1594 
         1595         /* by default use the original URL */
         1596         if (attr_src.len)
         1597                 url = attr_src.data;
         1598         else if (attr_href.len)
         1599                 url = attr_href.data;
         1600         else
         1601                 url = attr_data.data;
         1602 
         1603         if (!url)
         1604                 return;
         1605 
         1606         /* Not an absolute URL yet: try to make it absolute.
         1607            If it is not possible use the relative URL */
         1608         if (!uri_hasscheme(url) && basehrefset &&
         1609             uri_parse(url, &olduri) != -1 &&
         1610             uri_makeabs(&newuri, &olduri, &base) != -1 &&
         1611             newuri.proto[0]) {
         1612                 r = uri_format(buf, sizeof(buf), &newuri);
         1613                 if (r >= 0 && (size_t)r < sizeof(buf))
         1614                         url = buf;
         1615         }
         1616 
         1617         if (!url[0])
         1618                 return;
         1619 
         1620         cur = &nodes[curnode];
         1621 
         1622         if (!(cur->tag.displaytype & DisplayNone)) {
         1623                 string_clear(&nodes_links[curnode]);
         1624                 string_append(&nodes_links[curnode], url, strlen(url));
         1625         }
         1626 
         1627         /* add hidden links directly to the reference,
         1628            the order doesn't matter */
         1629         if (cur->tag.displaytype & DisplayNone)
         1630                 addlinkref(url, cur->tag.name, cur->tag.id, 1);
         1631 }
         1632 
         1633 static void
         1634 printlinkrefs(void)
         1635 {
         1636         struct linkref *ref;
         1637         size_t i;
         1638 
         1639         if (!nvisrefs && !nhiddenrefs)
         1640                 return;
         1641 
         1642         if (xresources) {
         1643                 for (i = 0; i < nvisrefs; i++) {
         1644                         ref = visrefs[i];
         1645                         dprintf(3, "%s\tv\t%s\n", ref->type, ref->url);
         1646                 }
         1647                 for (i = 0; i < nhiddenrefs; i++) {
         1648                         ref = hiddenrefs[i];
         1649                         dprintf(3, "%s\th\t%s\n", ref->type, ref->url);
         1650                 }
         1651         }
         1652 
         1653         if (showrefbottom) {
         1654                 printf("\nReferences\n\n");
         1655 
         1656                 for (i = 0; i < nvisrefs; i++) {
         1657                         ref = visrefs[i];
         1658 
         1659                         printf(" %zu. ", ref->linknr);
         1660                         printuri(ref->url);
         1661                         printf(" (%s)\n", ref->type);
         1662                 }
         1663 
         1664                 if (nhiddenrefs > 0)
         1665                         printf("\n\nHidden references\n\n");
         1666                 /* hidden links don't have a visible link number */
         1667                 for (i = 0; i < nhiddenrefs; i++) {
         1668                         ref = hiddenrefs[i];
         1669 
         1670                         printf(" %zu. ", ref->linknr);
         1671                         printuri(ref->url);
         1672                         printf(" (%s)\n", ref->type);
         1673                 }
         1674         }
         1675 }
         1676 
         1677 /* size to grow node capacity (greedy) */
         1678 #define NODE_CAP_INC 16
         1679 
         1680 /* increase node depth, allocate space for nodes if needed */
         1681 static void
         1682 incnode(void)
         1683 {
         1684         size_t i;
         1685 
         1686         curnode++;
         1687 
         1688         if (curnode >= MAX_NODE_DEPTH)
         1689                 errx(1, "max node depth reached: %d", curnode);
         1690 
         1691         if (curnode >= ncapnodes) {
         1692                 nodes = erealloc(nodes, sizeof(*nodes) * (ncapnodes + NODE_CAP_INC));
         1693                 nodes_links = erealloc(nodes_links, sizeof(*nodes_links) * (ncapnodes + NODE_CAP_INC));
         1694 
         1695                 /* clear new region */
         1696                 memset(&nodes[ncapnodes], 0, sizeof(*nodes) * NODE_CAP_INC);
         1697                 memset(&nodes_links[ncapnodes], 0, sizeof(*nodes_links) * NODE_CAP_INC);
         1698 
         1699                 for (i = 0; i < ncapnodes; i++)
         1700                         nodes[i].tag.name = nodes[i].tagname; /* assign to use fixed-size buffer */
         1701 
         1702                 for (i = ncapnodes; i < ncapnodes + NODE_CAP_INC; i++) {
         1703                         nodes[i].tag.displaytype = DisplayInline;
         1704                         nodes[i].tag.name = nodes[i].tagname; /* assign to use fixed-size buffer */
         1705                 }
         1706 
         1707                 ncapnodes += NODE_CAP_INC; /* greedy alloc */
         1708         }
         1709 }
         1710 
         1711 static void
         1712 xmldatastart(XMLParser *p)
         1713 {
         1714 }
         1715 
         1716 static void
         1717 printhtmldata(void)
         1718 {
         1719         char *start, *s, *e;
         1720 
         1721         if (!htmldata.data || !htmldata.len)
         1722                 return;
         1723 
         1724         start = htmldata.data;
         1725         e = htmldata.data + htmldata.len;
         1726 
         1727         for (s = start; s < e; s++)
         1728                 printc((unsigned char)*s);
         1729 }
         1730 
         1731 static void
         1732 xmldataend(XMLParser *p)
         1733 {
         1734         struct node *cur;
         1735 
         1736         if (!htmldata.data || !htmldata.len)
         1737                 return;
         1738 
         1739         cur = &nodes[curnode];
         1740 
         1741         if (reader_ignore || (cur->tag.displaytype & DisplayNone)) {
         1742                 /* print nothing */
         1743         } else if ((cur->tag.displaytype & DisplayPre) ||
         1744                    findparenttype(curnode - 1, DisplayPre)) {
         1745                 printpre(htmldata.data, htmldata.len);
         1746         } else {
         1747                 printhtmldata();
         1748         }
         1749 
         1750         string_clear(&htmldata);
         1751 }
         1752 
         1753 static void
         1754 xmldata(XMLParser *p, const char *data, size_t datalen)
         1755 {
         1756         struct node *cur;
         1757 
         1758         if (reader_ignore)
         1759                 return;
         1760 
         1761         cur = &nodes[curnode];
         1762         if (cur->tag.displaytype & DisplayNone)
         1763                 return;
         1764 
         1765         string_append(&htmldata, data, datalen);
         1766 }
         1767 
         1768 static void
         1769 xmldataentity(XMLParser *p, const char *data, size_t datalen)
         1770 {
         1771         struct node *cur;
         1772         char buf[8];
         1773         int len;
         1774 
         1775         if (reader_ignore)
         1776                 return;
         1777 
         1778         cur = &nodes[curnode];
         1779         if (cur->tag.displaytype & DisplayNone)
         1780                 return;
         1781 
         1782         len = xml_entitytostr(data, buf, sizeof(buf));
         1783         if (len > 0)
         1784                 xmldata(p, buf, (size_t)len);
         1785         else
         1786                 xmldata(p, data, datalen);
         1787 }
         1788 
         1789 static void
         1790 xmlcdatastart(XMLParser *p)
         1791 {
         1792         xmldatastart(p);
         1793 }
         1794 
         1795 static void
         1796 xmlcdataend(XMLParser *p)
         1797 {
         1798         xmldataend(p); /* treat CDATA as data */
         1799 }
         1800 
         1801 static void
         1802 xmlcdata(XMLParser *p, const char *data, size_t datalen)
         1803 {
         1804         xmldata(p, data, datalen); /* treat CDATA as data */
         1805 }
         1806 
         1807 /* lookup function to compare tag name (case-insensitive) for sort functions */
         1808 static int
         1809 findtagcmp(const void *v1, const void *v2)
         1810 {
         1811         struct tag *t1 = (struct tag *)v1;
         1812         struct tag *t2 = (struct tag *)v2;
         1813 
         1814         return strcasecmp(t1->name, t2->name);
         1815 }
         1816 
         1817 /* binary search tag by tag name */
         1818 static struct tag *
         1819 findtag(const char *t)
         1820 {
         1821         struct tag find = { 0 };
         1822 
         1823         find.name = t;
         1824 
         1825         return bsearch(&find, tags, LEN(tags), sizeof(*tags), findtagcmp);
         1826 }
         1827 
         1828 /* start OSC 8 (hyperlink) sequence */
         1829 static void
         1830 handleosc8(void)
         1831 {
         1832         struct node *cur;
         1833 
         1834         if (!allowosc8 || nodes_links[curnode].len == 0)
         1835                 return;
         1836 
         1837         /* do not show the link if the element is hidden */
         1838         cur = &nodes[curnode];
         1839         if (reader_ignore || (cur->tag.displaytype & DisplayNone))
         1840                 return;
         1841 
         1842         if (!nbytesline) {
         1843                 if (curmarkup)
         1844                         emitmarkup(0);
         1845                 rindent();
         1846                 /* emit code again per line, needed for GNU/less -R */
         1847                 if (curmarkup)
         1848                         emitmarkup(curmarkup);
         1849         }
         1850         hflush();
         1851         fputs("\033]8;;", stdout);
         1852         printuri(nodes_links[curnode].data);
         1853         fputs("\033\\", stdout);
         1854 }
         1855 
         1856 static void
         1857 handleendtag(struct tag *tag)
         1858 {
         1859         int i, marginbottom;
         1860 
         1861         if (tag->displaytype & DisplayNone)
         1862                 return;
         1863         if (reader_ignore)
         1864                 return;
         1865 
         1866         /* end OSC 8 (hyperlink sequence) */
         1867         if (allowosc8 && nodes_links[curnode].len > 0) {
         1868                 hflush();
         1869                 fputs("\033]8;;\033\\", stdout);
         1870         }
         1871 
         1872         if (tag->displaytype & DisplayQuote) {
         1873                 hputchar('"');
         1874                 hflush();
         1875         }
         1876         if (tag->displaytype & (DisplayButton | DisplayOption)) {
         1877                 hputchar(']');
         1878                 hflush();
         1879         }
         1880         if (tag->displaytype & (DisplayBlock | DisplayHeader | DisplayTable | DisplayTableRow |
         1881                 DisplayList | DisplayListItem | DisplayPre)) {
         1882                 endblock(); /* break line if needed */
         1883         }
         1884 
         1885         /* when a list ends and its not inside a list add an extra bottom margin */
         1886         marginbottom = tag->marginbottom;
         1887 
         1888         if (marginbottom > 0) {
         1889                 if (tag->displaytype & DisplayList) {
         1890                         if (findparenttype(curnode - 1, DisplayList))
         1891                                 marginbottom--;
         1892                 }
         1893         }
         1894 
         1895         if (marginbottom > 0) {
         1896                 hflush();
         1897                 for (i = currentnewlines; i < marginbottom; i++) {
         1898                         putchar('\n');
         1899                         nbytesline = 0;
         1900                         ncells = 0;
         1901                         currentnewlines++;
         1902                 }
         1903                 hadnewline = 1;
         1904         }
         1905 }
         1906 
         1907 static void
         1908 endnode(struct node *cur)
         1909 {
         1910         struct linkref *ref;
         1911         int i, ishidden;
         1912 
         1913         /* set a flag indicating the element and its parent containers have data.
         1914            This is used for some formatting */
         1915         if (cur->hasdata) {
         1916                 for (i = curnode; i >= 0; i--)
         1917                         nodes[i].hasdata = 1;
         1918         }
         1919 
         1920         endmarkup(cur->tag.markuptype);
         1921 
         1922         ishidden = reader_ignore || (cur->tag.displaytype & DisplayNone);
         1923 
         1924         /* add link and show the link number in the visible order */
         1925         if (nodes_links[curnode].len > 0) {
         1926                 ref = addlinkref(nodes_links[curnode].data,
         1927                         cur->tag.name, cur->tag.id, ishidden);
         1928 
         1929                 if (!ishidden) {
         1930                         if (showrefinline || showurlinline) {
         1931                                 hflush();
         1932                                 startmarkup(MarkupReverse);
         1933                         }
         1934 
         1935                         if (showrefinline)
         1936                                 hprintf("[%zu]", ref->linknr);
         1937                         if (showurlinline) {
         1938                                 hputchar('[');
         1939                                 if (ref->tagid != TagA) {
         1940                                         hprint(ref->type);
         1941                                         hprint(": ");
         1942                                 }
         1943                                 hprinturi(ref->url);
         1944                                 hputchar(']');
         1945                         }
         1946                         if (showrefinline || showurlinline) {
         1947                                 endmarkup(MarkupReverse);
         1948                                 hflush();
         1949                         }
         1950                 }
         1951         }
         1952 
         1953         handleendtag(&(cur->tag));
         1954 }
         1955 
         1956 static void
         1957 xmltagend(XMLParser *p, const char *t, size_t tl, int isshort)
         1958 {
         1959         struct tag *found, *tag;
         1960         enum TagId childs[16];
         1961         size_t nchilds;
         1962         int i, j, k, nchildfound, parenttype;
         1963 
         1964         /* match tag and lookup metadata */
         1965         /* ignore closing of void elements, like </br>, which is not allowed */
         1966         if ((found = findtag(t))) {
         1967                 if (!isshort && found->isvoid)
         1968                         return;
         1969         }
         1970 
         1971         /* TODO: implement more complete optional tag handling.
         1972            in reality the optional tag rules are more complex, see:
         1973            https://html.spec.whatwg.org/multipage/syntax.html#optional-tags */
         1974 
         1975         nchilds = 0;
         1976         nchildfound = 0;
         1977         parenttype = 0; /* by default, seek until the root */
         1978 
         1979         if (found && found->displaytype & DisplayPre) {
         1980                 skipinitialws = 0; /* do not skip white-space, for margins */
         1981         } else if (found && found->displaytype & DisplayList) {
         1982                 childs[0] = TagLi;
         1983                 nchilds = 1;
         1984                 parenttype = DisplayList;
         1985         } else if (found && found->displaytype & DisplayTableRow) {
         1986                 childs[0] = TagTd;
         1987                 nchilds = 1;
         1988                 parenttype = DisplayTableRow;
         1989         } else if (found && found->displaytype & DisplayTable) {
         1990                 childs[0] = TagTd;
         1991                 nchilds = 1;
         1992                 parenttype = DisplayTable;
         1993         } else if (found && found->displaytype & DisplaySelect) {
         1994                 childs[0] = TagOption;
         1995                 nchilds = 1;
         1996                 parenttype = DisplaySelect;
         1997         } else if (found && found->displaytype & DisplayDl) {
         1998                 childs[0] = TagP;
         1999                 childs[1] = TagDd;
         2000                 childs[2] = TagDt;
         2001                 nchilds = 3;
         2002                 parenttype = DisplayDl;
         2003         } else if (found && found->displaytype & DisplayBlock) {
         2004                 childs[0] = TagP;
         2005                 nchilds = 1;
         2006                 parenttype = 0; /* seek until the root */
         2007         }
         2008 
         2009         if (nchilds > 0) {
         2010                 for (i = curnode; i >= 0; i--) {
         2011                         if (nchildfound)
         2012                                 break;
         2013                         if ((nodes[i].tag.displaytype & parenttype))
         2014                                 break;
         2015                         for (j = 0; j < nchilds; j++) {
         2016                                 if (nodes[i].tag.id == childs[j]) {
         2017                                         /* fake closing the previous tags */
         2018                                         for (k = curnode; k >= i; k--)
         2019                                                 endnode(&nodes[k]);
         2020                                         curnode = k;
         2021                                         nchildfound = 1;
         2022                                         break;
         2023                                 }
         2024                         }
         2025                 }
         2026         }
         2027 
         2028         /* if the current closing tag matches the current open tag */
         2029         if (nodes[curnode].tag.name &&
         2030             !tagcmp(nodes[curnode].tag.name, t)) {
         2031                 endnode(&nodes[curnode]);
         2032                 if (curnode)
         2033                         curnode--;
         2034         } else {
         2035                 /* ... else lookup the first matching start tag. This is also
         2036                    for handling optional closing tags */
         2037                 tag = NULL;
         2038                 for (i = curnode; i >= 0; i--) {
         2039                         if (nodes[i].tag.name &&
         2040                             !tagcmp(nodes[i].tag.name, t)) {
         2041                                 endnode(&nodes[i]);
         2042                                 curnode = i > 0 ? i - 1 : 0;
         2043                                 tag = &nodes[i].tag;
         2044                                 break;
         2045                         }
         2046                 }
         2047                 /* unmatched closing tag found */
         2048                 if (!tag && found)
         2049                         handleendtag(found);
         2050         }
         2051         indent = calcindent();
         2052 
         2053 #if 0
         2054         /* check if linewrap is enabled, but currently is disabled and needs to
         2055            be restored */
         2056         if (allowlinewrap && !linewrap) {
         2057                 tag = NULL;
         2058                 for (i = curnode; i >= 0; i--) {
         2059                         if (nodes[i].tag.id == TagTable) {
         2060                                 tag = &nodes[i].tag;
         2061                                 break;
         2062                         }
         2063                 }
         2064                 if (!tag)
         2065                         linewrap = allowlinewrap;
         2066         }
         2067 #endif
         2068 
         2069         /* restore markup of the tag we are in now */
         2070         startmarkup(nodes[curnode].tag.markuptype);
         2071 
         2072         /* check if the current node still matches the visible selector */
         2073         if (reader_mode && sel_show && !reader_ignore) {
         2074                 if (!iscssmatchany(sel_show, nodes, curnode)) {
         2075                         reader_ignore = 1;
         2076                         newline();
         2077                 }
         2078         }
         2079 }
         2080 
         2081 static void
         2082 xmltagstart(XMLParser *p, const char *t, size_t tl)
         2083 {
         2084         struct tag *found;
         2085         struct node *cur;
         2086         enum TagId tagid;
         2087         enum TagId childs[16];
         2088         size_t nchilds;
         2089         char *s;
         2090         int i, j, k, nchildfound, parenttype;
         2091 
         2092         cur = &nodes[curnode];
         2093 
         2094         string_clear(&attr_alt);
         2095         string_clear(&attr_checked);
         2096         string_clear(&attr_class);
         2097         attr_class_set = 0;
         2098         string_clear(&attr_data);
         2099         string_clear(&attr_href);
         2100         string_clear(&attr_id);
         2101         attr_id_set = 0;
         2102         string_clear(&attr_src);
         2103         string_clear(&attr_type);
         2104         string_clear(&attr_value);
         2105 
         2106         /* match tag and lookup metadata */
         2107         found = findtag(t);
         2108 
         2109         /* TODO: implement more complete optional tag handling.
         2110            in reality the optional tag rules are more complex, see:
         2111            https://html.spec.whatwg.org/multipage/syntax.html#optional-tags */
         2112 
         2113         nchilds = 0;
         2114         nchildfound = 0;
         2115         parenttype = 0; /* by default, seek until the root */
         2116 
         2117         /* if optional tag <p> is open and a list element is found, close </p>. */
         2118         if (found && (found->displaytype & DisplayList)) {
         2119                 /* not inside a list */
         2120                 childs[0] = TagP;
         2121                 nchilds = 1;
         2122                 parenttype = DisplayList;
         2123         } else if (found && found->isoptional) {
         2124                 tagid = found->id;
         2125                 if (tagid == TagLi) {
         2126                         childs[0] = tagid;
         2127                         nchilds = 1;
         2128                         parenttype = DisplayList;
         2129                 } else if (tagid == TagTd) {
         2130                         childs[0] = tagid;
         2131                         nchilds = 1;
         2132                         parenttype = DisplayTableRow;
         2133                 } else if (tagid == TagTr) {
         2134                         childs[0] = tagid;
         2135                         nchilds = 1;
         2136                         parenttype = DisplayTable;
         2137                 } else if (tagid == TagP) {
         2138                         childs[0] = tagid;
         2139                         nchilds = 1;
         2140                         parenttype = 0; /* seek until the root */
         2141                 } else if (tagid == TagOption) {
         2142                         childs[0] = tagid;
         2143                         nchilds = 1;
         2144                         parenttype = DisplaySelect;
         2145                 } else if (tagid == TagDt) {
         2146                         childs[0] = TagDd;
         2147                         nchilds = 1;
         2148                         parenttype = DisplayDl;
         2149                 } else if (tagid == TagDd) {
         2150                         childs[0] = tagid;
         2151                         childs[1] = TagDt;
         2152                         nchilds = 2;
         2153                         parenttype = DisplayDl;
         2154                 } else if (tagid == cur->tag.id) {
         2155                         /* fake closing the previous tag if it is the same and repeated */
         2156                         xmltagend(p, t, tl, 0);
         2157                 }
         2158         } else if (found && found->displaytype & DisplayBlock) {
         2159                 /* check if we have an open "<p>" tag */
         2160                 childs[0] = TagP;
         2161                 childs[1] = TagDl;
         2162                 nchilds = 2;
         2163                 parenttype = DisplayDl;
         2164         }
         2165 
         2166         if (nchilds > 0) {
         2167                 for (i = curnode; i >= 0; i--) {
         2168                         if (nchildfound)
         2169                                 break;
         2170                         if ((nodes[i].tag.displaytype & parenttype))
         2171                                 break;
         2172                         for (j = 0; j < nchilds; j++) {
         2173                                 if (nodes[i].tag.id == childs[j]) {
         2174                                         /* fake closing the previous tags */
         2175                                         for (k = curnode; k >= i; k--)
         2176                                                 xmltagend(p, nodes[k].tag.name, strlen(nodes[k].tag.name), 0);
         2177                                         nchildfound = 1;
         2178                                         break;
         2179                                 }
         2180                         }
         2181                 }
         2182         }
         2183 
         2184         incnode();
         2185         string_clear(&nodes_links[curnode]); /* clear possible link reference for this node */
         2186         cur = &nodes[curnode];
         2187         memset(cur, 0, sizeof(*cur)); /* clear / reset node */
         2188         /* tag defaults */
         2189         cur->tag.displaytype = DisplayInline;
         2190         cur->tag.name = cur->tagname; /* assign fixed-size buffer */
         2191         strlcpy(cur->tagname, t, sizeof(cur->tagname));
         2192 
         2193         /* force to lowercase */
         2194         for (s = cur->tagname; *s; s++)
         2195                 *s = TOLOWER((unsigned char)*s);
         2196 
         2197         /* matched tag: copy tag information to current node */
         2198         if (found)
         2199                 memcpy(&(cur->tag), found, sizeof(*found));
         2200 
         2201         /* if parent tag is hidden then hide itself too */
         2202         if (curnode > 0 && (nodes[curnode - 1].tag.displaytype & DisplayNone))
         2203                 cur->tag.displaytype |= DisplayNone;
         2204 }
         2205 
         2206 static void
         2207 xmltagstartparsed(XMLParser *p, const char *t, size_t tl, int isshort)
         2208 {
         2209         struct tag *found;
         2210         enum TagId tagid;
         2211         struct node *cur, *parent;
         2212         int i, margintop;
         2213 
         2214         /* match tag and lookup metadata */
         2215         tagid = 0;
         2216         if ((found = findtag(t)))
         2217                 tagid = found->id;
         2218 
         2219 #if 0
         2220         /* disable line-wrapping inside tables */
         2221         if (tagid == TagTable)
         2222                 linewrap = 0;
         2223 #endif
         2224 
         2225         cur = &nodes[curnode];
         2226 
         2227         /* copy attributes if set */
         2228         if (attr_id.len)
         2229                 strlcpy(cur->id, attr_id.data, sizeof(cur->id));
         2230         else
         2231                 cur->id[0] = '\0';
         2232         if (attr_class.len)
         2233                 strlcpy(cur->classnames, attr_class.data, sizeof(cur->classnames));
         2234         else
         2235                 cur->classnames[0] = '\0';
         2236 
         2237         /* parent node */
         2238         if (curnode > 0) {
         2239                 parent = &nodes[curnode - 1];
         2240                 parent->nchildren++; /* increase child node count */
         2241                 /* count visible childnodes */
         2242                 if (!(cur->tag.displaytype & DisplayNone))
         2243                         parent->visnchildren++;
         2244         } else {
         2245                 parent = NULL;
         2246         }
         2247 
         2248         if (reader_mode && sel_show && reader_ignore &&
         2249             iscssmatchany(sel_show, nodes, curnode))
         2250                 reader_ignore = 0;
         2251 
         2252         /* hide element */
         2253         if (reader_mode && sel_hide &&
         2254             iscssmatchany(sel_hide, nodes, curnode))
         2255                 cur->tag.displaytype |= DisplayNone;
         2256 
         2257         /* indent for this tag */
         2258         cur->indent = cur->tag.indent;
         2259 
         2260         /* add link reference */
         2261         handleinlinelink();
         2262         /* handle OSC8 start sequence */
         2263         handleosc8();
         2264         /* print inline links and alt text */
         2265         handleinlinealt();
         2266 
         2267         /* <select><option> */
         2268         if ((cur->tag.displaytype & DisplayOption) && parent) {
         2269                 /* <select multiple>: show all options */
         2270                 if (parent->tag.displaytype & DisplaySelectMulti)
         2271                         cur->tag.displaytype |= DisplayBlock;
         2272                 else if (parent->nchildren > 1) /* show the first item as selected */
         2273                         cur->tag.displaytype |= DisplayNone; /* else hide */
         2274         }
         2275 
         2276         if (cur->tag.displaytype & DisplayNone)
         2277                 return;
         2278 
         2279         if (reader_ignore)
         2280                 return;
         2281 
         2282         indent = calcindent();
         2283 
         2284         if ((cur->tag.displaytype & (DisplayBlock | DisplayHeader | DisplayPre |
         2285                 DisplayTable | DisplayTableRow |
         2286                 DisplayList | DisplayListItem))) {
         2287                 startblock(); /* break line if needed */
         2288         }
         2289 
         2290         if (cur->tag.displaytype & DisplayQuote) {
         2291                 hflush();
         2292                 hputchar('"');
         2293         }
         2294         if (cur->tag.displaytype & (DisplayButton | DisplayOption)) {
         2295                 hflush();
         2296                 hputchar('[');
         2297         }
         2298 
         2299         margintop = cur->tag.margintop;
         2300         if (cur->tag.displaytype & (DisplayList)) {
         2301                 for (i = curnode - 1; i >= 0; i--) {
         2302                         if (nodes[i].tag.displaytype & DisplayList)
         2303                                 break;
         2304                         if (!(nodes[i].tag.displaytype & DisplayListItem))
         2305                                 continue;
         2306                         if (nodes[i].hasdata && margintop > 0) {
         2307                                 margintop--;
         2308                                 break;
         2309                         }
         2310                 }
         2311         } else if (cur->tag.displaytype & (DisplayBlock|DisplayTable)) {
         2312                 if (!parentcontainerhasdata(cur->tag.displaytype, curnode - 1)) {
         2313                         if (margintop > 0)
         2314                                 margintop--;
         2315                 }
         2316         }
         2317 
         2318         if (margintop > 0) {
         2319                 hflush();
         2320                 for (i = currentnewlines; i < margintop; i++) {
         2321                         putchar('\n');
         2322                         nbytesline = 0;
         2323                         ncells = 0;
         2324                         currentnewlines++;
         2325                 }
         2326                 hadnewline = 1;
         2327         }
         2328 
         2329         if (cur->tag.displaytype & DisplayPre) {
         2330                 skipinitialws = 1;
         2331         } else if (cur->tag.displaytype & DisplayTableCell) {
         2332                 if (parent && parent->visnchildren > 1)
         2333                         hputchar('\t');
         2334         } else if (cur->tag.displaytype & DisplayListItem) {
         2335                 /* find first parent node and ordered numbers or unordered */
         2336                 if (parent) {
         2337                         skipinitialws = 0;
         2338 
         2339                         /* print bullet, add columns to indentation level */
         2340                         if (parent->tag.displaytype & DisplayListOrdered) {
         2341                                 hprintf("%4zu. ", parent->nchildren);
         2342                                 cur->indent = 6;
         2343                                 indent += cur->indent; /* align to number */
         2344                         } else if (parent->tag.displaytype & DisplayList) {
         2345                                 hprint(str_bullet_item);
         2346                                 cur->indent = 2;
         2347                                 indent += 2; /* align to bullet */
         2348                         }
         2349                 }
         2350                 skipinitialws = 0;
         2351         } else if (cur->tag.displaytype & DisplayInput) {
         2352                 if (!attr_type.len) {
         2353                         hprintf("[%-15s]", attr_value.len ? attr_value.data : ""); /* default: text */
         2354                 } else if (!strcasecmp(attr_type.data, "button")) {
         2355                         hprintf("[%s]", attr_value.len ? attr_value.data : "");
         2356                 } else if (!strcasecmp(attr_type.data, "submit")) {
         2357                         hprintf("[%s]", attr_value.len ? attr_value.data : "Submit Query");
         2358                 } else if (!strcasecmp(attr_type.data, "reset")) {
         2359                         hprintf("[%s]", attr_value.len ? attr_value.data : "Reset");
         2360                 } else if (!strcasecmp(attr_type.data, "checkbox")) {
         2361                         hprintf("[%s]",
         2362                                 attr_checked.len &&
         2363                                 !strcasecmp(attr_checked.data, "checked") ? str_checkbox_checked : " ");
         2364                 } else if (!strcasecmp(attr_type.data, "radio")) {
         2365                         hprintf("[%s]",
         2366                                 attr_checked.len &&
         2367                                 !strcasecmp(attr_checked.data, "checked") ? str_radio_checked : " ");
         2368                 } else if (!strcasecmp(attr_type.data, "hidden")) {
         2369                         cur->tag.displaytype |= DisplayNone;
         2370                 } else {
         2371                         /* unrecognized / default case is text */
         2372                         hprintf("[%-15s]", attr_value.len ? attr_value.data : "");
         2373                 }
         2374         }
         2375 
         2376         startmarkup(cur->tag.markuptype);
         2377 
         2378         /* do not count data such as an item bullet as part of the data for
         2379            the node */
         2380         cur->hasdata = 0;
         2381 
         2382         if (tagid == TagHr) { /* ruler */
         2383                 i = termwidth - indent - defaultindent;
         2384                 for (; i > 0; i--)
         2385                         hprint(str_ruler);
         2386                 cur->hasdata = 1; /* treat <hr/> as data */
         2387         } else if (tagid == TagBr) {
         2388                 hflush();
         2389                 hadnewline = 0; /* forced newline */
         2390                 hputchar('\n');
         2391                 cur->hasdata = 1; /* treat <br/> as data */
         2392         }
         2393 
         2394         /* autoclose tags, such as <br>, pretend we are <br/> */
         2395         if (!isshort && cur->tag.isvoid) {
         2396                 xmltagend(p, t, tl, 1); /* pretend close of short tag */
         2397                 return;
         2398         }
         2399 
         2400         /* Temporary replace the callback except the reader and end of tag
         2401            restore the context once we receive the same ignored tag in the
         2402            end tag handler.
         2403 
         2404            HTML script tags often contain characters such as "<".
         2405            These are interpreted as literal "<" text and not HTML.
         2406 
         2407            This is why old <style> used to be wrapped as comments: <!-- -->.
         2408            In XHTML CDATA sections would be used. */
         2409         switch (tagid) {
         2410         case TagScript:
         2411         case TagStyle:
         2412         case TagTitle:
         2413         case TagTemplate:
         2414         case TagTextarea:
         2415                 getnext_literal_start(t);
         2416                 break;
         2417         default:
         2418                 break;
         2419         }
         2420 }
         2421 
         2422 static void
         2423 xmlattr(XMLParser *p, const char *t, size_t tl, const char *n,
         2424         size_t nl, const char *v, size_t vl)
         2425 {
         2426         struct node *cur;
         2427         enum TagId tagid;
         2428 
         2429         cur = &nodes[curnode];
         2430         tagid = cur->tag.id;
         2431 
         2432         /* hide tags with attribute aria-hidden or hidden */
         2433         if (!attrcmp(n, "aria-hidden") || !attrcmp(n, "hidden"))
         2434                 cur->tag.displaytype |= DisplayNone;
         2435 
         2436         if (!attr_class_set && !attrcmp(n, "class")) /* use the first set attribute */
         2437                 string_append(&attr_class, v, vl);
         2438         else if (!attr_id_set && !attrcmp(n, "id")) /* use the first set attribute */
         2439                 string_append(&attr_id, v, vl);
         2440         else if (!attrcmp(n, "type"))
         2441                 string_append(&attr_type, v, vl);
         2442         else if (!attrcmp(n, "value"))
         2443                 string_append(&attr_value, v, vl);
         2444 
         2445         /* input checkbox state (to display if checked) */
         2446         if (cur->tag.displaytype & DisplayInput && !attrcmp(n, "checked"))
         2447                 string_append(&attr_checked, v, vl);
         2448 
         2449         switch (tagid) {
         2450         case TagA:
         2451                 if (!attrcmp(n, "href"))
         2452                         string_append(&attr_href, v, vl);
         2453                 break;
         2454         case TagBase: /* <base href="..." /> */
         2455                 if (!basehrefset && !attrcmp(n, "href"))
         2456                         strlcat(basehrefdoc, v, sizeof(basehrefdoc));
         2457                 break;
         2458         case TagImg: /* show img alt attribute as text. */
         2459                 if (!attrcmp(n, "alt"))
         2460                         string_append(&attr_alt, v, vl);
         2461                 break;
         2462         case TagObject:
         2463                 if (!attrcmp(n, "data"))
         2464                         string_append(&attr_data, v, vl);
         2465                 break;
         2466         case TagSelect:
         2467                 if (!attrcmp(n, "multiple"))
         2468                         cur->tag.displaytype |= DisplaySelectMulti;
         2469                 break;
         2470         default:
         2471                 break;
         2472         }
         2473 
         2474         /* src attribute */
         2475         switch (tagid) {
         2476         case TagAudio:
         2477         case TagEmbed:
         2478         case TagFrame:
         2479         case TagIframe:
         2480         case TagImg:
         2481         case TagSource:
         2482         case TagTrack:
         2483         case TagVideo:
         2484                 if (!attrcmp(n, "src"))
         2485                         string_append(&attr_src, v, vl);
         2486                 break;
         2487         default:
         2488                 break;
         2489         }
         2490 }
         2491 
         2492 static void
         2493 xmlattrentity(XMLParser *p, const char *t, size_t tl, const char *n,
         2494         size_t nl, const char *v, size_t vl)
         2495 {
         2496         char buf[8];
         2497         int len;
         2498 
         2499         len = xml_entitytostr(v, buf, sizeof(buf));
         2500         if (len > 0)
         2501                 xmlattr(p, t, tl, n, nl, buf, (size_t)len);
         2502         else
         2503                 xmlattr(p, t, tl, n, nl, v, vl);
         2504 }
         2505 
         2506 static void
         2507 xmlattrend(XMLParser *p, const char *t, size_t tl, const char *n,
         2508         size_t nl)
         2509 {
         2510         struct node *cur;
         2511         enum TagId tagid;
         2512 
         2513         cur = &nodes[curnode];
         2514         tagid = cur->tag.id;
         2515 
         2516         if (!attr_class_set && !attrcmp(n, "class"))
         2517                 attr_class_set = 1; /* use the first set attribute */
         2518         else if (!attr_id_set && !attrcmp(n, "id"))
         2519                 attr_id_set = 1; /* use the first set attribute */
         2520 
         2521         /* set base URL, if it is set it cannot be overwritten again */
         2522         if (!basehrefset && basehrefdoc[0] &&
         2523             tagid == TagBase && !attrcmp(n, "href"))
         2524                 basehrefset = uri_parse(basehrefdoc, &base) != -1 ? 1 : 0;
         2525 
         2526         /* if attribute checked is set but it has no value then set it to "checked" */
         2527         if (cur->tag.displaytype & DisplayInput && !attrcmp(n, "checked") && !attr_checked.len)
         2528                 string_append(&attr_checked, "checked", sizeof("checked") - 1);
         2529 }
         2530 
         2531 static void
         2532 xmlattrstart(XMLParser *p, const char *t, size_t tl, const char *n,
         2533         size_t nl)
         2534 {
         2535         struct node *cur;
         2536         enum TagId tagid;
         2537 
         2538         cur = &nodes[curnode];
         2539         tagid = cur->tag.id;
         2540 
         2541         if (!attrcmp(n, "alt"))
         2542                 string_clear(&attr_alt);
         2543         else if (!attrcmp(n, "checked"))
         2544                 string_clear(&attr_checked);
         2545         else if (!attr_class_set && !attrcmp(n, "class"))
         2546                 string_clear(&attr_class);
         2547         else if (!attrcmp(n, "data"))
         2548                 string_clear(&attr_data);
         2549         else if (!attrcmp(n, "href"))
         2550                 string_clear(&attr_href);
         2551         else if (!attr_id_set && !attrcmp(n, "id"))
         2552                 string_clear(&attr_id);
         2553         else if (!attrcmp(n, "src"))
         2554                 string_clear(&attr_src);
         2555         else if (!attrcmp(n, "type"))
         2556                 string_clear(&attr_type);
         2557         else if (!attrcmp(n, "value"))
         2558                 string_clear(&attr_value);
         2559 
         2560         if (basehrefdoc[0] && tagid == TagBase && !attrcmp(n, "href"))
         2561                 basehrefdoc[0] = '\0';
         2562 }
         2563 
         2564 static void
         2565 usage(void)
         2566 {
         2567         fprintf(stderr, "%s [-8adiIlorx] [-b basehref] [-s selector] [-u selector] [-w termwidth]\n", argv0);
         2568         exit(1);
         2569 }
         2570 
         2571 int
         2572 main(int argc, char **argv)
         2573 {
         2574         char *basehref;
         2575 
         2576         if (pledge("stdio", NULL) < 0)
         2577                 err(1, "pledge");
         2578 
         2579         ARGBEGIN {
         2580         case '8':
         2581                 str_bullet_item = "\xe2\x80\xa2 ";
         2582                 str_ruler = "\xe2\x94\x80"; /* symbol: "light horizontal" */
         2583                 break;
         2584         case 'a':
         2585                 allowansi = !allowansi;
         2586                 break;
         2587         case 'b':
         2588                 basehref = EARGF(usage());
         2589                 if (uri_parse(basehref, &base) == -1 ||
         2590                     !base.proto[0])
         2591                         usage();
         2592                 basehrefset = 1;
         2593                 break;
         2594         case 'd':
         2595                 uniqrefs = !uniqrefs;
         2596                 break;
         2597         case 'i':
         2598                 showrefinline = !showrefinline;
         2599                 break;
         2600         case 'I':
         2601                 showurlinline = !showurlinline;
         2602                 break;
         2603         case 'l':
         2604                 showrefbottom = !showrefbottom;
         2605                 break;
         2606         case 'o':
         2607                 allowosc8 = !allowosc8;
         2608                 break;
         2609         case 'r':
         2610                 allowlinewrap = !allowlinewrap;
         2611                 break;
         2612         case 's':
         2613                 sel_show = compileselectors(EARGF(usage()));
         2614                 if (!sel_show)
         2615                         usage(); /* invalid selector */
         2616                 /* switch to reader/selector mode, ignore all data except when matched */
         2617                 reader_mode = 1;
         2618                 reader_ignore = 1;
         2619                 break;
         2620         case 'u':
         2621                 sel_hide = compileselectors(EARGF(usage()));
         2622                 if (!sel_hide)
         2623                         usage(); /* invalid selector */
         2624                 /* switch to reader/selector mode */
         2625                 reader_mode = 1;
         2626                 break;
         2627         case 'w':
         2628                 if ((termwidth = strtol(EARGF(usage()), NULL, 10)) < 1)
         2629                         usage();
         2630                 break;
         2631         case 'x':
         2632                 xresources = !xresources;
         2633                 break;
         2634         default:
         2635                 usage();
         2636         } ARGEND
         2637 
         2638         linewrap = allowlinewrap;
         2639 
         2640         /* initial nodes */
         2641         ncapnodes = NODE_CAP_INC;
         2642         nodes = ecalloc(ncapnodes, sizeof(*nodes));
         2643         nodes_links = ecalloc(ncapnodes, sizeof(*nodes_links));
         2644 
         2645         parser.xmlattrstart = xmlattrstart;
         2646         parser.xmlattr = xmlattr;
         2647         parser.xmlattrentity = xmlattrentity;
         2648         parser.xmlattrend = xmlattrend;
         2649         parser.xmlcdatastart = xmlcdatastart;
         2650         parser.xmlcdata = xmlcdata;
         2651         parser.xmlcdataend = xmlcdataend;
         2652         parser.xmldatastart = xmldatastart;
         2653         parser.xmldata = xmldata;
         2654         parser.xmldataentity = xmldataentity;
         2655         parser.xmldataend = xmldataend;
         2656         parser.xmltagstart = xmltagstart;
         2657         parser.xmltagstartparsed = xmltagstartparsed;
         2658         parser.xmltagend = xmltagend;
         2659 
         2660         parser.getnext = getchar;
         2661         xml_parse(&parser);
         2662 
         2663         hflush();
         2664         if (ncells > 0)
         2665                 newline();
         2666 
         2667         if (showrefbottom || xresources)
         2668                 printlinkrefs();
         2669 
         2670         hflush();
         2671         setmarkup(0);
         2672 
         2673         return 0;
         2674 }