Skip to content

Commit 912817c

Browse files
committed
feature: add option for pretty encoding
1 parent 91ca29d commit 912817c

File tree

3 files changed

+207
-0
lines changed

3 files changed

+207
-0
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Table of Contents
1616
* [encode_number_precision](#encode_number_precision)
1717
* [encode_escape_forward_slash](#encode_escape_forward_slash)
1818
* [encode_skip_unsupported_value_types](#encode_skip_unsupported_value_types)
19+
* [encode_pretty](#encode_pretty)
1920
* [decode_array_with_array_mt](#decode_array_with_array_mt)
2021

2122
Description
@@ -201,6 +202,41 @@ This will generate:
201202

202203
[Back to TOC](#table-of-contents)
203204

205+
encode_pretty
206+
----------------------------
207+
**syntax:** `cjson.encode_pretty(enabled, indent, item_separator, key_separator)`
208+
209+
If enabled, JSON values encoded by `cjson.encode()` will be formatted in a human-readable way, using indentation and separators.
210+
The strings for indent and separators must contain only spaces, tabs, line feeds (LF), or carriage returns (CR).
211+
212+
- enabled: (boolean|string|nil, default: false)
213+
Enables or disables pretty-formatting. Accepts `true`/`false` or `"on"`/`"off"`.
214+
215+
- indent: (string|integer|nil, default: 2) Controls the indentation for nested levels.
216+
- If an integer, that many spaces are used per level.
217+
- If a string, the string itself is repeated for each level.
218+
219+
- item_separator: (string|nil, default: "\n") Separator inserted between array elements and object members.
220+
221+
- key_separator: (string|nil, default: " ") Separator inserted after the colon between an object key and its value.
222+
223+
Example:
224+
225+
```lua
226+
local cjson = require "cjson"
227+
228+
cjson.encode_pretty(true)
229+
print(cjson.encode({ a = 1, b = { c = 2 } }))
230+
-- {
231+
-- "a": 1,
232+
-- "b": {
233+
-- "c": 2
234+
-- }
235+
-- }
236+
```
237+
238+
[Back to TOC](#table-of-contents)
239+
204240
decode_array_with_array_mt
205241
--------------------------
206242
**syntax:** `cjson.decode_array_with_array_mt(enabled)`

lua_cjson.c

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@
9191
#define DEFAULT_DECODE_ARRAY_WITH_ARRAY_MT 0
9292
#define DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH 1
9393
#define DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES 0
94+
#define DEFAULT_ENCODE_PRETTY_ENABLED 0
95+
#define DEFAULT_ENCODE_PRETTY_INDENT " "
96+
#define DEFAULT_ENCODE_PRETTY_ITEM_SEPARATOR "\n"
97+
#define DEFAULT_ENCODE_PRETTY_KEY_SEPARATOR " "
9498

9599
#ifdef DISABLE_INVALID_NUMBERS
96100
#undef DEFAULT_DECODE_INVALID_NUMBERS
@@ -172,6 +176,10 @@ typedef struct {
172176
int encode_keep_buffer;
173177
int encode_empty_table_as_object;
174178
int encode_escape_forward_slash;
179+
int encode_pretty_enabled;
180+
const char *encode_pretty_indent;
181+
const char *encode_pretty_item_separator;
182+
const char *encode_pretty_key_separator;
175183

176184
int decode_invalid_numbers;
177185
int decode_max_depth;
@@ -310,6 +318,18 @@ static int json_enum_option(lua_State *l, int optindex, int *setting,
310318
return 1;
311319
}
312320

321+
/* Process string option for a configuration function */
322+
static int json_string_option(lua_State *l, int optindex, const char **setting)
323+
{
324+
if (!lua_isnil(l, optindex)) {
325+
const char *value = luaL_checkstring(l, optindex);
326+
*setting = value;
327+
}
328+
329+
lua_pushstring(l, *setting ? *setting : "");
330+
return 1;
331+
}
332+
313333
/* Configures handling of extremely sparse arrays:
314334
* convert: Convert extremely sparse arrays into objects? Otherwise error.
315335
* ratio: 0: always allow sparse; 1: never allow sparse; >1: use ratio
@@ -400,6 +420,73 @@ static int json_cfg_encode_keep_buffer(lua_State *l)
400420
return 1;
401421
}
402422

423+
/* Returns nonzero if the string consists only of allowed whitespace characters */
424+
static int is_allowed_ws(const char *s) {
425+
if (!s) return 0;
426+
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
427+
switch (*p) {
428+
case 0x20: /* space */
429+
case 0x09: /* tab */
430+
case 0x0A: /* newline */
431+
case 0x0D: /* carriage return */
432+
break;
433+
default:
434+
return 0;
435+
}
436+
}
437+
return 1;
438+
}
439+
440+
/* Configure pretty encoding */
441+
static int json_cfg_encode_pretty(lua_State *l)
442+
{
443+
json_config_t *cfg = json_arg_init(l, 4);
444+
445+
json_enum_option(l, 1, &cfg->encode_pretty_enabled, NULL, 1);
446+
447+
/* Process "indent" option, it can be either a string or a non-negative integer. */
448+
int optindex = 2;
449+
if (!lua_isnil(l, optindex)) {
450+
if (lua_isnumber(l, optindex)) {
451+
lua_Integer n = luaL_checkinteger(l, optindex);
452+
luaL_argcheck(l, n >= 0, optindex, "indent must be non-negative");
453+
454+
luaL_argcheck(l, (unsigned long long)n <= SIZE_MAX, optindex, "indent is too large");
455+
size_t indentLen = (size_t)n;
456+
457+
strbuf_t b;
458+
strbuf_init(&b, indentLen);
459+
for (size_t i = 0; i < indentLen; i++)
460+
strbuf_append_char_unsafe(&b, ' ');
461+
462+
cfg->encode_pretty_indent = strbuf_free_to_string(&b, &indentLen);
463+
} else if (lua_isstring(l, optindex)) {
464+
const char *indent = luaL_checkstring(l, optindex);
465+
if (!is_allowed_ws(indent))
466+
luaL_argerror(l, optindex, "indent may only contain space, tab, LF, or CR");
467+
cfg->encode_pretty_indent = indent;
468+
} else {
469+
luaL_argerror(l, optindex, "string or number expected");
470+
}
471+
}
472+
lua_pushstring(l, cfg->encode_pretty_indent ? cfg->encode_pretty_indent : "");
473+
474+
475+
const char *item_separator = cfg->encode_pretty_item_separator;
476+
json_string_option(l, 3, &item_separator);
477+
if (!is_allowed_ws(item_separator))
478+
luaL_argerror(l, optindex, "item_separator may only contain space, tab, LF, or CR");
479+
cfg->encode_pretty_item_separator = item_separator;
480+
481+
const char *key_separator = cfg->encode_pretty_key_separator;
482+
json_string_option(l, 4, &key_separator);
483+
if (!is_allowed_ws(key_separator))
484+
luaL_argerror(l, optindex, "key_separator may only contain space, tab, LF, or CR");
485+
cfg->encode_pretty_key_separator = key_separator;
486+
487+
return 4;
488+
}
489+
403490
#if defined(DISABLE_INVALID_NUMBERS) && !defined(USE_INTERNAL_FPCONV)
404491
void json_verify_invalid_number_setting(lua_State *l, int *setting)
405492
{
@@ -491,6 +578,10 @@ static void json_create_config(lua_State *l)
491578
cfg->decode_array_with_array_mt = DEFAULT_DECODE_ARRAY_WITH_ARRAY_MT;
492579
cfg->encode_escape_forward_slash = DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH;
493580
cfg->encode_skip_unsupported_value_types = DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES;
581+
cfg->encode_pretty_enabled = DEFAULT_ENCODE_PRETTY_ENABLED;
582+
cfg->encode_pretty_indent = DEFAULT_ENCODE_PRETTY_INDENT;
583+
cfg->encode_pretty_item_separator = DEFAULT_ENCODE_PRETTY_ITEM_SEPARATOR;
584+
cfg->encode_pretty_key_separator = DEFAULT_ENCODE_PRETTY_KEY_SEPARATOR;
494585

495586
#if DEFAULT_ENCODE_KEEP_BUFFER > 0
496587
strbuf_init(&cfg->encode_buf, 0);
@@ -660,6 +751,13 @@ static void json_check_encode_depth(lua_State *l, json_config_t *cfg,
660751
static int json_append_data(lua_State *l, json_config_t *cfg,
661752
int current_depth, strbuf_t *json);
662753

754+
static void json_append_item_separator_and_indent(strbuf_t *json, json_config_t *cfg, int depth)
755+
{
756+
strbuf_append_string(json, cfg->encode_pretty_item_separator);
757+
for (int i = 0; i < depth; i++)
758+
strbuf_append_string(json, cfg->encode_pretty_indent);
759+
}
760+
663761
/* json_append_array args:
664762
* - lua_State
665763
* - JSON strbuf
@@ -668,15 +766,21 @@ static void json_append_array(lua_State *l, json_config_t *cfg, int current_dept
668766
strbuf_t *json, int array_length, int raw)
669767
{
670768
int comma, i, json_pos, err;
769+
int has_items = 0;
671770

672771
strbuf_append_char(json, '[');
673772

674773
comma = 0;
675774
for (i = 1; i <= array_length; i++) {
775+
has_items = 1;
776+
676777
json_pos = strbuf_length(json);
677778
if (comma++ > 0)
678779
strbuf_append_char(json, ',');
679780

781+
if (cfg->encode_pretty_enabled)
782+
json_append_item_separator_and_indent(json, cfg, current_depth);
783+
680784
if (raw) {
681785
lua_rawgeti(l, -1, i);
682786
} else {
@@ -698,6 +802,9 @@ static void json_append_array(lua_State *l, json_config_t *cfg, int current_dept
698802
lua_pop(l, 1);
699803
}
700804

805+
if (has_items && cfg->encode_pretty_enabled)
806+
json_append_item_separator_and_indent(json, cfg, current_depth-1);
807+
701808
strbuf_append_char(json, ']');
702809
}
703810

@@ -752,6 +859,7 @@ static void json_append_object(lua_State *l, json_config_t *cfg,
752859
int current_depth, strbuf_t *json)
753860
{
754861
int comma, keytype, json_pos, err;
862+
int has_items = 0;
755863

756864
/* Object */
757865
strbuf_append_char(json, '{');
@@ -760,10 +868,15 @@ static void json_append_object(lua_State *l, json_config_t *cfg,
760868
/* table, startkey */
761869
comma = 0;
762870
while (lua_next(l, -2) != 0) {
871+
has_items = 1;
872+
763873
json_pos = strbuf_length(json);
764874
if (comma++ > 0)
765875
strbuf_append_char(json, ',');
766876

877+
if (cfg->encode_pretty_enabled)
878+
json_append_item_separator_and_indent(json, cfg, current_depth);
879+
767880
/* table, key, value */
768881
keytype = lua_type(l, -2);
769882
if (keytype == LUA_TNUMBER) {
@@ -778,6 +891,8 @@ static void json_append_object(lua_State *l, json_config_t *cfg,
778891
"table key must be a number or string");
779892
/* never returns */
780893
}
894+
if (cfg->encode_pretty_enabled)
895+
strbuf_append_string(json, cfg->encode_pretty_key_separator);
781896

782897
/* table, key, value */
783898
err = json_append_data(l, cfg, current_depth, json);
@@ -792,6 +907,9 @@ static void json_append_object(lua_State *l, json_config_t *cfg,
792907
/* table, key */
793908
}
794909

910+
if (has_items && cfg->encode_pretty_enabled)
911+
json_append_item_separator_and_indent(json, cfg, current_depth-1);
912+
795913
strbuf_append_char(json, '}');
796914
}
797915

@@ -1571,6 +1689,7 @@ static int lua_cjson_new(lua_State *l)
15711689
{ "decode_invalid_numbers", json_cfg_decode_invalid_numbers },
15721690
{ "encode_escape_forward_slash", json_cfg_encode_escape_forward_slash },
15731691
{ "encode_skip_unsupported_value_types", json_cfg_encode_skip_unsupported_value_types },
1692+
{ "encode_pretty", json_cfg_encode_pretty },
15741693
{ "new", lua_cjson_new },
15751694
{ NULL, NULL }
15761695
};

tests/test.lua

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,58 @@ local cjson_tests = {
333333
json.decode, { [["\uDB00\uD"]] },
334334
false, { "Expected value but found invalid unicode escape code at character 2" } },
335335

336+
-- Test pretty encoding
337+
{ "Set encode_pretty(true)",
338+
json.encode_pretty, { true }, true, { true, " ", "\n", " " } },
339+
{ "Encode simple object",
340+
json.encode, { { a = "a", b = "b" } },
341+
true, {
342+
util.one_of {
343+
'{\n "a": "a",\n "b": "b"\n}',
344+
'{\n "b": "b",\n "a": "a"\n}',
345+
}
346+
} },
347+
{ "Encode empty object",
348+
json.encode, { { } }, true, { '{}' } },
349+
{ "Encode nested object",
350+
json.encode, { { a = { b = 1 } } },
351+
true, { '{\n "a": {\n "b": 1\n }\n}' } },
352+
{ "Encode simple array",
353+
json.encode, { { 1, 2, 3 } },
354+
true, { '[\n 1,\n 2,\n 3\n]' } },
355+
{ "Encode empty array",
356+
json.encode, { json.empty_array }, true, { '[]' } },
357+
{ "Encode nested arrays",
358+
json.encode, { { { 1, 2 }, { 3, 4 } } },
359+
true, { '[\n [\n 1,\n 2\n ],\n [\n 3,\n 4\n ]\n]' } },
360+
{ "Encode array of objects",
361+
json.encode, { { { a = "a" }, { b = "b" } } },
362+
true, { '[\n {\n "a": "a"\n },\n {\n "b": "b"\n }\n]' } },
363+
-- Custom options
364+
{ 'Set encode_pretty(true, "\t", "\r\n", "")',
365+
json.encode_pretty, { true, "\t", "\r\n", "" }, true, { true, "\t", "\r\n", "" } },
366+
{ "Encode array of objects with custom options",
367+
json.encode, { { { a = "a" }, { b = "b" } } },
368+
true, { '[\r\n\t{\r\n\t\t"a":"a"\r\n\t},\r\n\t{\r\n\t\t"b":"b"\r\n\t}\r\n]' } },
369+
-- Integer indent
370+
{ 'Set encode_pretty(true, 4, "\r\n", "")',
371+
json.encode_pretty, { true, 4, "\r\n", "" }, true, { true, " ", "\r\n", "" } },
372+
{ "Encode array of objects with custom options",
373+
json.encode, { { { a = "a" }, { b = "b" } } },
374+
true, { '[\r\n {\r\n "a":"a"\r\n },\r\n {\r\n "b":"b"\r\n }\r\n]' } },
375+
-- Invalid options
376+
{ 'Set encode_pretty(true, 4, "c", "")',
377+
json.encode_pretty, { true, 4, "c", "" }, false,
378+
{
379+
util.one_of {
380+
"bad argument #2 to '?' (item_separator may only contain space, tab, LF, or CR)",
381+
"bad argument #2 to 'cjson.encode_pretty' (item_separator may only contain space, tab, LF, or CR)"
382+
}
383+
}
384+
},
385+
{ "Set encode_pretty(false)",
386+
json.encode_pretty, { false }, true, { false, " ", "\r\n", "" } },
387+
336388
-- Test locale support
337389
--
338390
-- The standard Lua interpreter is ANSI C online doesn't support locales

0 commit comments

Comments
 (0)