Skip to content

Commit 84a4799

Browse files
committed
feat(json): add pretty option for vim.json.encode()
Problem: There is no straightforward way to pretty-print objects as JSON. The existing `vim.inspect` outputs LON. Solution: Introduce `pretty` option for `vim.json.encode()`, which enables pretty-formatting when table provided or `true`. Supports the following keys: - indent: (integer|string) (default 2) Controls how nested levels are indented. - If a integer: indents that many spaces per nesting level. - If a string: that string is used for the indentation. - item_separator: (string) (default "\n") Separator inserted between array elements and object members. - key_separator: (string) (default " ") Separator inserted after the colon between an object key and its value. Adapts PR to upstream: openresty/lua-cjson#114.
1 parent 516363e commit 84a4799

File tree

5 files changed

+285
-15
lines changed

5 files changed

+285
-15
lines changed

runtime/doc/lua.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3375,6 +3375,24 @@ vim.json.encode({obj}, {opts}) *vim.json.encode()*
33753375
{opts} (`table<string,any>?`) Options table with keys:
33763376
• escape_slash: (boolean) (default false) Escape slash
33773377
characters "/" in string values.
3378+
• pretty: (table|boolean) (default false) Enables
3379+
pretty-formatting when provided or `true`. Supports the
3380+
following keys:
3381+
• indent: (integer|string) (default 2) Controls how nested
3382+
levels are indented.
3383+
• If a integer: indents that many spaces per nesting
3384+
level.
3385+
• If a string: that string is used for the indentation.
3386+
Must contain only spaces, tabs, line feeds (LF), or
3387+
carriage returns (CR).
3388+
• item_separator: (string) (default "\n") Separator inserted
3389+
between array elements and object members. Must contain
3390+
only spaces, tabs, line feeds (LF), or carriage returns
3391+
(CR).
3392+
• key_separator: (string) (default " ") Separator inserted
3393+
after the colon between an object key and its value. Must
3394+
contain only spaces, tabs, line feeds (LF), or carriage
3395+
returns (CR).
33783396

33793397
Return: ~
33803398
(`string`)

runtime/doc/news.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ LUA
247247
|vim.list.unique()| to deduplicate lists.
248248
|vim.list.bisect()| for binary search.
249249
• Experimental `vim.pos` and `vim.range` for Position/Range abstraction.
250+
|vim.json.encode()| has an option for pretty-formatting.
250251

251252
OPTIONS
252253

runtime/lua/vim/_meta/json.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,11 @@ function vim.json.decode(str, opts) end
3838
---@param opts? table<string,any> Options table with keys:
3939
--- - escape_slash: (boolean) (default false) Escape slash
4040
--- characters "/" in string values.
41+
--- - pretty: (table|boolean) (default false) Enables pretty-formatting when provided or `true`. Supports the following keys:
42+
--- - indent: (integer|string) (default 2) Controls how nested levels are indented.
43+
--- - If a integer: indents that many spaces per nesting level.
44+
--- - If a string: that string is used for the indentation. Must contain only spaces, tabs, line feeds (LF), or carriage returns (CR).
45+
--- - item_separator: (string) (default "\n") Separator inserted between array elements and object members. Must contain only spaces, tabs, line feeds (LF), or carriage returns (CR).
46+
--- - key_separator: (string) (default " ") Separator inserted after the colon between an object key and its value. Must contain only spaces, tabs, line feeds (LF), or carriage returns (CR).
4147
---@return string
4248
function vim.json.encode(obj, opts) end

src/cjson/lua_cjson.c

Lines changed: 192 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@
8787
#define DEFAULT_DECODE_ARRAY_WITH_ARRAY_MT 0
8888
#define DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH 1
8989
#define DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES 0
90+
#define DEFAULT_ENCODE_PRETTY_ENABLED 0
91+
#define DEFAULT_ENCODE_PRETTY_INDENT " "
92+
#define DEFAULT_ENCODE_PRETTY_ITEM_SEPARATOR "\n"
93+
#define DEFAULT_ENCODE_PRETTY_KEY_SEPARATOR " "
9094

9195
#ifdef DISABLE_INVALID_NUMBERS
9296
#undef DEFAULT_DECODE_INVALID_NUMBERS
@@ -168,6 +172,10 @@ typedef struct {
168172
int encode_keep_buffer;
169173
int encode_empty_table_as_object;
170174
int encode_escape_forward_slash;
175+
int encode_pretty_enabled;
176+
const char *encode_pretty_indent;
177+
const char *encode_pretty_item_separator;
178+
const char *encode_pretty_key_separator;
171179

172180
int decode_invalid_numbers;
173181
int decode_max_depth;
@@ -177,6 +185,10 @@ typedef struct {
177185

178186
typedef struct {
179187
const char **char2escape[256];
188+
int pretty_enabled;
189+
const char *pretty_indent;
190+
const char *pretty_item_separator;
191+
const char *pretty_key_separator;
180192
} json_encode_options_t;
181193

182194
typedef struct {
@@ -330,6 +342,20 @@ static int json_enum_option(lua_State *l, int optindex, int *setting,
330342
}
331343
*/
332344

345+
/* Process string option for a configuration function */
346+
/*
347+
static int json_string_option(lua_State *l, int optindex, const char **setting)
348+
{
349+
if (!lua_isnil(l, optindex)) {
350+
const char *value = luaL_checkstring(l, optindex);
351+
*setting = value;
352+
}
353+
354+
lua_pushstring(l, *setting ? *setting : "");
355+
return 1;
356+
}
357+
*/
358+
333359
/* Configures handling of extremely sparse arrays:
334360
* convert: Convert extremely sparse arrays into objects? Otherwise error.
335361
* ratio: 0: always allow sparse; 1: never allow sparse; >1: use ratio
@@ -436,6 +462,75 @@ static int json_cfg_encode_keep_buffer(lua_State *l)
436462
}
437463
*/
438464

465+
/* Returns nonzero if the string consists only of allowed whitespace characters */
466+
static int is_allowed_ws(const char *s) {
467+
if (!s) return 0;
468+
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
469+
switch (*p) {
470+
case 0x20: /* space */
471+
case 0x09: /* tab */
472+
case 0x0A: /* newline */
473+
case 0x0D: /* carriage return */
474+
break;
475+
default:
476+
return 0;
477+
}
478+
}
479+
return 1;
480+
}
481+
482+
/* Configure pretty encoding */
483+
/*
484+
static int json_cfg_encode_pretty(lua_State *l)
485+
{
486+
json_config_t *cfg = json_arg_init(l, 4);
487+
488+
json_enum_option(l, 1, &cfg->encode_pretty_enabled, NULL, 1);
489+
490+
// Process "indent" option, it can be either a string or a non-negative integer.
491+
int optindex = 2;
492+
if (!lua_isnil(l, optindex)) {
493+
if (lua_isnumber(l, optindex)) {
494+
lua_Integer n = luaL_checkinteger(l, optindex);
495+
luaL_argcheck(l, n >= 0, optindex, "indent must be non-negative");
496+
497+
luaL_argcheck(l, (unsigned long long)n <= SIZE_MAX, optindex, "indent is too large");
498+
size_t indentLen = (size_t)n;
499+
500+
strbuf_t b;
501+
strbuf_init(&b, indentLen);
502+
for (size_t i = 0; i < indentLen; i++)
503+
strbuf_append_char_unsafe(&b, ' ');
504+
505+
cfg->encode_pretty_indent = strbuf_free_to_string(&b, &indentLen);
506+
} else if (lua_isstring(l, optindex)) {
507+
const char *indent = luaL_checkstring(l, optindex);
508+
if (!is_allowed_ws(indent))
509+
luaL_argerror(l, optindex, "indent may only contain space, tab, LF, or CR");
510+
cfg->encode_pretty_indent = indent;
511+
} else {
512+
luaL_argerror(l, optindex, "string or number expected");
513+
}
514+
}
515+
lua_pushstring(l, cfg->encode_pretty_indent ? cfg->encode_pretty_indent : "");
516+
517+
518+
const char *item_separator = cfg->encode_pretty_item_separator;
519+
json_string_option(l, 3, &item_separator);
520+
if (!is_allowed_ws(item_separator))
521+
luaL_argerror(l, optindex, "item_separator may only contain space, tab, LF, or CR");
522+
cfg->encode_pretty_item_separator = item_separator;
523+
524+
const char *key_separator = cfg->encode_pretty_key_separator;
525+
json_string_option(l, 4, &key_separator);
526+
if (!is_allowed_ws(key_separator))
527+
luaL_argerror(l, optindex, "key_separator may only contain space, tab, LF, or CR");
528+
cfg->encode_pretty_key_separator = key_separator;
529+
530+
return 4;
531+
}
532+
*/
533+
439534
#if defined(DISABLE_INVALID_NUMBERS) && !defined(USE_INTERNAL_FPCONV)
440535
void json_verify_invalid_number_setting(lua_State *l, int *setting)
441536
{
@@ -533,6 +628,10 @@ static void json_create_config(lua_State *l)
533628
cfg->decode_array_with_array_mt = DEFAULT_DECODE_ARRAY_WITH_ARRAY_MT;
534629
cfg->encode_escape_forward_slash = DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH;
535630
cfg->encode_skip_unsupported_value_types = DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES;
631+
cfg->encode_pretty_enabled = DEFAULT_ENCODE_PRETTY_ENABLED;
632+
cfg->encode_pretty_indent = DEFAULT_ENCODE_PRETTY_INDENT;
633+
cfg->encode_pretty_item_separator = DEFAULT_ENCODE_PRETTY_ITEM_SEPARATOR;
634+
cfg->encode_pretty_key_separator = DEFAULT_ENCODE_PRETTY_KEY_SEPARATOR;
536635

537636
#if DEFAULT_ENCODE_KEEP_BUFFER > 0
538637
strbuf_init(&cfg->encode_buf, 0);
@@ -704,6 +803,13 @@ static void json_check_encode_depth(lua_State *l, json_config_t *cfg,
704803
static int json_append_data(lua_State *l, json_encode_t *cfg,
705804
int current_depth);
706805

806+
static void json_append_item_separator_and_indent(strbuf_t *json, json_encode_t *ctx, int depth)
807+
{
808+
strbuf_append_string(json, ctx->options->pretty_item_separator);
809+
for (int i = 0; i < depth; i++)
810+
strbuf_append_string(json, ctx->options->pretty_indent);
811+
}
812+
707813
/* json_append_array args:
708814
* - lua_State
709815
* - JSON strbuf
@@ -712,15 +818,22 @@ static void json_append_array(lua_State *l, json_encode_t *ctx, int current_dept
712818
int array_length, int raw)
713819
{
714820
int comma, i, json_pos, err;
821+
int has_items = 0;
715822
strbuf_t *json = ctx->json;
716823

717824
strbuf_append_char(json, '[');
718825

719826
comma = 0;
720827
for (i = 1; i <= array_length; i++) {
828+
has_items = 1;
829+
721830
json_pos = strbuf_length(json);
722831
if (comma++ > 0)
723832
strbuf_append_char(json, ',');
833+
834+
if (ctx->options->pretty_enabled)
835+
json_append_item_separator_and_indent(json, ctx, current_depth);
836+
724837
if (raw) {
725838
lua_rawgeti(l, -1, i);
726839
} else {
@@ -742,6 +855,9 @@ static void json_append_array(lua_State *l, json_encode_t *ctx, int current_dept
742855
lua_pop(l, 1);
743856
}
744857

858+
if (has_items && ctx->options->pretty_enabled)
859+
json_append_item_separator_and_indent(json, ctx, current_depth-1);
860+
745861
strbuf_append_char(json, ']');
746862
}
747863

@@ -798,6 +914,7 @@ static void json_append_object(lua_State *l, json_encode_t *ctx,
798914
int current_depth)
799915
{
800916
int comma, keytype, json_pos, err;
917+
int has_items = 0;
801918
strbuf_t *json = ctx->json;
802919

803920
/* Object */
@@ -807,12 +924,17 @@ static void json_append_object(lua_State *l, json_encode_t *ctx,
807924
/* table, startkey */
808925
comma = 0;
809926
while (lua_next(l, -2) != 0) {
927+
has_items = 1;
928+
810929
json_pos = strbuf_length(json);
811930
if (comma++ > 0)
812931
strbuf_append_char(json, ',');
813932
else
814933
comma = 1;
815934

935+
if (ctx->options->pretty_enabled)
936+
json_append_item_separator_and_indent(json, ctx, current_depth);
937+
816938
/* table, key, value */
817939
keytype = lua_type(l, -2);
818940
if (keytype == LUA_TNUMBER) {
@@ -827,6 +949,8 @@ static void json_append_object(lua_State *l, json_encode_t *ctx,
827949
"table key must be a number or string");
828950
/* never returns */
829951
}
952+
if (ctx->options->pretty_enabled)
953+
strbuf_append_string(json, ctx->options->pretty_key_separator);
830954

831955
/* table, key, value */
832956
err = json_append_data(l, ctx, current_depth);
@@ -841,6 +965,9 @@ static void json_append_object(lua_State *l, json_encode_t *ctx,
841965
/* table, key */
842966
}
843967

968+
if (has_items && ctx->options->pretty_enabled)
969+
json_append_item_separator_and_indent(json, ctx, current_depth-1);
970+
844971
strbuf_append_char(json, '}');
845972
}
846973

@@ -966,7 +1093,13 @@ static int json_append_data(lua_State *l, json_encode_t *ctx,
9661093
static int json_encode(lua_State *l)
9671094
{
9681095
json_config_t *cfg = json_fetch_config(l);
969-
json_encode_options_t options = { .char2escape = { char2escape } };
1096+
json_encode_options_t options = {
1097+
.char2escape = { char2escape },
1098+
.pretty_enabled = DEFAULT_ENCODE_PRETTY_ENABLED,
1099+
.pretty_indent = DEFAULT_ENCODE_PRETTY_INDENT,
1100+
.pretty_item_separator = DEFAULT_ENCODE_PRETTY_ITEM_SEPARATOR,
1101+
.pretty_key_separator = DEFAULT_ENCODE_PRETTY_KEY_SEPARATOR,
1102+
};
9701103
json_encode_t ctx = { .options = &options, .cfg = cfg };
9711104
strbuf_t local_encode_buf;
9721105
strbuf_t *encode_buf;
@@ -979,26 +1112,69 @@ static int json_encode(lua_State *l)
9791112
break;
9801113
case 2:
9811114
luaL_checktype(l, 2, LUA_TTABLE);
982-
lua_getfield(l, 2, "escape_slash");
9831115

984-
/* We only handle the escape_slash option for now */
985-
if (lua_isnil(l, -1)) {
986-
lua_pop(l, 2);
987-
break;
1116+
lua_getfield(l, 2, "escape_slash");
1117+
if (!lua_isnil(l, -1)) {
1118+
luaL_checktype(l, -1, LUA_TBOOLEAN);
1119+
1120+
int escape_slash = lua_toboolean(l, -1);
1121+
if (escape_slash) {
1122+
/* This can be optimised by adding a new hard-coded escape table for this case,
1123+
* but this path will rarely if ever be used, so let's just memcpy. */
1124+
memcpy(customChar2escape, char2escape, sizeof(char2escape));
1125+
customChar2escape['/'] = "\\/";
1126+
*ctx.options->char2escape = customChar2escape;
1127+
}
9881128
}
1129+
lua_pop(l, 1);
9891130

990-
luaL_checktype(l, -1, LUA_TBOOLEAN);
1131+
lua_getfield(l, 2, "pretty");
1132+
if (!lua_isnil(l, -1)) {
1133+
if (lua_isboolean(l, -1)) {
1134+
options.pretty_enabled = lua_toboolean(l, -1);
1135+
} else {
1136+
luaL_checktype(l, -1, LUA_TTABLE);
1137+
/* enable pretty-formatting if table provided */
1138+
options.pretty_enabled = 1;
1139+
1140+
lua_getfield(l, -1, "indent");
1141+
if (lua_isnumber(l, -1)) {
1142+
lua_Integer n = luaL_checkinteger(l, -1);
1143+
luaL_argcheck(l, n >= 0, 2, "indent must be non-negative");
1144+
1145+
luaL_argcheck(l, (unsigned long long)n <= SIZE_MAX, 2, "indent too large");
1146+
size_t indent_len = (size_t)n;
1147+
1148+
strbuf_t b;
1149+
strbuf_init(&b, indent_len);
1150+
for (size_t i = 0; i < indent_len; i++)
1151+
strbuf_append_char_unsafe(&b, ' ');
1152+
1153+
options.pretty_indent = strbuf_free_to_string(&b, &indent_len);
1154+
} else if (lua_isstring(l, -1)) {
1155+
options.pretty_indent = lua_tostring(l, -1);
1156+
if (!is_allowed_ws(options.pretty_indent))
1157+
luaL_error(l, "indent may only contain space, tab, LF, or CR");
1158+
}
1159+
lua_pop(l, 1);
9911160

992-
int escape_slash = lua_toboolean(l, -1);
1161+
lua_getfield(l, -1, "item_separator");
1162+
if (lua_isstring(l, -1)) {
1163+
options.pretty_item_separator = lua_tostring(l, -1);
1164+
if (!is_allowed_ws(options.pretty_item_separator))
1165+
luaL_error(l, "item_separator may only contain space, tab, LF, or CR");
1166+
}
1167+
lua_pop(l, 1);
9931168

994-
if (escape_slash) {
995-
/* This can be optimised by adding a new hard-coded escape table for this case,
996-
* but this path will rarely if ever be used, so let's just memcpy.*/
997-
memcpy(customChar2escape, char2escape, sizeof(char2escape));
998-
customChar2escape['/'] = "\\/";
999-
*ctx.options->char2escape = customChar2escape;
1169+
lua_getfield(l, -1, "key_separator");
1170+
if (lua_isstring(l, -1)) {
1171+
options.pretty_key_separator = lua_tostring(l, -1);
1172+
if (!is_allowed_ws(options.pretty_key_separator))
1173+
luaL_error(l, "key_separator may only contain space, tab, LF, or CR");
1174+
}
1175+
lua_pop(l, 1);
1176+
}
10001177
}
1001-
10021178
lua_pop(l, 2);
10031179
break;
10041180
default:
@@ -1710,6 +1886,7 @@ int lua_cjson_new(lua_State *l)
17101886
{ "decode_invalid_numbers", json_cfg_decode_invalid_numbers },
17111887
{ "encode_escape_forward_slash", json_cfg_encode_escape_forward_slash },
17121888
{ "encode_skip_unsupported_value_types", json_cfg_encode_skip_unsupported_value_types },
1889+
{ "encode_pretty", json_cfg_encode_pretty },
17131890
*/
17141891
{ "new", lua_cjson_new },
17151892
{ NULL, NULL }

0 commit comments

Comments
 (0)