Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
n_html.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
27#include "nilorea/n_html.h"
28
29#include "nilorea/n_common.h"
30#include "nilorea/n_list.h"
31#include "nilorea/n_log.h"
32#include "nilorea/n_str.h"
33
34#include <ctype.h>
35#include <stdlib.h>
36#include <string.h>
37
39typedef struct N_HTML_ATTR_ {
40 char* name;
41 char* value;
43
45typedef struct N_HTML_TAG_ {
46 char* name;
47 int closing;
51
52/* Free an internal N_HTML_ATTR_ stored in a LIST. */
53static void _n_html_attr_free(void* p) {
54 if (!p) return;
56 FreeNoLog(a->name);
57 FreeNoLog(a->value);
58 Free(a);
59}
60
61/* Free an internal N_HTML_TAG_ stored in a LIST. */
62static void _n_html_tag_free(void* p) {
63 if (!p) return;
64 N_HTML_TAG_* t = (N_HTML_TAG_*)p;
65 FreeNoLog(t->name);
66 if (t->attrs) list_destroy(&t->attrs);
67 Free(t);
68}
69
70/* Free a public N_FORM_FIELD stored in a LIST. */
71static void _n_form_field_free(void* p) {
72 if (!p) return;
74 FreeNoLog(f->name);
75 FreeNoLog(f->type);
76 FreeNoLog(f->value);
77 Free(f);
78}
79
80/* Free a public N_HTML_FORM stored in a LIST. */
81static void _n_html_form_free(void* p) {
82 if (!p) return;
83 N_HTML_FORM* fm = (N_HTML_FORM*)p;
84 FreeNoLog(fm->method);
85 FreeNoLog(fm->action);
86 FreeNoLog(fm->enctype);
87 if (fm->fields) list_destroy(&fm->fields);
88 Free(fm);
89}
90
91/* Duplicate n bytes from s into a fresh, NUL-terminated, lowercased buffer. */
92static char* _n_html_strndup_lower(const char* s, size_t n) {
93 char* out = NULL;
94 Malloc(out, char, n + 1);
95 if (!out) return NULL;
96 for (size_t i = 0; i < n; i++) {
97 unsigned char c = (unsigned char)s[i];
98 out[i] = (char)((c >= 'A' && c <= 'Z') ? (c - 'A' + 'a') : c);
99 }
100 out[n] = '\0';
101 return out;
102}
103
104/* Duplicate n bytes from s into a fresh, NUL-terminated buffer (preserves case). */
105static char* _n_html_strndup(const char* s, size_t n) {
106 char* out = NULL;
107 Malloc(out, char, n + 1);
108 if (!out) return NULL;
109 if (n > 0) memcpy(out, s, n);
110 out[n] = '\0';
111 return out;
112}
113
114/* Duplicate s into a fresh upper-cased buffer (for the HTTP method). */
115static char* _n_html_strdup_upper(const char* s) {
116 if (!s) return _n_html_strndup("", 0);
117 size_t n = strlen(s);
118 char* out = NULL;
119 Malloc(out, char, n + 1);
120 if (!out) return NULL;
121 for (size_t i = 0; i < n; i++) {
122 unsigned char c = (unsigned char)s[i];
123 out[i] = (char)((c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c);
124 }
125 out[n] = '\0';
126 return out;
127}
128
129/* Case-insensitive ASCII string equality. */
130static int _n_html_ci_eq(const char* a, const char* b) {
131 while (*a && *b) {
132 unsigned char ca = (unsigned char)*a;
133 unsigned char cb = (unsigned char)*b;
134 if (ca >= 'A' && ca <= 'Z') ca = (unsigned char)(ca - 'A' + 'a');
135 if (cb >= 'A' && cb <= 'Z') cb = (unsigned char)(cb - 'A' + 'a');
136 if (ca != cb) return 0;
137 a++;
138 b++;
139 }
140 return *a == '\0' && *b == '\0';
141}
142
143/* Predicate for a tag/attribute name character (very liberal). */
144static int _n_html_is_name_char(int c) {
145 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == ':' || c == '.';
146}
147
148/* Find an attribute on a parsed tag. Returns the (possibly empty) value or NULL when absent. */
149static const char* _n_html_attr_lookup(const N_HTML_TAG_* tag, const char* name) {
150 if (!tag || !tag->attrs) return NULL;
151 list_foreach(node, tag->attrs) {
152 const N_HTML_ATTR_* a = (const N_HTML_ATTR_*)node->ptr;
153 if (a && a->name && _n_html_ci_eq(a->name, name)) {
154 return a->value ? a->value : "";
155 }
156 }
157 return NULL;
158}
159
160/* Parse one tag from *pos (which points just past '<') into out and advance *pos past '>'.
161 * Tolerant of malformed input: when no tag name is recognized, advance past the next '>'. */
162static void _n_html_parse_tag(const char* html, size_t len, size_t* pos, LIST* out) {
163 size_t i = *pos;
164 if (i >= len) return;
165
166 int closing = 0;
167 if (html[i] == '/') {
168 closing = 1;
169 i++;
170 }
171
172 size_t name_start = i;
173 while (i < len && _n_html_is_name_char((unsigned char)html[i])) i++;
174 if (i == name_start) {
175 while (i < len && html[i] != '>') i++;
176 if (i < len) i++;
177 *pos = i;
178 return;
179 }
180 char* name = _n_html_strndup_lower(html + name_start, i - name_start);
181 if (!name) {
182 while (i < len && html[i] != '>') i++;
183 if (i < len) i++;
184 *pos = i;
185 return;
186 }
187
188 N_HTML_TAG_* tag = NULL;
189 Malloc(tag, N_HTML_TAG_, 1);
190 if (!tag) {
191 Free(name);
192 while (i < len && html[i] != '>') i++;
193 if (i < len) i++;
194 *pos = i;
195 return;
196 }
197 tag->name = name;
198 tag->closing = closing;
199 tag->self_closing = 0;
201
202 while (i < len && html[i] != '>') {
203 while (i < len && isspace((unsigned char)html[i])) i++;
204 if (i >= len) break;
205 if (html[i] == '>') break;
206 if (html[i] == '/') {
207 tag->self_closing = 1;
208 i++;
209 continue;
210 }
211
212 size_t an_start = i;
213 while (i < len && html[i] != '=' && html[i] != '>' && html[i] != '/' && !isspace((unsigned char)html[i])) i++;
214 if (i == an_start) {
215 i++;
216 continue;
217 }
218 char* an = _n_html_strndup_lower(html + an_start, i - an_start);
219 if (!an) break;
220
221 size_t j = i;
222 while (j < len && isspace((unsigned char)html[j])) j++;
223
224 char* av = NULL;
225 if (j < len && html[j] == '=') {
226 j++;
227 while (j < len && isspace((unsigned char)html[j])) j++;
228 if (j < len && (html[j] == '"' || html[j] == '\'')) {
229 char q = html[j];
230 j++;
231 size_t v_start = j;
232 while (j < len && html[j] != q) j++;
233 av = _n_html_strndup(html + v_start, j - v_start);
234 if (j < len) j++;
235 } else {
236 size_t v_start = j;
237 while (j < len && html[j] != '>' && !isspace((unsigned char)html[j])) j++;
238 av = _n_html_strndup(html + v_start, j - v_start);
239 }
240 i = j;
241 } else {
242 av = _n_html_strndup("", 0);
243 }
244 if (!av) {
245 FreeNoLog(an);
246 break;
247 }
248 N_HTML_ATTR_* a = NULL;
249 Malloc(a, N_HTML_ATTR_, 1);
250 if (!a) {
251 FreeNoLog(an);
252 FreeNoLog(av);
253 break;
254 }
255 a->name = an;
256 a->value = av;
258 }
259 if (i < len && html[i] == '>') i++;
260
261 list_push(out, tag, _n_html_tag_free);
262 *pos = i;
263}
264
265/* Skip the raw text content of an open <script> or <style> up to its matching close tag.
266 * Returns the byte index of the '<' of the close tag, or len when none is found. */
267static size_t _n_html_skip_rawtext(const char* html, size_t len, size_t start, const char* close_tag) {
268 size_t clen = strlen(close_tag);
269 size_t j = start;
270 while (j + clen <= len) {
271 if (html[j] == '<') {
272 int eq = 1;
273 for (size_t k = 0; k < clen; k++) {
274 unsigned char a = (unsigned char)html[j + k];
275 unsigned char b = (unsigned char)close_tag[k];
276 if (a >= 'A' && a <= 'Z') a = (unsigned char)(a - 'A' + 'a');
277 if (a != b) {
278 eq = 0;
279 break;
280 }
281 }
282 if (eq) return j;
283 }
284 j++;
285 }
286 return len;
287}
288
289/* Scan all tags in [html, html+len). Returns a LIST of N_HTML_TAG_* (destructor wired). */
290static LIST* _n_html_scan_tags(const char* html, size_t len) {
292 if (!tags) return NULL;
293
294 size_t i = 0;
295 while (i < len) {
296 if (html[i] != '<') {
297 i++;
298 continue;
299 }
300 i++;
301 if (i >= len) break;
302
303 if (i + 2 < len && html[i] == '!' && html[i + 1] == '-' && html[i + 2] == '-') {
304 i += 3;
305 while (i + 2 < len && !(html[i] == '-' && html[i + 1] == '-' && html[i + 2] == '>')) i++;
306 if (i + 2 < len)
307 i += 3;
308 else
309 i = len;
310 continue;
311 }
312 if (html[i] == '!') {
313 if (i + 7 < len && memcmp(html + i, "![CDATA[", 8) == 0) {
314 i += 8;
315 while (i + 2 < len && !(html[i] == ']' && html[i + 1] == ']' && html[i + 2] == '>')) i++;
316 if (i + 2 < len)
317 i += 3;
318 else
319 i = len;
320 continue;
321 }
322 while (i < len && html[i] != '>') i++;
323 if (i < len) i++;
324 continue;
325 }
326 if (html[i] == '?') {
327 i++;
328 while (i + 1 < len && !(html[i] == '?' && html[i + 1] == '>')) i++;
329 if (i + 1 < len)
330 i += 2;
331 else
332 i = len;
333 continue;
334 }
335
336 _n_html_parse_tag(html, len, &i, tags);
337
338 const LIST_NODE* tail = tags->end;
339 if (tail && tail->ptr) {
340 const N_HTML_TAG_* t = (const N_HTML_TAG_*)tail->ptr;
341 if (!t->closing && !t->self_closing && t->name) {
342 if (strcmp(t->name, "script") == 0) {
343 i = _n_html_skip_rawtext(html, len, i, "</script");
344 } else if (strcmp(t->name, "style") == 0) {
345 i = _n_html_skip_rawtext(html, len, i, "</style");
346 }
347 }
348 }
349 }
350 return tags;
351}
352
353/* Push a URL extracted off a tag attribute onto out as an N_STR*. */
354static void _n_html_push_link(LIST* out, const char* url) {
355 if (!url || !*url) return;
356 N_STR* s = char_to_nstr(url);
357 if (s) list_push(out, s, free_nstr_ptr);
358}
359
360LIST* n_html_extract_links(const char* html, size_t len) {
362 if (!out) return NULL;
363 if (!html || len == 0) return out;
364
365 LIST* tags = _n_html_scan_tags(html, len);
366 if (!tags) return out;
367
368 list_foreach(node, tags) {
369 const N_HTML_TAG_* t = (const N_HTML_TAG_*)node->ptr;
370 if (!t || !t->name || t->closing) continue;
371 const char* attr = NULL;
372 if (strcmp(t->name, "a") == 0)
373 attr = _n_html_attr_lookup(t, "href");
374 else if (strcmp(t->name, "area") == 0)
375 attr = _n_html_attr_lookup(t, "href");
376 else if (strcmp(t->name, "form") == 0)
377 attr = _n_html_attr_lookup(t, "action");
378 else if (strcmp(t->name, "img") == 0)
379 attr = _n_html_attr_lookup(t, "src");
380 else if (strcmp(t->name, "script") == 0)
381 attr = _n_html_attr_lookup(t, "src");
382 else if (strcmp(t->name, "link") == 0)
383 attr = _n_html_attr_lookup(t, "href");
384 else if (strcmp(t->name, "iframe") == 0)
385 attr = _n_html_attr_lookup(t, "src");
386 if (attr) _n_html_push_link(out, attr);
387 }
388 list_destroy(&tags);
389 return out;
390}
391
392LIST* n_html_extract_forms(const char* html, size_t len) {
394 if (!out) return NULL;
395 if (!html || len == 0) return out;
396
397 LIST* tags = _n_html_scan_tags(html, len);
398 if (!tags) return out;
399
400 N_HTML_FORM* cur = NULL;
401 list_foreach(node, tags) {
402 const N_HTML_TAG_* t = (const N_HTML_TAG_*)node->ptr;
403 if (!t || !t->name) continue;
404
405 if (strcmp(t->name, "form") == 0) {
406 if (t->closing) {
407 if (cur) {
408 list_push(out, cur, _n_html_form_free);
409 cur = NULL;
410 }
411 continue;
412 }
413 if (cur) {
414 /* unclosed previous form: flush and start fresh */
415 list_push(out, cur, _n_html_form_free);
416 cur = NULL;
417 }
418 Malloc(cur, N_HTML_FORM, 1);
419 if (!cur) continue;
420 const char* m = _n_html_attr_lookup(t, "method");
421 const char* a = _n_html_attr_lookup(t, "action");
422 const char* e = _n_html_attr_lookup(t, "enctype");
423 cur->method = _n_html_strdup_upper(m ? m : "GET");
424 cur->action = _n_html_strndup(a ? a : "", a ? strlen(a) : 0);
425 cur->enctype = _n_html_strndup_lower(e ? e : "application/x-www-form-urlencoded", e ? strlen(e) : 33);
427 continue;
428 }
429
430 if (!cur) continue;
431 if (t->closing) continue;
432
433 int is_field = (strcmp(t->name, "input") == 0 || strcmp(t->name, "select") == 0 || strcmp(t->name, "textarea") == 0 || strcmp(t->name, "button") == 0);
434 if (!is_field) continue;
435
436 N_FORM_FIELD* f = NULL;
437 Malloc(f, N_FORM_FIELD, 1);
438 if (!f) continue;
439 const char* n_attr = _n_html_attr_lookup(t, "name");
440 const char* ty_attr = _n_html_attr_lookup(t, "type");
441 const char* v_attr = _n_html_attr_lookup(t, "value");
442 const char* req_attr = _n_html_attr_lookup(t, "required");
443 if (!ty_attr || !*ty_attr) ty_attr = "text";
444 f->name = _n_html_strndup(n_attr ? n_attr : "", n_attr ? strlen(n_attr) : 0);
445 f->type = _n_html_strndup_lower(ty_attr, strlen(ty_attr));
446 f->value = _n_html_strndup(v_attr ? v_attr : "", v_attr ? strlen(v_attr) : 0);
447 f->required = (req_attr != NULL) ? 1 : 0;
449 }
450
451 if (cur) list_push(out, cur, _n_html_form_free);
452 list_destroy(&tags);
453 return out;
454}
455
456LIST* n_sitemap_extract_urls(const char* xml, size_t len) {
458 if (!out) return NULL;
459 if (!xml || len == 0) return out;
460
461 size_t i = 0;
462 while (i < len) {
463 /* find next '<loc' open tag (case-insensitive) */
464 const char* content = NULL;
465 while (i + 5 <= len) {
466 if (xml[i] == '<') {
467 unsigned char c1 = (unsigned char)xml[i + 1];
468 unsigned char c2 = (unsigned char)xml[i + 2];
469 unsigned char c3 = (unsigned char)xml[i + 3];
470 unsigned char c4 = (unsigned char)xml[i + 4];
471 if ((c1 == 'l' || c1 == 'L') && (c2 == 'o' || c2 == 'O') && (c3 == 'c' || c3 == 'C') && (c4 == '>' || c4 == ' ' || c4 == '\t' || c4 == '\r' || c4 == '\n' || c4 == '/')) {
472 i += 4;
473 while (i < len && xml[i] != '>') i++;
474 if (i >= len) break;
475 i++;
476 content = xml + i;
477 break;
478 }
479 }
480 i++;
481 }
482 if (!content) break;
483
484 const char* close = NULL;
485 while (i + 6 <= len) {
486 if (xml[i] == '<' && xml[i + 1] == '/') {
487 unsigned char c1 = (unsigned char)xml[i + 2];
488 unsigned char c2 = (unsigned char)xml[i + 3];
489 unsigned char c3 = (unsigned char)xml[i + 4];
490 if ((c1 == 'l' || c1 == 'L') && (c2 == 'o' || c2 == 'O') && (c3 == 'c' || c3 == 'C') && (xml[i + 5] == '>' || xml[i + 5] == ' ' || xml[i + 5] == '\t' || xml[i + 5] == '\r' || xml[i + 5] == '\n')) {
491 close = xml + i;
492 break;
493 }
494 }
495 i++;
496 }
497 if (!close) break;
498
499 const char* s = content;
500 const char* e = close;
501 while (s < e && isspace((unsigned char)*s)) s++;
502 while (e > s && isspace((unsigned char)*(e - 1))) e--;
503 if (e > s) {
504 size_t n = (size_t)(e - s);
505 char* raw = _n_html_strndup(s, n);
506 if (raw) {
507 N_STR* nstr = char_to_nstr(raw);
508 Free(raw);
509 if (nstr) list_push(out, nstr, free_nstr_ptr);
510 }
511 }
512 /* advance past the close-tag '>' */
513 while (i < len && xml[i] != '>') i++;
514 if (i < len) i++;
515 }
516 return out;
517}
518
519/* Lowercase one ASCII letter. */
520static char _n_html_lc(char c) {
521 if (c >= 'A' && c <= 'Z') return (char)(c - 'A' + 'a');
522 return c;
523}
524
525/* Append a Unicode codepoint as UTF-8 (or ASCII) into buf, bounded by cap. */
526static void _n_html_put_cp(char* buf, size_t cap, size_t* o, unsigned long cp) {
527 if (cp == 0 || cp > 0x10FFFFUL) return;
528 if (cp < 0x80UL) {
529 if (*o + 1 <= cap) buf[(*o)++] = (char)cp;
530 } else if (cp < 0x800UL) {
531 if (*o + 2 <= cap) {
532 buf[(*o)++] = (char)(0xC0UL | (cp >> 6));
533 buf[(*o)++] = (char)(0x80UL | (cp & 0x3FUL));
534 }
535 } else if (cp < 0x10000UL) {
536 if (*o + 3 <= cap) {
537 buf[(*o)++] = (char)(0xE0UL | (cp >> 12));
538 buf[(*o)++] = (char)(0x80UL | ((cp >> 6) & 0x3FUL));
539 buf[(*o)++] = (char)(0x80UL | (cp & 0x3FUL));
540 }
541 } else {
542 if (*o + 4 <= cap) {
543 buf[(*o)++] = (char)(0xF0UL | (cp >> 18));
544 buf[(*o)++] = (char)(0x80UL | ((cp >> 12) & 0x3FUL));
545 buf[(*o)++] = (char)(0x80UL | ((cp >> 6) & 0x3FUL));
546 buf[(*o)++] = (char)(0x80UL | (cp & 0x3FUL));
547 }
548 }
549}
550
551/* Append a NUL-terminated replacement string into buf, bounded by cap. */
552static void _n_html_put_str(char* buf, size_t cap, size_t* o, const char* s) {
553 while (*s && *o < cap) buf[(*o)++] = *s++;
554}
555
556/* Named HTML entities mapped to an ASCII-friendly replacement. The five XML
557 entities decode exactly; the smart-punctuation set is folded to plain ASCII so
558 the rendered text stays readable in a fixed font. */
559static const struct {
560 const char* name;
561 const char* rep;
562} _n_html_entities[] = {
563 {"amp", "&"},
564 {"lt", "<"},
565 {"gt", ">"},
566 {"quot", "\""},
567 {"apos", "'"},
568 {"nbsp", " "},
569 {"copy", "(c)"},
570 {"reg", "(R)"},
571 {"trade", "(tm)"},
572 {"mdash", "-"},
573 {"ndash", "-"},
574 {"minus", "-"},
575 {"shy", ""},
576 {"hellip", "..."},
577 {"lsquo", "'"},
578 {"rsquo", "'"},
579 {"sbquo", "'"},
580 {"ldquo", "\""},
581 {"rdquo", "\""},
582 {"bdquo", "\""},
583 {"middot", "."},
584 {"bull", "*"},
585 {"deg", " deg "},
586 {"laquo", "<<"},
587 {"raquo", ">>"},
588 {"times", "x"},
589 {"divide", "/"},
590 {"frac12", "1/2"},
591 {"frac14", "1/4"},
592 {"frac34", "3/4"},
593 {"plusmn", "+/-"}};
594
595/* Classify a tag name for line breaking: 0 none (inline), 1 a cell separator
596 (a single space), 2 a line break, 3 a paragraph break (a blank line). */
597static int _n_html_block_kind(const char* name) {
598 static const char* para[] = {"p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "table", "ul", "ol", "blockquote", "pre", "section", "article", "header", "footer", "nav", "aside", "main", "form", "fieldset", "dl", "figure", "figcaption", "address", "title", "body", NULL};
599 static const char* line[] = {"br", "li", "tr", "dt", "dd", "option", "caption", "label", "legend", "thead", "tbody", "tfoot", NULL};
600 int i;
601 for (i = 0; para[i]; i++)
602 if (strcmp(name, para[i]) == 0) return 3;
603 for (i = 0; line[i]; i++)
604 if (strcmp(name, line[i]) == 0) return 2;
605 if (strcmp(name, "td") == 0 || strcmp(name, "th") == 0) return 1;
606 return 0;
607}
608
609/* Emit any pending newlines (capped at 2, i.e. one blank line) or a pending
610 space before the next visible character. */
611static void _n_html_flush(char* buf, size_t cap, size_t* o, int* pending_nl, int* pending_sp, int* line_content) {
612 if (*pending_nl > 0) {
613 if (*o > 0) { /* suppress leading newlines at the document start */
614 int n = (*pending_nl > 2) ? 2 : *pending_nl;
615 int t;
616 for (t = 0; t < n; t++)
617 if (*o < cap) buf[(*o)++] = '\n';
618 }
619 *pending_nl = 0;
620 *line_content = 0;
621 } else if (*pending_sp && *line_content) {
622 if (*o < cap) buf[(*o)++] = ' ';
623 }
624 *pending_sp = 0;
625}
626
627N_STR* n_html_to_text(const char* html, size_t len) {
628 char* buf = NULL;
629 size_t cap, o = 0, i = 0;
630 int pending_nl = 0, pending_sp = 0, line_content = 0;
631 N_STR* out = NULL;
632
633 if (!html) return NULL;
634 cap = len + 16;
635 Malloc(buf, char, cap + 1);
636 if (!buf) {
637 n_log(LOG_ERR, "n_html_to_text: out of memory for %zu bytes", cap + 1);
638 return NULL;
639 }
640
641 while (i < len) {
642 char c = html[i];
643 if (c == '<') {
644 /* HTML comment: skip to the closing --> */
645 if (i + 3 < len && html[i + 1] == '!' && html[i + 2] == '-' && html[i + 3] == '-') {
646 i += 4;
647 while (i + 2 < len && !(html[i] == '-' && html[i + 1] == '-' && html[i + 2] == '>'))
648 i++;
649 i = (i + 3 <= len) ? i + 3 : len;
650 continue;
651 }
652 /* doctype or other declaration: skip to '>' */
653 if (i + 1 < len && html[i + 1] == '!') {
654 while (i < len && html[i] != '>') i++;
655 if (i < len) i++;
656 continue;
657 }
658 /* parse the tag name */
659 {
660 size_t j = i + 1;
661 int closing = 0;
662 char name[32];
663 size_t nlen = 0;
664 if (j < len && html[j] == '/') {
665 closing = 1;
666 j++;
667 }
668 while (j < len && nlen < sizeof(name) - 1) {
669 char tc = html[j];
670 if ((tc >= 'a' && tc <= 'z') || (tc >= 'A' && tc <= 'Z') || (tc >= '0' && tc <= '9')) {
671 name[nlen++] = _n_html_lc(tc);
672 j++;
673 } else {
674 break;
675 }
676 }
677 name[nlen] = '\0';
678 /* script and style: drop the element content wholesale */
679 int is_script = !closing && strcmp(name, "script") == 0;
680 int is_style = !closing && strcmp(name, "style") == 0;
681 if (is_script || is_style) {
682 const char* endtag = is_script ? "</script" : "</style";
683 size_t tlen = strlen(endtag);
684 while (j < len && html[j] != '>') j++;
685 if (j < len) j++;
686 while (j < len) {
687 if (j + tlen <= len) {
688 size_t k = 0;
689 while (k < tlen && _n_html_lc(html[j + k]) == endtag[k]) k++;
690 if (k == tlen) {
691 j += tlen;
692 while (j < len && html[j] != '>') j++;
693 if (j < len) j++;
694 break;
695 }
696 }
697 j++;
698 }
699 if (pending_nl < 2) pending_nl = 2;
700 i = j;
701 continue;
702 }
703 /* line/paragraph structure */
704 {
705 int bk = _n_html_block_kind(name);
706 if (bk == 3) {
707 if (pending_nl < 2) pending_nl = 2;
708 pending_sp = 0;
709 } else if (bk == 2) {
710 if (pending_nl < 1) pending_nl = 1;
711 pending_sp = 0;
712 } else if (bk == 1) {
713 if (line_content) pending_sp = 1;
714 }
715 }
716 /* skip to the end of the tag */
717 while (j < len && html[j] != '>') j++;
718 if (j < len) j++;
719 i = j;
720 continue;
721 }
722 } else if (c == '&') {
723 size_t j = i + 1;
724 int handled = 0;
725 if (j < len && html[j] == '#') {
726 /* numeric character reference (decimal or hex) */
727 unsigned long cp = 0;
728 size_t start;
729 int hex = 0, digits = 0;
730 j++;
731 if (j < len && (html[j] == 'x' || html[j] == 'X')) {
732 hex = 1;
733 j++;
734 }
735 start = j;
736 while (j < len && html[j] != ';' && (j - start) < 8) {
737 char d = html[j];
738 if (hex) {
739 if (d >= '0' && d <= '9')
740 cp = cp * 16UL + (unsigned long)(d - '0');
741 else if (d >= 'a' && d <= 'f')
742 cp = cp * 16UL + (unsigned long)(d - 'a' + 10);
743 else if (d >= 'A' && d <= 'F')
744 cp = cp * 16UL + (unsigned long)(d - 'A' + 10);
745 else
746 break;
747 } else {
748 if (d >= '0' && d <= '9')
749 cp = cp * 10UL + (unsigned long)(d - '0');
750 else
751 break;
752 }
753 digits++;
754 j++;
755 }
756 if (digits > 0 && j < len && html[j] == ';') {
757 _n_html_flush(buf, cap, &o, &pending_nl, &pending_sp, &line_content);
758 _n_html_put_cp(buf, cap, &o, cp);
759 line_content = 1;
760 i = j + 1;
761 handled = 1;
762 }
763 } else {
764 /* named character reference */
765 char ename[16];
766 size_t en = 0;
767 size_t k = j;
768 while (k < len && en < sizeof(ename) - 1) {
769 char d = html[k];
770 if ((d >= 'a' && d <= 'z') || (d >= 'A' && d <= 'Z') || (d >= '0' && d <= '9')) {
771 ename[en++] = d;
772 k++;
773 } else {
774 break;
775 }
776 }
777 ename[en] = '\0';
778 if (en > 0 && k < len && html[k] == ';') {
779 size_t e;
780 for (e = 0; e < sizeof(_n_html_entities) / sizeof(_n_html_entities[0]); e++) {
781 if (strcmp(ename, _n_html_entities[e].name) == 0) {
782 _n_html_flush(buf, cap, &o, &pending_nl, &pending_sp, &line_content);
783 _n_html_put_str(buf, cap, &o, _n_html_entities[e].rep);
784 line_content = 1;
785 i = k + 1;
786 handled = 1;
787 break;
788 }
789 }
790 }
791 }
792 if (!handled) {
793 _n_html_flush(buf, cap, &o, &pending_nl, &pending_sp, &line_content);
794 if (o < cap) buf[o++] = '&';
795 line_content = 1;
796 i++;
797 }
798 continue;
799 } else if (c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\f' || c == '\v') {
800 if (line_content) pending_sp = 1; /* collapse a whitespace run */
801 i++;
802 continue;
803 } else {
804 _n_html_flush(buf, cap, &o, &pending_nl, &pending_sp, &line_content);
805 if (o < cap) buf[o++] = c;
806 line_content = 1;
807 i++;
808 continue;
809 }
810 }
811
812 buf[o] = '\0';
813 char_to_nstr_ex(buf, o, &out);
814 Free(buf);
815 return out;
816}
817
818/* Portable substring search over a bounded buffer (no GNU memmem dependency).
819 Returns a pointer to the first occurrence of needle in hay, or NULL. */
820static const char* _n_html_find(const char* hay, size_t haylen, const char* needle, size_t nlen) {
821 size_t i;
822 if (!hay || !needle || nlen == 0 || nlen > haylen)
823 return NULL;
824 for (i = 0; i + nlen <= haylen; i++)
825 if (memcmp(hay + i, needle, nlen) == 0)
826 return hay + i;
827 return NULL;
828}
829
830/* Portable case-insensitive substring search; needle must be given lowercase. */
831static const char* _n_html_find_ci(const char* hay, size_t haylen, const char* needle, size_t nlen) {
832 size_t i, j;
833 if (!hay || !needle || nlen == 0 || nlen > haylen)
834 return NULL;
835 for (i = 0; i + nlen <= haylen; i++) {
836 for (j = 0; j < nlen; j++)
837 if (tolower((unsigned char)hay[i + j]) != needle[j])
838 break;
839 if (j == nlen)
840 return hay + i;
841 }
842 return NULL;
843}
844
845/* Case-insensitive prefix test: does s (length n) begin with the lowercase pre? */
846static int _n_html_starts_ci(const char* s, size_t n, const char* pre, size_t plen) {
847 size_t i;
848 if (n < plen)
849 return 0;
850 for (i = 0; i < plen; i++)
851 if (tolower((unsigned char)s[i]) != pre[i])
852 return 0;
853 return 1;
854}
855
856/* True when a captured string literal looks like a URL or a site-relative path
857 worth treating as an endpoint: an absolute http(s) URL, a protocol-relative
858 "//host" URL, or a "/" / "./" / "../" path. Rejects tokens carrying characters
859 that a URL does not (whitespace, quotes, angle/curly brackets, backslash, and
860 regex metacharacters), which filters most non-URL string literals and inline
861 regular expressions out. */
862static int _n_html_js_url_like(const char* s, size_t n) {
863 size_t i;
864 int ok_prefix = 0;
865 if (!s || n < 2)
866 return 0;
867 if (_n_html_starts_ci(s, n, "http://", 7) ||
868 _n_html_starts_ci(s, n, "https://", 8) ||
869 (s[0] == '/' && s[1] == '/') ||
870 (s[0] == '/') ||
871 (s[0] == '.' && s[1] == '/') ||
872 (n >= 3 && s[0] == '.' && s[1] == '.' && s[2] == '/'))
873 ok_prefix = 1;
874 if (!ok_prefix)
875 return 0;
876 for (i = 0; i < n; i++) {
877 unsigned char c = (unsigned char)s[i];
878 if (c <= ' ' || c == '"' || c == '\'' || c == '`' || c == '<' || c == '>' ||
879 c == '{' || c == '}' || c == '|' || c == '^' || c == '\\' || c == 127)
880 return 0;
881 }
882 return 1;
883}
884
885/* Append a URL/path token (bounded by len) to out, deduped, when it looks like a
886 URL. A "${" template placeholder truncates the token at its static prefix. */
887static void _n_html_push_js_url(LIST* out, const char* tok, size_t len) {
888 size_t use = len;
889 size_t i;
890 char* buf;
891 const char* tmpl;
892 if (!tok || len == 0)
893 return;
894 /* keep only the static prefix before a template placeholder */
895 tmpl = _n_html_find(tok, len, "${", 2);
896 if (tmpl)
897 use = (size_t)(tmpl - tok);
898 if (!_n_html_js_url_like(tok, use))
899 return;
900 list_foreach(node, out) { /* dedup */
901 const N_STR* e = (const N_STR*)node->ptr;
902 if (e && e->data && strlen(e->data) == use && strncmp(e->data, tok, use) == 0)
903 return;
904 }
905 buf = malloc(use + 1);
906 if (!buf)
907 return;
908 for (i = 0; i < use; i++)
909 buf[i] = tok[i];
910 buf[use] = '\0';
911 _n_html_push_link(out, buf);
912 free(buf);
913}
914
915LIST* n_html_extract_js_urls(const char* js, size_t len) {
917 size_t i = 0;
918 if (!out) return NULL;
919 if (!js || len == 0) return out;
920 for (i = 0; i < len; i++) {
921 char q = js[i];
922 size_t start, j;
923 if (q != '"' && q != '\'' && q != '`')
924 continue;
925 start = i + 1;
926 for (j = start; j < len; j++) {
927 if (js[j] == '\\') { /* skip an escaped char */
928 j++;
929 continue;
930 }
931 if (js[j] == q)
932 break;
933 }
934 if (j <= len && j > start)
935 _n_html_push_js_url(out, js + start, j - start);
936 i = (j < len) ? j : len; /* resume after the closing quote */
937 }
938 return out;
939}
940
941LIST* n_html_extract_scripts(const char* html, size_t len) {
943 size_t i = 0;
944 if (!out) return NULL;
945 if (!html || len == 0) return out;
946 while (i < len) {
947 const char* open = _n_html_find_ci(html + i, len - i, "<script", 7);
948 size_t tag_start, body_start, k;
949 const char* close;
950 N_STR* body;
951 if (!open)
952 break;
953 tag_start = (size_t)(open - html);
954 /* find the end of the opening tag */
955 body_start = tag_start + 7;
956 while (body_start < len && html[body_start] != '>')
957 body_start++;
958 if (body_start >= len)
959 break;
960 /* skip external scripts (a src attribute); their body is fetched separately */
961 if (_n_html_find_ci(html + tag_start, body_start - tag_start, "src", 3) != NULL) {
962 i = body_start + 1;
963 continue;
964 }
965 body_start++; /* past '>' */
966 close = _n_html_find_ci(html + body_start, len - body_start, "</script", 8);
967 k = close ? (size_t)(close - html) : len;
968 if (k > body_start) {
969 body = NULL;
970 char_to_nstr_ex(html + body_start, k - body_start, &body);
971 if (body)
972 list_push(out, body, free_nstr_ptr);
973 }
974 i = close ? k + 8 : len;
975 }
976 return out;
977}
978
979void n_html_links_free(LIST** links) {
980 if (!links || !*links) return;
981 list_destroy(links);
982}
983
984void n_html_forms_free(LIST** forms) {
985 if (!forms || !*forms) return;
986 list_destroy(forms);
987}
#define FreeNoLog(__ptr)
Free Handler without log.
Definition n_common.h:272
#define Malloc(__ptr, __struct, __size)
Malloc Handler to get errors and set to 0.
Definition n_common.h:204
#define Free(__ptr)
Free Handler to get errors.
Definition n_common.h:263
LIST_NODE * end
pointer to the end of the list
Definition n_list.h:68
void * ptr
void pointer to store
Definition n_list.h:46
int list_push(LIST *list, void *ptr, void(*destructor)(void *ptr))
Add a pointer to the end of the list.
Definition n_list.c:228
#define list_foreach(__ITEM_, __LIST_)
ForEach macro helper, safe for node removal during iteration.
Definition n_list.h:89
int list_destroy(LIST **list)
Empty and Free a list container.
Definition n_list.c:548
LIST * new_generic_list(size_t max_items)
Initialiaze a generic list container to max_items pointers.
Definition n_list.c:37
#define MAX_LIST_ITEMS
flag to pass to new_generic_list for the maximum possible number of item in a list
Definition n_list.h:75
Structure of a generic LIST container.
Definition n_list.h:59
Structure of a generic list node.
Definition n_list.h:44
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:89
#define LOG_ERR
error conditions
Definition n_log.h:76
char * data
the string
Definition n_str.h:63
void free_nstr_ptr(void *ptr)
Free a N_STR pointer structure.
Definition n_str.c:70
N_STR * char_to_nstr(const char *src)
Convert a char into a N_STR, short version.
Definition n_str.c:255
int char_to_nstr_ex(const char *from, NSTRBYTE nboct, N_STR **to)
Convert a char into a N_STR, extended version.
Definition n_str.c:232
A box including a string and his lenght.
Definition n_str.h:61
Common headers and low-level functions & define.
static int _n_html_ci_eq(const char *a, const char *b)
Definition n_html.c:130
static int _n_html_starts_ci(const char *s, size_t n, const char *pre, size_t plen)
Definition n_html.c:846
static char * _n_html_strndup(const char *s, size_t n)
Definition n_html.c:105
static int _n_html_is_name_char(int c)
Definition n_html.c:144
static void _n_html_push_js_url(LIST *out, const char *tok, size_t len)
Definition n_html.c:887
void n_html_links_free(LIST **links)
free a list returned by n_html_extract_links or n_sitemap_extract_urls
Definition n_html.c:979
char * value
raw value, "" when the attribute has no '='
Definition n_html.c:41
static void _n_html_push_link(LIST *out, const char *url)
Definition n_html.c:354
LIST * n_html_extract_js_urls(const char *js, size_t len)
extract URL/path tokens from JavaScript source (quoted string literals that look like an http(s) URL,...
Definition n_html.c:915
static void _n_html_put_str(char *buf, size_t cap, size_t *o, const char *s)
Definition n_html.c:552
static void _n_html_put_cp(char *buf, size_t cap, size_t *o, unsigned long cp)
Definition n_html.c:526
static const char * _n_html_find_ci(const char *hay, size_t haylen, const char *needle, size_t nlen)
Definition n_html.c:831
static char _n_html_lc(char c)
Definition n_html.c:520
LIST * n_sitemap_extract_urls(const char *xml, size_t len)
extract <loc> URLs from a sitemap.xml as a LIST of N_STR*; free with n_html_links_free
Definition n_html.c:456
char * name
lowercased attribute name
Definition n_html.c:40
LIST * n_html_extract_scripts(const char *html, size_t len)
extract the inline <script> bodies (those without a src attribute) from an HTML document as a LIST of...
Definition n_html.c:941
static void _n_html_flush(char *buf, size_t cap, size_t *o, int *pending_nl, int *pending_sp, int *line_content)
Definition n_html.c:611
N_STR * n_html_to_text(const char *html, size_t len)
render HTML to readable plain text: drop tags, skip script/style, decode common entities,...
Definition n_html.c:627
static size_t _n_html_skip_rawtext(const char *html, size_t len, size_t start, const char *close_tag)
Definition n_html.c:267
static const struct @0 _n_html_entities[]
static char * _n_html_strndup_lower(const char *s, size_t n)
Definition n_html.c:92
static int _n_html_block_kind(const char *name)
Definition n_html.c:597
static void _n_html_form_free(void *p)
Definition n_html.c:81
void n_html_forms_free(LIST **forms)
free a list returned by n_html_extract_forms
Definition n_html.c:984
static const char * _n_html_attr_lookup(const N_HTML_TAG_ *tag, const char *name)
Definition n_html.c:149
static void _n_html_tag_free(void *p)
Definition n_html.c:62
static const char * _n_html_find(const char *hay, size_t haylen, const char *needle, size_t nlen)
Definition n_html.c:820
LIST * attrs
attributes (only populated for open tags)
Definition n_html.c:49
static LIST * _n_html_scan_tags(const char *html, size_t len)
Definition n_html.c:290
char * name
lowercased tag name
Definition n_html.c:46
static int _n_html_js_url_like(const char *s, size_t n)
Definition n_html.c:862
static void _n_html_parse_tag(const char *html, size_t len, size_t *pos, LIST *out)
Definition n_html.c:162
LIST * n_html_extract_links(const char *html, size_t len)
extract link URLs (a/href, form/action, img/src, script/src, link/href, iframe/src) as a LIST of N_ST...
Definition n_html.c:360
static char * _n_html_strdup_upper(const char *s)
Definition n_html.c:115
int self_closing
1 if the tag ended with '/>'
Definition n_html.c:48
LIST * n_html_extract_forms(const char *html, size_t len)
extract forms as a LIST of N_HTML_FORM*; free with n_html_forms_free
Definition n_html.c:392
static void _n_html_attr_free(void *p)
Definition n_html.c:53
static void _n_form_field_free(void *p)
Definition n_html.c:71
int closing
1 if the tag started with '/'
Definition n_html.c:47
attribute parsed off an HTML start tag (name lowercased, value raw)
Definition n_html.c:39
a single tag picked out by the scanner
Definition n_html.c:45
Lightweight HTML/XML extraction: links, forms, and sitemap URLs.
char * action
action attribute (may be "" for self)
Definition n_html.h:55
LIST * fields
list of N_FORM_FIELD*
Definition n_html.h:57
char * value
default value attribute, or ""
Definition n_html.h:48
char * type
field type (lowercased), default "text"
Definition n_html.h:47
char * name
field name attribute, or ""
Definition n_html.h:46
int required
1 when the required attribute is present
Definition n_html.h:49
char * enctype
lower-case enctype, default "application/x-www-form-urlencoded"
Definition n_html.h:56
char * method
upper-case method, default "GET"
Definition n_html.h:54
a single form field (input, select, textarea, button)
Definition n_html.h:45
a parsed HTML form with its fields
Definition n_html.h:53
List structures and definitions.
Generic log system.
N_STR and string function declaration.