41 while ((opt = getopt(argc, argv,
"hvV:")) != EOF) {
44 if (!strncmp(
"LOG_NULL", optarg, 8))
46 else if (!strncmp(
"LOG_NOTICE", optarg, 10))
48 else if (!strncmp(
"LOG_INFO", optarg, 8))
50 else if (!strncmp(
"LOG_ERR", optarg, 7))
52 else if (!strncmp(
"LOG_DEBUG", optarg, 9))
55 fprintf(stderr,
"Unknown log level %s\n", optarg);
60 fprintf(stderr,
"ex_html\n");
64 fprintf(stderr,
"usage: %s [-V LOG_LEVEL]\n", argv[0]);
74 if (s && s->
data && strcmp(s->
data, want) == 0)
return 1;
99 " <link rel=\"stylesheet\" HREF=\"/css/main.css\">\n"
100 " <script src='/js/app.js'></script>\n"
101 " <!-- a comment with <a href=\"/in/comment\">trap</a> -->\n"
103 " <a href=\"/about\">about</a>\n"
104 " <a HREF=/contact>contact</a>\n"
105 " <a href = \"/help\" target=_blank>help</a>\n"
107 " <form action=\"/login\" method=POST><input name=u></form>\n"
108 " <img src=\"/img/logo.png\"/>\n"
109 " <iframe src=\"//cdn.example.com/frame\"></iframe>\n"
110 " <area shape=rect href=\"/map/area1\"/>\n"
111 " <script>var x = '<a href=\"/in/script\">trap</a>';</script>\n"
112 " <style>a[href=\"/in/style\"] { color: red; }</style>\n"
114 size_t len = strlen(html);
140 n_log(
LOG_ERR,
"n_html_links_free did not NULL the pointer");
147 "<form action=\"/signup\" method=\"post\">"
148 " <input type='text' name='user' value=\"alice\" required>"
149 " <input type=password name=\"pass\">"
150 " <textarea name=bio></textarea>"
151 " <select name=country><option>fr</option></select>"
152 " <button type=submit name=go value=1>Go</button>"
157 size_t len = strlen(html);
160 if (!forms || forms->
nb_items != 2) {
175 if (!f0->
action || strcmp(f0->
action,
"/signup") != 0) {
185 if (strcmp(user->
name,
"user") != 0 || strcmp(user->
type,
"text") != 0 || strcmp(user->
value,
"alice") != 0 || user->
required != 1) {
191 if (strcmp(pass->
name,
"pass") != 0 || strcmp(pass->
type,
"password") != 0 || pass->
required != 0) {
197 if (strcmp(bio->
name,
"bio") != 0 || strcmp(bio->
type,
"text") != 0) {
218 n_log(
LOG_ERR,
"n_html_forms_free did not NULL the pointer");
225 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
226 "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">"
227 " <url><loc>https://example.com/a</loc><lastmod>2026-01-01</lastmod></url>"
228 " <url>\n <LOC>\n https://example.com/b\n </LOC>\n</url>"
229 " <url><loc>https://example.com/c</loc></url>"
231 size_t len = strlen(xml);
242 const char* want[3] = {
"https://example.com/a",
"https://example.com/b",
"https://example.com/c"};
243 for (
int i = 0; i < 3; i++) {
245 if (!s || !s->
data || strcmp(s->
data, want[i]) != 0) {
246 n_log(
LOG_ERR,
"sitemap url %d: got '%s' expected '%s'", i, s && s->
data ? s->
data :
"(null)", want[i]);
272 "<form action=\"/up\" method=\"post\" enctype=\"multipart/form-data\"><input name=f></form>"
273 "<form action=\"/x\"><input name=g></form>";
275 if (!forms || forms->
nb_items != 2) {
284 if (!f0->
enctype || strcmp(f0->
enctype,
"multipart/form-data") != 0) {
288 if (!f1->
enctype || strcmp(f1->
enctype,
"application/x-www-form-urlencoded") != 0) {
298static void expect_text(
const char* label,
N_STR* txt,
const char* needle,
int want_present) {
299 int present = (txt && txt->
data && strstr(txt->
data, needle) != NULL);
300 if (present != want_present) {
301 n_log(
LOG_ERR,
"to_text %s: '%s' should be %s in '%s'", label, needle, want_present ?
"present" :
"absent", (txt && txt->
data) ? txt->
data :
"(null)");
309 const char* html =
"<html><body><h1>Title</h1><p>Hello <b>world</b></p></body></html>";
318 const char* html =
"<p>a & b <c> dABe "q" n m</p>";
320 expect_text(
"entities", t,
"a & b <c> dABe \"q\" n m", 1);
325 const char* html =
"<p>x</p><script>var a = 1 < 2;</script><style>.c{color:red}</style><p>y</p>";
335 const char* html =
"<p> a b\n\t c </p>";
337 if (!t || !t->
data || strcmp(t->
data,
"a b c") != 0) {
345 const char* html =
"<p>a<!-- secret comment -->b</p>";
373 "const API = '/api/v1';\n"
374 "fetch(\"/api/v1/users?id=1\").then(r => r.json());\n"
375 "axios.get('https://api.example.com/orders');\n"
376 "const u = `/api/v1/item/${id}/detail`;\n"
377 "xhr.open('GET', '//cdn.example.com/data.json');\n"
378 "const rel = './partials/menu.html';\n"
379 "const mime = 'text/html';\n"
380 "const re = '/^\\\\d+$/';\n"
381 "const cls = 'btn btn-primary';\n";
412 "<script src='/js/app.js'></script>\n"
413 "<script>const a = '/inline/one';</script>\n"
414 "<SCRIPT type='text/javascript'>fetch('/inline/two');</SCRIPT>\n"
415 "</head><body>text</body></html>";
443int main(
int argc,
char** argv) {
void process_args(int argc, char **argv)
static size_t list_count(LIST *l)
static void test_scripts(void)
static void test_js_urls(void)
static void test_empty_and_null(void)
static void test_enctype(void)
static void test_to_text(void)
static void expect_link_present(LIST *links, const char *want)
static void test_links(void)
static void expect_link_absent(LIST *links, const char *probe)
static void test_forms(void)
static void expect_text(const char *label, N_STR *txt, const char *needle, int want_present)
static void test_sitemap(void)
static int find_link(LIST *links, const char *want)
void * ptr
void pointer to store
LIST_NODE * start
pointer to the start of the list
size_t nb_items
number of item currently in the list
struct LIST_NODE * next
pointer to the next node
#define list_foreach(__ITEM_, __LIST_)
ForEach macro helper, safe for node removal during iteration.
Structure of a generic LIST container.
Structure of a generic list node.
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
#define LOG_DEBUG
debug-level messages
#define LOG_ERR
error conditions
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL )
#define LOG_NOTICE
normal but significant condition
#define LOG_NULL
no log output
#define LOG_INFO
informational
size_t written
number of meaningful bytes in data, excluding the null terminator; the size including the null termin...
#define free_nstr(__ptr)
free a N_STR structure and set the pointer to NULL
#define nstrprintf_cat(__nstr_var, __format,...)
Macro to quickly allocate and sprintf and cat to a N_STR.
N_STR * new_nstr(NSTRBYTE size)
create a new N_STR string
A box including a string and his lenght.
Common headers and low-level functions & define.
void n_html_links_free(LIST **links)
free a list returned by n_html_extract_links or n_sitemap_extract_urls
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,...
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
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...
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,...
void n_html_forms_free(LIST **forms)
free a list returned by n_html_extract_forms
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...
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
Lightweight HTML/XML extraction: links, forms, and sitemap URLs.
char * action
action attribute (may be "" for self)
LIST * fields
list of N_FORM_FIELD*
char * value
default value attribute, or ""
char * type
field type (lowercased), default "text"
char * name
field name attribute, or ""
int required
1 when the required attribute is present
char * enctype
lower-case enctype, default "application/x-www-form-urlencoded"
char * method
upper-case method, default "GET"
a single form field (input, select, textarea, button)
a parsed HTML form with its fields
List structures and definitions.
N_STR and string function declaration.