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 "<".
488 buffers are not checked, they always fit (size of tag + space for at least "<<"). */
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, "<", 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, "<", 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 }