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
27
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 int _jsonl_lead(PRETTY_OUT* o, int depth, int* at_line_start) {
111 if (!*at_line_start) return TRUE;
112 *at_line_start = 0;
113 return _out_indent(o, depth);
114}
115
117static int _jsonl_newline(PRETTY_OUT* o, int* at_line_start) {
118 if (*at_line_start) return TRUE;
119 *at_line_start = 1;
120 return _out_ch(o, '\n');
121}
122
123N_STR* n_pretty_json_lenient(const char* text) {
124 PRETTY_OUT o;
125 const char* p = text;
126 int depth = 0;
127 int at_line_start = 1;
128 int ok = TRUE;
129
130 __n_assert(text, return NULL);
131 if (!text[0]) return NULL;
132 memset(&o, 0, sizeof(o));
133
134 while (*p && ok) {
135 char c = *p;
136 if (isspace((unsigned char)c)) {
137 p++;
138 continue;
139 }
140 if (c == '"') {
141 /* copied verbatim, so a brace or a comma inside a string cannot
142 break the line; an unterminated one runs to the end of input */
143 size_t n = 1;
144 while (p[n]) {
145 if (p[n] == '\\' && p[n + 1]) {
146 n += 2;
147 continue;
148 }
149 if (p[n] == '"') {
150 n++;
151 break;
152 }
153 n++;
154 }
155 ok = _jsonl_lead(&o, depth, &at_line_start) && _out_mem(&o, p, n);
156 p += n;
157 continue;
158 }
159 if (c == '{' || c == '[') {
160 char close = (c == '{') ? '}' : ']';
161 const char* q = p + 1;
162 while (*q && isspace((unsigned char)*q)) q++;
163 ok = _jsonl_lead(&o, depth, &at_line_start) && _out_ch(&o, c);
164 if (*q == close) {
165 /* an empty container reads better kept on its line */
166 ok = ok && _out_ch(&o, close);
167 p = q + 1;
168 continue;
169 }
170 depth++;
171 ok = ok && _jsonl_newline(&o, &at_line_start);
172 p++;
173 continue;
174 }
175 if (c == '}' || c == ']') {
176 if (depth > 0) depth--;
177 ok = _jsonl_newline(&o, &at_line_start) &&
178 _jsonl_lead(&o, depth, &at_line_start) && _out_ch(&o, c);
179 p++;
180 continue;
181 }
182 if (c == ',') {
183 ok = _jsonl_lead(&o, depth, &at_line_start) && _out_ch(&o, c) &&
184 _jsonl_newline(&o, &at_line_start);
185 p++;
186 continue;
187 }
188 if (c == ':') {
189 ok = _jsonl_lead(&o, depth, &at_line_start) && _out_mem(&o, ": ", 2);
190 p++;
191 continue;
192 }
193 {
194 /* numbers, true/false/null, and whatever else a malformed or
195 truncated document left behind: passed through as one token */
196 size_t n = 0;
197 while (p[n] && !strchr("\"{}[],:", p[n]) && !isspace((unsigned char)p[n])) n++;
198 if (n == 0) n = 1;
199 ok = _jsonl_lead(&o, depth, &at_line_start) && _out_mem(&o, p, n);
200 p += n;
201 }
202 }
203 if (ok) ok = _jsonl_newline(&o, &at_line_start);
204 if (!ok) {
205 Free(o.buf);
206 return NULL;
207 }
208 return _out_finish(&o);
209}
210
212static const char* PRETTY_VOID_ELEMENTS[] = {
213 "area", "base", "br", "col", "embed", "hr", "img", "input",
214 "link", "meta", "param", "source", "track", "wbr", NULL};
215
217static const char* PRETTY_RAWTEXT_ELEMENTS[] = {"script", "style", NULL};
218
220static int _name_in_list(const char* name, const char** list) {
221 for (size_t it = 0; list[it]; it++) {
222 if (strcmp(name, list[it]) == 0) return TRUE;
223 }
224 return FALSE;
225}
226
228static void _tag_name(const char* p, char* name, size_t name_size) {
229 size_t out = 0;
230 p++;
231 if (*p == '/') p++;
232 while (*p && out + 1 < name_size &&
233 (isalnum((unsigned char)*p) || *p == ':' || *p == '-' || *p == '_')) {
234 name[out++] = (char)tolower((unsigned char)*p);
235 p++;
236 }
237 name[out] = '\0';
238}
239
242static const char* _find_tag_end(const char* p) {
243 char quote = 0;
244 p++;
245 while (*p) {
246 if (quote) {
247 if (*p == quote) quote = 0;
248 } else if (*p == '"' || *p == '\'') {
249 quote = *p;
250 } else if (*p == '>') {
251 return p;
252 }
253 p++;
254 }
255 return NULL;
256}
257
259static const char* _stristr(const char* haystack, const char* needle) {
260 size_t nlen = strlen(needle);
261 if (nlen == 0) return haystack;
262 for (const char* p = haystack; *p; p++) {
263 if (strncasecmp(p, needle, nlen) == 0) return p;
264 }
265 return NULL;
266}
267
269static int _emit_line(PRETTY_OUT* o, int depth, const char* s, size_t n) {
270 if (!_out_indent(o, depth)) return FALSE;
271 if (!_out_mem(o, s, n)) return FALSE;
272 return _out_ch(o, '\n');
273}
274
277static int _emit_trimmed(PRETTY_OUT* o, int depth, const char* s, size_t n) {
278 size_t a = 0;
279 size_t b = n;
280 while (a < b && isspace((unsigned char)s[a])) a++;
281 while (b > a && isspace((unsigned char)s[b - 1])) b--;
282 if (b <= a) return TRUE;
283 return _emit_line(o, depth, s + a, b - a);
284}
285
286N_STR* n_pretty_xml(const char* text) {
287 __n_assert(text, return NULL);
288 const char* p = text;
289 while (*p && isspace((unsigned char)*p)) p++;
290 if (*p != '<') return NULL;
291
292 PRETTY_OUT o = {NULL, 0, 0};
293 int depth = 0;
294 int ok = TRUE;
295
296 while (*p && ok) {
297 if (*p != '<') {
298 /* text run up to the next tag */
299 const char* e = strchr(p, '<');
300 size_t n = e ? (size_t)(e - p) : strlen(p);
301 ok = _emit_trimmed(&o, depth, p, n);
302 p += n;
303 continue;
304 }
305 /* comment "<!--" (p[0] == '<' already checked above); explicit byte
306 * tests instead of strncmp so GCC can prove p + 4 stays in bounds */
307 if (p[1] == '!' && p[2] == '-' && p[3] == '-') {
308 const char* e = strstr(p + 4, "-->");
309 size_t n = e ? (size_t)(e + 3 - p) : strlen(p);
310 ok = _emit_line(&o, depth, p, n);
311 p += n;
312 continue;
313 }
314 if (strncasecmp(p, "<![CDATA[", 9) == 0) {
315 const char* e = strstr(p + 9, "]]>");
316 size_t n = e ? (size_t)(e + 3 - p) : strlen(p);
317 ok = _emit_line(&o, depth, p, n);
318 p += n;
319 continue;
320 }
321 if (p[1] == '?' || p[1] == '!') {
322 /* processing instruction or declaration (<?xml ...?>, <!DOCTYPE ...>) */
323 const char* e = _find_tag_end(p);
324 size_t n = e ? (size_t)(e + 1 - p) : strlen(p);
325 ok = _emit_line(&o, depth, p, n);
326 p += n;
327 continue;
328 }
329 /* regular tag: open, close, or self-closing */
330 {
331 const char* e = _find_tag_end(p);
332 size_t n = e ? (size_t)(e + 1 - p) : strlen(p);
333 int is_close = (p[1] == '/');
334 int self_close = (e && n >= 2 && p[n - 2] == '/');
335 char name[PRETTY_TAG_NAME_MAX + 1];
336 _tag_name(p, name, sizeof(name));
337 if (is_close && depth > 0) depth--;
338 ok = _emit_line(&o, depth, p, n);
339 p += n;
340 if (!ok || is_close || self_close || !e) continue;
341 if (_name_in_list(name, PRETTY_VOID_ELEMENTS)) continue;
342 depth++;
344 /* raw-text content: copy verbatim up to the matching close tag */
345 char closer[PRETTY_TAG_NAME_MAX + 4];
346 snprintf(closer, sizeof(closer), "</%s", name);
347 const char* end = _stristr(p, closer);
348 size_t rn = end ? (size_t)(end - p) : strlen(p);
349 ok = _emit_trimmed(&o, depth, p, rn);
350 p += rn;
351 }
352 }
353 }
354 if (!ok || !o.buf) {
355 Free(o.buf);
356 return NULL;
357 }
358 return _out_finish(&o);
359}
360
363static int _js_regex_possible(char c) {
364 if (c == 0) return TRUE;
365 return strchr("(,=:[!&|?;{}<>+-*%~^", c) != NULL;
366}
367
370static int _js_regex_keyword(const char* s, size_t n) {
371 static const char* kw[] = {"await", "case", "delete", "do", "else", "in",
372 "instanceof", "new", "of", "return", "throw",
373 "typeof", "void", "yield", NULL};
374 for (size_t it = 0; kw[it]; it++) {
375 if (strlen(kw[it]) == n && strncmp(s, kw[it], n) == 0) return TRUE;
376 }
377 return FALSE;
378}
379
395
397static int _js_lead(JS_STATE* js) {
398 if (js->at_line_start) {
399 if (!_out_indent(&js->o, js->indent)) return FALSE;
400 js->at_line_start = 0;
401 } else if (js->pending_space) {
402 if (!_out_ch(&js->o, ' ')) return FALSE;
403 }
404 js->pending_space = 0;
405 return TRUE;
406}
407
409static int _js_token(JS_STATE* js, const char* s, size_t n) {
410 if (!_js_lead(js)) return FALSE;
411 return _out_mem(&js->o, s, n);
412}
413
415static int _js_newline(JS_STATE* js) {
416 if (js->at_line_start) return TRUE;
417 js->at_line_start = 1;
418 js->pending_space = 0;
419 return _out_ch(&js->o, '\n');
420}
421
424static size_t _js_string_len(const char* s) {
425 char quote = s[0];
426 size_t it = 1;
427 while (s[it]) {
428 if (s[it] == '\\' && s[it + 1]) {
429 it += 2;
430 continue;
431 }
432 if (s[it] == quote) return it + 1;
433 if (s[it] == '\n') break;
434 it++;
435 }
436 return it;
437}
438
441static size_t _js_template_len(const char* s) {
442 size_t it = 1;
443 while (s[it]) {
444 if (s[it] == '\\' && s[it + 1]) {
445 it += 2;
446 continue;
447 }
448 if (s[it] == '`') return it + 1;
449 if (s[it] == '$' && s[it + 1] == '{') {
450 int braces = 1;
451 it += 2;
452 while (s[it] && braces > 0) {
453 if (s[it] == '{') braces++;
454 if (s[it] == '}') braces--;
455 it++;
456 }
457 continue;
458 }
459 it++;
460 }
461 return it;
462}
463
466static size_t _js_regex_len(const char* s) {
467 size_t it = 1;
468 int in_class = 0;
469 while (s[it]) {
470 if (s[it] == '\\' && s[it + 1]) {
471 it += 2;
472 continue;
473 }
474 if (s[it] == '\n') return it;
475 if (in_class) {
476 if (s[it] == ']') in_class = 0;
477 } else if (s[it] == '[') {
478 in_class = 1;
479 } else if (s[it] == '/') {
480 it++;
481 while (isalpha((unsigned char)s[it])) it++;
482 return it;
483 }
484 it++;
485 }
486 return it;
487}
488
489N_STR* n_pretty_js(const char* text) {
490 __n_assert(text, return NULL);
491 if (!text[0]) return NULL;
492
493 JS_STATE js = {{NULL, 0, 0}, 0, 1, 0, 0, 0};
494 long paren = 0;
495 const char* p = text;
496 int ok = TRUE;
497
498 while (*p && ok) {
499 char c = *p;
500 if (isspace((unsigned char)c)) {
501 if (!js.at_line_start) js.pending_space = 1;
502 p++;
503 continue;
504 }
505 if (c == '/' && p[1] == '/') {
506 const char* e = strchr(p, '\n');
507 size_t n = e ? (size_t)(e - p) : strlen(p);
508 ok = _js_token(&js, p, n) && _js_newline(&js);
509 p += n;
510 continue;
511 }
512 if (c == '/' && p[1] == '*') {
513 const char* e = strstr(p + 2, "*/");
514 size_t n = e ? (size_t)(e + 2 - p) : strlen(p);
515 ok = _js_token(&js, p, n);
516 js.pending_space = 1;
517 p += n;
518 continue;
519 }
520 if (c == '"' || c == '\'') {
521 size_t n = _js_string_len(p);
522 ok = _js_token(&js, p, n);
523 js.last_sig = c;
524 js.kw_regex = 0;
525 p += n;
526 continue;
527 }
528 if (c == '`') {
529 size_t n = _js_template_len(p);
530 ok = _js_token(&js, p, n);
531 /* the literal may span lines; whatever follows is mid-line */
532 js.last_sig = c;
533 js.kw_regex = 0;
534 p += n;
535 continue;
536 }
537 if (c == '/' && (js.kw_regex || _js_regex_possible(js.last_sig))) {
538 size_t n = _js_regex_len(p);
539 ok = _js_token(&js, p, n);
540 js.last_sig = '/';
541 js.kw_regex = 0;
542 p += n;
543 continue;
544 }
545 if (isalpha((unsigned char)c) || c == '_' || c == '$') {
546 /* whole identifier as one token so keywords are recognizable */
547 size_t n = 1;
548 while (p[n] && (isalnum((unsigned char)p[n]) || p[n] == '_' || p[n] == '$')) n++;
549 ok = _js_token(&js, p, n);
550 js.last_sig = p[n - 1];
551 js.kw_regex = _js_regex_keyword(p, n);
552 p += n;
553 continue;
554 }
555 if (c == '{' && paren == 0) {
556 ok = _js_token(&js, "{", 1) && _js_newline(&js);
557 js.indent++;
558 js.last_sig = c;
559 js.kw_regex = 0;
560 p++;
561 continue;
562 }
563 if (c == '}' && paren == 0) {
564 ok = _js_newline(&js);
565 if (js.indent > 0) js.indent--;
566 ok = ok && _js_token(&js, "}", 1);
567 js.last_sig = c;
568 js.kw_regex = 0;
569 p++;
570 /* keep a statement terminator on the same line as its brace */
571 while (*p && isspace((unsigned char)*p) && *p != '\n') p++;
572 if (*p == ';' || *p == ',') {
573 ok = ok && _out_ch(&js.o, *p);
574 js.last_sig = *p;
575 p++;
576 }
577 ok = ok && _js_newline(&js);
578 continue;
579 }
580 if (c == ';' && paren == 0) {
581 ok = _js_token(&js, ";", 1) && _js_newline(&js);
582 js.last_sig = c;
583 js.kw_regex = 0;
584 p++;
585 continue;
586 }
587 if (c == '(' || c == '[') paren++;
588 if ((c == ')' || c == ']') && paren > 0) paren--;
589 ok = _js_token(&js, &c, 1);
590 js.last_sig = c;
591 js.kw_regex = 0;
592 p++;
593 }
594 if (!ok || !js.o.buf) {
595 Free(js.o.buf);
596 return NULL;
597 }
598 return _out_finish(&js.o);
599}
#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:113
#define LOG_ERR
error conditions
Definition n_log.h:100
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:286
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:489
N_STR * n_pretty_json(const char *text)
Reformat a JSON document with cJSON (parse + print).
Definition n_pretty.c:89
N_STR * n_pretty_json_lenient(const char *text)
Re-indent a JSON document by punctuation alone, without parsing it.
Definition n_pretty.c:123
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:275
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:1194
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:277
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:424
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:466
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:441
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:409
#define PRETTY_INDENT_STEP
spaces per indentation level
Definition n_pretty.c:52
int indent
current indentation depth
Definition n_pretty.c:385
int kw_regex
1 when the last token was a keyword that can precede a regex literal
Definition n_pretty.c:393
static int _js_newline(JS_STATE *js)
terminate the current line unless already at a line start
Definition n_pretty.c:415
static const char * PRETTY_RAWTEXT_ELEMENTS[]
HTML elements whose content is raw text (no nested markup).
Definition n_pretty.c:217
int at_line_start
1 when nothing has been emitted on the current line yet
Definition n_pretty.c:387
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:242
int pending_space
1 when collapsed whitespace separates the previous and next token
Definition n_pretty.c:389
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 _jsonl_newline(PRETTY_OUT *o, int *at_line_start)
end the current lenient JSON line unless it is still empty, TRUE on success
Definition n_pretty.c:117
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:269
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:391
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:363
PRETTY_OUT o
output buffer
Definition n_pretty.c:383
static const char * PRETTY_VOID_ELEMENTS[]
HTML elements that never have content or a closing tag.
Definition n_pretty.c:212
static const char * _stristr(const char *haystack, const char *needle)
case-insensitive search for needle in haystack
Definition n_pretty.c:259
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:370
#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:397
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
static int _jsonl_lead(PRETTY_OUT *o, int depth, int *at_line_start)
emit the pending indent of a lenient JSON line, TRUE on success
Definition n_pretty.c:110
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:220
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:228
JS emitter state threaded through the helpers.
Definition n_pretty.c:381
growing output buffer fed by write_and_fit_ex
Definition n_pretty.c:40
Lexical pretty-printers for JSON, XML/HTML and JavaScript text.