Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
n_pretty.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
28#include "nilorea/n_pretty.h"
29#include "nilorea/n_common.h"
30#include "nilorea/n_log.h"
31
32#include <ctype.h>
33#include <string.h>
34
35#ifdef HAVE_CJSON
36#include "cJSON.h"
37#endif
38
48
50#define PRETTY_OUT_PADDING 256
52#define PRETTY_INDENT_STEP 2
54#define PRETTY_TAG_NAME_MAX 31
55
57static int _out_mem(PRETTY_OUT* o, const char* s, size_t n) {
58 if (n == 0) return TRUE;
59 return write_and_fit_ex(&o->buf, &o->size, &o->written, s, (NSTRBYTE)n, PRETTY_OUT_PADDING);
60}
61
63static int _out_ch(PRETTY_OUT* o, char c) {
64 return _out_mem(o, &c, 1);
65}
66
68static int _out_indent(PRETTY_OUT* o, int depth) {
69 for (int it = 0; it < depth; it++) {
70 if (!_out_mem(o, " ", PRETTY_INDENT_STEP)) return FALSE;
71 }
72 return TRUE;
73}
74
77 N_STR* out = NULL;
78 if (!o->buf) return NULL;
79 out = char_to_nstr_nocopy(o->buf);
80 if (!out) {
81 Free(o->buf);
82 }
83 o->buf = NULL;
84 o->size = 0;
85 o->written = 0;
86 return out;
87}
88
89N_STR* n_pretty_json(const char* text) {
90 __n_assert(text, return NULL);
91#ifdef HAVE_CJSON
92 const char* p = text;
93 while (*p && isspace((unsigned char)*p)) p++;
94 if (*p != '{' && *p != '[') return NULL;
95 cJSON* json = cJSON_Parse(text);
96 if (!json) return NULL;
97 char* printed = cJSON_Print(json);
98 cJSON_Delete(json);
99 if (!printed) return NULL;
100 N_STR* out = char_to_nstr_nocopy(printed);
101 if (!out) free(printed);
102 return out;
103#else
104 n_log(LOG_ERR, "n_pretty_json: built without cJSON support (HAVE_CJSON)");
105 return NULL;
106#endif
107}
108
110static const char* PRETTY_VOID_ELEMENTS[] = {
111 "area", "base", "br", "col", "embed", "hr", "img", "input",
112 "link", "meta", "param", "source", "track", "wbr", NULL};
113
115static const char* PRETTY_RAWTEXT_ELEMENTS[] = {"script", "style", NULL};
116
118static int _name_in_list(const char* name, const char** list) {
119 for (size_t it = 0; list[it]; it++) {
120 if (strcmp(name, list[it]) == 0) return TRUE;
121 }
122 return FALSE;
123}
124
126static void _tag_name(const char* p, char* name, size_t name_size) {
127 size_t out = 0;
128 p++;
129 if (*p == '/') p++;
130 while (*p && out + 1 < name_size &&
131 (isalnum((unsigned char)*p) || *p == ':' || *p == '-' || *p == '_')) {
132 name[out++] = (char)tolower((unsigned char)*p);
133 p++;
134 }
135 name[out] = '\0';
136}
137
140static const char* _find_tag_end(const char* p) {
141 char quote = 0;
142 p++;
143 while (*p) {
144 if (quote) {
145 if (*p == quote) quote = 0;
146 } else if (*p == '"' || *p == '\'') {
147 quote = *p;
148 } else if (*p == '>') {
149 return p;
150 }
151 p++;
152 }
153 return NULL;
154}
155
157static const char* _stristr(const char* haystack, const char* needle) {
158 size_t nlen = strlen(needle);
159 if (nlen == 0) return haystack;
160 for (const char* p = haystack; *p; p++) {
161 if (strncasecmp(p, needle, nlen) == 0) return p;
162 }
163 return NULL;
164}
165
167static int _emit_line(PRETTY_OUT* o, int depth, const char* s, size_t n) {
168 if (!_out_indent(o, depth)) return FALSE;
169 if (!_out_mem(o, s, n)) return FALSE;
170 return _out_ch(o, '\n');
171}
172
175static int _emit_trimmed(PRETTY_OUT* o, int depth, const char* s, size_t n) {
176 size_t a = 0;
177 size_t b = n;
178 while (a < b && isspace((unsigned char)s[a])) a++;
179 while (b > a && isspace((unsigned char)s[b - 1])) b--;
180 if (b <= a) return TRUE;
181 return _emit_line(o, depth, s + a, b - a);
182}
183
184N_STR* n_pretty_xml(const char* text) {
185 __n_assert(text, return NULL);
186 const char* p = text;
187 while (*p && isspace((unsigned char)*p)) p++;
188 if (*p != '<') return NULL;
189
190 PRETTY_OUT o = {NULL, 0, 0};
191 int depth = 0;
192 int ok = TRUE;
193
194 while (*p && ok) {
195 if (*p != '<') {
196 /* text run up to the next tag */
197 const char* e = strchr(p, '<');
198 size_t n = e ? (size_t)(e - p) : strlen(p);
199 ok = _emit_trimmed(&o, depth, p, n);
200 p += n;
201 continue;
202 }
203 /* comment "<!--" (p[0] == '<' already checked above); explicit byte
204 * tests instead of strncmp so GCC can prove p + 4 stays in bounds */
205 if (p[1] == '!' && p[2] == '-' && p[3] == '-') {
206 const char* e = strstr(p + 4, "-->");
207 size_t n = e ? (size_t)(e + 3 - p) : strlen(p);
208 ok = _emit_line(&o, depth, p, n);
209 p += n;
210 continue;
211 }
212 if (strncasecmp(p, "<![CDATA[", 9) == 0) {
213 const char* e = strstr(p + 9, "]]>");
214 size_t n = e ? (size_t)(e + 3 - p) : strlen(p);
215 ok = _emit_line(&o, depth, p, n);
216 p += n;
217 continue;
218 }
219 if (p[1] == '?' || p[1] == '!') {
220 /* processing instruction or declaration (<?xml ...?>, <!DOCTYPE ...>) */
221 const char* e = _find_tag_end(p);
222 size_t n = e ? (size_t)(e + 1 - p) : strlen(p);
223 ok = _emit_line(&o, depth, p, n);
224 p += n;
225 continue;
226 }
227 /* regular tag: open, close, or self-closing */
228 {
229 const char* e = _find_tag_end(p);
230 size_t n = e ? (size_t)(e + 1 - p) : strlen(p);
231 int is_close = (p[1] == '/');
232 int self_close = (e && n >= 2 && p[n - 2] == '/');
233 char name[PRETTY_TAG_NAME_MAX + 1];
234 _tag_name(p, name, sizeof(name));
235 if (is_close && depth > 0) depth--;
236 ok = _emit_line(&o, depth, p, n);
237 p += n;
238 if (!ok || is_close || self_close || !e) continue;
239 if (_name_in_list(name, PRETTY_VOID_ELEMENTS)) continue;
240 depth++;
242 /* raw-text content: copy verbatim up to the matching close tag */
243 char closer[PRETTY_TAG_NAME_MAX + 4];
244 snprintf(closer, sizeof(closer), "</%s", name);
245 const char* end = _stristr(p, closer);
246 size_t rn = end ? (size_t)(end - p) : strlen(p);
247 ok = _emit_trimmed(&o, depth, p, rn);
248 p += rn;
249 }
250 }
251 }
252 if (!ok || !o.buf) {
253 Free(o.buf);
254 return NULL;
255 }
256 return _out_finish(&o);
257}
258
261static int _js_regex_possible(char c) {
262 if (c == 0) return TRUE;
263 return strchr("(,=:[!&|?;{}<>+-*%~^", c) != NULL;
264}
265
268static int _js_regex_keyword(const char* s, size_t n) {
269 static const char* kw[] = {"await", "case", "delete", "do", "else", "in",
270 "instanceof", "new", "of", "return", "throw",
271 "typeof", "void", "yield", NULL};
272 for (size_t it = 0; kw[it]; it++) {
273 if (strlen(kw[it]) == n && strncmp(s, kw[it], n) == 0) return TRUE;
274 }
275 return FALSE;
276}
277
293
295static int _js_lead(JS_STATE* js) {
296 if (js->at_line_start) {
297 if (!_out_indent(&js->o, js->indent)) return FALSE;
298 js->at_line_start = 0;
299 } else if (js->pending_space) {
300 if (!_out_ch(&js->o, ' ')) return FALSE;
301 }
302 js->pending_space = 0;
303 return TRUE;
304}
305
307static int _js_token(JS_STATE* js, const char* s, size_t n) {
308 if (!_js_lead(js)) return FALSE;
309 return _out_mem(&js->o, s, n);
310}
311
313static int _js_newline(JS_STATE* js) {
314 if (js->at_line_start) return TRUE;
315 js->at_line_start = 1;
316 js->pending_space = 0;
317 return _out_ch(&js->o, '\n');
318}
319
322static size_t _js_string_len(const char* s) {
323 char quote = s[0];
324 size_t it = 1;
325 while (s[it]) {
326 if (s[it] == '\\' && s[it + 1]) {
327 it += 2;
328 continue;
329 }
330 if (s[it] == quote) return it + 1;
331 if (s[it] == '\n') break;
332 it++;
333 }
334 return it;
335}
336
339static size_t _js_template_len(const char* s) {
340 size_t it = 1;
341 while (s[it]) {
342 if (s[it] == '\\' && s[it + 1]) {
343 it += 2;
344 continue;
345 }
346 if (s[it] == '`') return it + 1;
347 if (s[it] == '$' && s[it + 1] == '{') {
348 int braces = 1;
349 it += 2;
350 while (s[it] && braces > 0) {
351 if (s[it] == '{') braces++;
352 if (s[it] == '}') braces--;
353 it++;
354 }
355 continue;
356 }
357 it++;
358 }
359 return it;
360}
361
364static size_t _js_regex_len(const char* s) {
365 size_t it = 1;
366 int in_class = 0;
367 while (s[it]) {
368 if (s[it] == '\\' && s[it + 1]) {
369 it += 2;
370 continue;
371 }
372 if (s[it] == '\n') return it;
373 if (in_class) {
374 if (s[it] == ']') in_class = 0;
375 } else if (s[it] == '[') {
376 in_class = 1;
377 } else if (s[it] == '/') {
378 it++;
379 while (isalpha((unsigned char)s[it])) it++;
380 return it;
381 }
382 it++;
383 }
384 return it;
385}
386
387N_STR* n_pretty_js(const char* text) {
388 __n_assert(text, return NULL);
389 if (!text[0]) return NULL;
390
391 JS_STATE js = {{NULL, 0, 0}, 0, 1, 0, 0, 0};
392 long paren = 0;
393 const char* p = text;
394 int ok = TRUE;
395
396 while (*p && ok) {
397 char c = *p;
398 if (isspace((unsigned char)c)) {
399 if (!js.at_line_start) js.pending_space = 1;
400 p++;
401 continue;
402 }
403 if (c == '/' && p[1] == '/') {
404 const char* e = strchr(p, '\n');
405 size_t n = e ? (size_t)(e - p) : strlen(p);
406 ok = _js_token(&js, p, n) && _js_newline(&js);
407 p += n;
408 continue;
409 }
410 if (c == '/' && p[1] == '*') {
411 const char* e = strstr(p + 2, "*/");
412 size_t n = e ? (size_t)(e + 2 - p) : strlen(p);
413 ok = _js_token(&js, p, n);
414 js.pending_space = 1;
415 p += n;
416 continue;
417 }
418 if (c == '"' || c == '\'') {
419 size_t n = _js_string_len(p);
420 ok = _js_token(&js, p, n);
421 js.last_sig = c;
422 js.kw_regex = 0;
423 p += n;
424 continue;
425 }
426 if (c == '`') {
427 size_t n = _js_template_len(p);
428 ok = _js_token(&js, p, n);
429 /* the literal may span lines; whatever follows is mid-line */
430 js.last_sig = c;
431 js.kw_regex = 0;
432 p += n;
433 continue;
434 }
435 if (c == '/' && (js.kw_regex || _js_regex_possible(js.last_sig))) {
436 size_t n = _js_regex_len(p);
437 ok = _js_token(&js, p, n);
438 js.last_sig = '/';
439 js.kw_regex = 0;
440 p += n;
441 continue;
442 }
443 if (isalpha((unsigned char)c) || c == '_' || c == '$') {
444 /* whole identifier as one token so keywords are recognizable */
445 size_t n = 1;
446 while (p[n] && (isalnum((unsigned char)p[n]) || p[n] == '_' || p[n] == '$')) n++;
447 ok = _js_token(&js, p, n);
448 js.last_sig = p[n - 1];
449 js.kw_regex = _js_regex_keyword(p, n);
450 p += n;
451 continue;
452 }
453 if (c == '{' && paren == 0) {
454 ok = _js_token(&js, "{", 1) && _js_newline(&js);
455 js.indent++;
456 js.last_sig = c;
457 js.kw_regex = 0;
458 p++;
459 continue;
460 }
461 if (c == '}' && paren == 0) {
462 ok = _js_newline(&js);
463 if (js.indent > 0) js.indent--;
464 ok = ok && _js_token(&js, "}", 1);
465 js.last_sig = c;
466 js.kw_regex = 0;
467 p++;
468 /* keep a statement terminator on the same line as its brace */
469 while (*p && isspace((unsigned char)*p) && *p != '\n') p++;
470 if (*p == ';' || *p == ',') {
471 ok = ok && _out_ch(&js.o, *p);
472 js.last_sig = *p;
473 p++;
474 }
475 ok = ok && _js_newline(&js);
476 continue;
477 }
478 if (c == ';' && paren == 0) {
479 ok = _js_token(&js, ";", 1) && _js_newline(&js);
480 js.last_sig = c;
481 js.kw_regex = 0;
482 p++;
483 continue;
484 }
485 if (c == '(' || c == '[') paren++;
486 if ((c == ')' || c == ']') && paren > 0) paren--;
487 ok = _js_token(&js, &c, 1);
488 js.last_sig = c;
489 js.kw_regex = 0;
490 p++;
491 }
492 if (!ok || !js.o.buf) {
493 Free(js.o.buf);
494 return NULL;
495 }
496 return _out_finish(&js.o);
497}
#define __n_assert(__ptr, __ret)
macro to assert things
Definition n_common.h:279
#define Free(__ptr)
Free Handler to get errors.
Definition n_common.h:263
#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
N_STR * n_pretty_xml(const char *text)
Re-indent an XML or HTML document (one tag or text run per line).
Definition n_pretty.c:184
N_STR * n_pretty_js(const char *text)
Conservatively re-indent JavaScript (newlines at top-level braces and semicolons, two-space indent by...
Definition n_pretty.c:387
N_STR * n_pretty_json(const char *text)
Reformat a JSON document with cJSON (parse + print).
Definition n_pretty.c:89
size_t NSTRBYTE
N_STR base unit.
Definition n_str.h:58
N_STR * char_to_nstr_nocopy(char *src)
Convert a char into a N_STR, direct use of linked source pointer.
Definition n_str.c:268
int write_and_fit_ex(char **dest, NSTRBYTE *size, NSTRBYTE *written, const char *src, NSTRBYTE src_size, NSTRBYTE additional_padding)
Append src_size bytes from src to *dest, updating size/written, growing the buffer if needed.
Definition n_str.c:1187
A box including a string and his lenght.
Definition n_str.h:61
Common headers and low-level functions & define.
Generic log system.
static int _emit_trimmed(PRETTY_OUT *o, int depth, const char *s, size_t n)
emit s trimmed of leading/trailing whitespace as one indented line; empty-after-trim runs are skipped
Definition n_pretty.c:175
static size_t _js_string_len(const char *s)
length of the string literal starting at s (quote to closing quote, escapes honored,...
Definition n_pretty.c:322
static size_t _js_regex_len(const char *s)
length of the regex literal starting at s (slash to closing slash, escapes and character classes hono...
Definition n_pretty.c:364
static size_t _js_template_len(const char *s)
length of the template literal starting at s (backtick to backtick, escapes honored,...
Definition n_pretty.c:339
static int _js_token(JS_STATE *js, const char *s, size_t n)
emit a raw token of n bytes with leading indent/space handling
Definition n_pretty.c:307
#define PRETTY_INDENT_STEP
spaces per indentation level
Definition n_pretty.c:52
int indent
current indentation depth
Definition n_pretty.c:283
int kw_regex
1 when the last token was a keyword that can precede a regex literal
Definition n_pretty.c:291
static int _js_newline(JS_STATE *js)
terminate the current line unless already at a line start
Definition n_pretty.c:313
static const char * PRETTY_RAWTEXT_ELEMENTS[]
HTML elements whose content is raw text (no nested markup)
Definition n_pretty.c:115
int at_line_start
1 when nothing has been emitted on the current line yet
Definition n_pretty.c:285
static const char * _find_tag_end(const char *p)
find the '>' closing a tag starting at p, honoring quoted attribute values; returns NULL when the tag...
Definition n_pretty.c:140
int pending_space
1 when collapsed whitespace separates the previous and next token
Definition n_pretty.c:287
NSTRBYTE written
meaningful bytes, excluding the terminating '\0'
Definition n_pretty.c:46
static N_STR * _out_finish(PRETTY_OUT *o)
hand the buffer over to a new N_STR (no copy); the PRETTY_OUT is emptied
Definition n_pretty.c:76
static int _emit_line(PRETTY_OUT *o, int depth, const char *s, size_t n)
emit one indented line holding n raw bytes of s
Definition n_pretty.c:167
static int _out_mem(PRETTY_OUT *o, const char *s, size_t n)
append n bytes of s to the output, TRUE on success
Definition n_pretty.c:57
char last_sig
last significant code character emitted (0 = none yet)
Definition n_pretty.c:289
static int _js_regex_possible(char c)
whether a '/' may start a regex literal after last significant char c (start of input,...
Definition n_pretty.c:261
PRETTY_OUT o
output buffer
Definition n_pretty.c:281
static const char * PRETTY_VOID_ELEMENTS[]
HTML elements that never have content or a closing tag.
Definition n_pretty.c:110
static const char * _stristr(const char *haystack, const char *needle)
case-insensitive search for needle in haystack
Definition n_pretty.c:157
static int _js_regex_keyword(const char *s, size_t n)
whether the n bytes at s are a keyword after which an expression (and therefore a regex literal) can ...
Definition n_pretty.c:268
#define PRETTY_TAG_NAME_MAX
longest element name kept for void/raw-text lookups
Definition n_pretty.c:54
static int _js_lead(JS_STATE *js)
emit the indent or the collapsed space before a token, as appropriate
Definition n_pretty.c:295
static int _out_indent(PRETTY_OUT *o, int depth)
append depth * PRETTY_INDENT_STEP spaces, TRUE on success
Definition n_pretty.c:68
static int _out_ch(PRETTY_OUT *o, char c)
append a single character to the output, TRUE on success
Definition n_pretty.c:63
char * buf
buffer, reallocated on growth
Definition n_pretty.c:42
static int _name_in_list(const char *name, const char **list)
whether lowercase element name is in a NULL-terminated list
Definition n_pretty.c:118
NSTRBYTE size
total allocation in bytes
Definition n_pretty.c:44
#define PRETTY_OUT_PADDING
growth headroom so consecutive short appends share one realloc
Definition n_pretty.c:50
static void _tag_name(const char *p, char *name, size_t name_size)
extract the lowercased element name from a tag starting at p ("<name" or "</name")
Definition n_pretty.c:126
JS emitter state threaded through the helpers.
Definition n_pretty.c:279
growing output buffer fed by write_and_fit_ex
Definition n_pretty.c:40
Lexical pretty-printers for JSON, XML/HTML and JavaScript text.