diff --git a/autoload/vital/__vital__/Web/JSON.vim b/autoload/vital/__vital__/Web/JSON.vim index 0967dd8c3..fabd6c6f5 100644 --- a/autoload/vital/__vital__/Web/JSON.vim +++ b/autoload/vital/__vital__/Web/JSON.vim @@ -38,38 +38,31 @@ let s:control_chars = { \ } lockvar s:control_chars -function! s:_true() abort - return 1 -endfunction - -function! s:_false() abort - return 0 -endfunction - -function! s:_null() abort - return 0 -endfunction +let s:float_constants = { + \ 'nan': 'NaN', + \ '-nan': 'NaN', + \ 'inf': 'Infinity', + \ '-inf': '-Infinity', + \ } +let s:float_nan = 0.0 / 0 +let s:float_inf = 1.0 / 0 +lockvar s:float_constants s:float_nan s:float_inf -function! s:_resolve(val, prefix) abort - let t = type(a:val) - if t == type('') - let m = matchlist(a:val, '^' . a:prefix . '\(null\|true\|false\)$') - if !empty(m) - return s:const[m[1]] - endif - elseif t == type([]) || t == type({}) - return map(a:val, 's:_resolve(v:val, a:prefix)') - endif - return a:val -endfunction +let s:special_constants = { + \ 'v:true': 'true', + \ 'v:false': 'false', + \ 'v:null': 'null', + \ 'v:none': 'null', + \ } +lockvar s:special_constants function! s:_vital_created(module) abort " define constant variables if !exists('s:const') let s:const = {} - let s:const.true = function('s:_true') - let s:const.false = function('s:_false') - let s:const.null = function('s:_null') + let s:const.true = v:true + let s:const.false = v:false + let s:const.null = v:null lockvar s:const endif call extend(a:module, s:const) @@ -78,18 +71,22 @@ endfunction function! s:_vital_loaded(V) abort let s:V = a:V let s:string = s:V.import('Data.String') + let s:bytes = s:V.import('Data.List.Byte') + let s:t = s:V.import('Vim.Type').types endfunction function! s:_vital_depends() abort - return ['Data.String'] + return ['Data.String', 'Data.List.Byte', 'Vim.Type'] endfunction " @vimlint(EVL102, 1, l:null) " @vimlint(EVL102, 1, l:true) " @vimlint(EVL102, 1, l:false) +" @vimlint(EVL102, 1, l:NaN) +" @vimlint(EVL102, 1, l:Infinity) function! s:decode(json, ...) abort let settings = extend({ - \ 'use_token': 0, + \ 'allow_nan': 1, \}, get(a:000, 0, {})) let json = iconv(a:json, 'utf-8', &encoding) let json = join(split(json, "\n"), '') @@ -99,51 +96,70 @@ function! s:decode(json, ...) abort let json = substitute(json, '\([\uD800-\uDBFF]\)\([\uDC00-\uDFFF]\)', \ '\=nr2char(0x10000+and(0x7ff,char2nr(submatch(1)))*0x400+and(0x3ff,char2nr(submatch(2))))', \ 'g') - if settings.use_token - let prefix = '__Web.JSON__' - while stridx(json, prefix) != -1 - let prefix .= '_' - endwhile - let [null,true,false] = map(['null','true','false'], 'prefix . v:val') - sandbox return s:_resolve(eval(json), prefix) - else - let [null,true,false] = [s:const.null(),s:const.true(),s:const.false()] - sandbox return eval(json) + if settings.allow_nan + let [NaN,Infinity] = [s:float_nan,s:float_inf] endif + let [null, true, false] = [v:null, v:true, v:false] + sandbox return eval(json) endfunction " @vimlint(EVL102, 0, l:null) " @vimlint(EVL102, 0, l:true) " @vimlint(EVL102, 0, l:false) +" @vimlint(EVL102, 0, l:NaN) +" @vimlint(EVL102, 0, l:Infinity) function! s:encode(val, ...) abort let settings = extend({ \ 'indent': 0, + \ 'allow_nan': 1, + \ 'from_encoding': &encoding, + \ 'ensure_ascii': 0, \}, get(a:000, 0, {}) \) - if type(a:val) == 0 + let json = s:_encode(a:val, settings) + if settings.ensure_ascii + let json = substitute(json, '[\U0000007f-\U0010ffff]', + \ {m -> s:_escape_unicode_chars(m[0])}, 'g') + endif + return json +endfunction + +function! s:_escape_unicode_chars(char) abort + let n = char2nr(a:char) + if n < 0x10000 + return printf('\u%04x', n) + else + let n -= 0x10000 + return printf('\u%04x%\u%04x', 0xd800 + n / 0x400, 0xdc00 + and(0x3ff, n)) + endif +endfunction + +function! s:_encode(val, settings) abort + let t = type(a:val) + if t ==# s:t.number return a:val - elseif type(a:val) == 1 - let s = substitute(a:val, '[\x01-\x1f\\"]', '\=s:control_chars[submatch(0)]', 'g') - let s = iconv(s, &encoding, 'utf-8') + elseif t ==# s:t.string + let s = iconv(a:val, a:settings.from_encoding, 'utf-8') + let s = substitute(s, '[\x01-\x1f\\"]', '\=s:control_chars[submatch(0)]', 'g') return '"' . s . '"' - elseif type(a:val) == 2 - if s:const.true == a:val - return 'true' - elseif s:const.false == a:val - return 'false' - elseif s:const.null == a:val - return 'null' - else - " backward compatibility - return string(a:val) + elseif t ==# s:t.list + return s:_encode_list(a:val, a:settings) + elseif t ==# s:t.dict + return s:_encode_dict(a:val, a:settings) + elseif t ==# s:t.float + let val = string(a:val) + if a:settings.allow_nan + let val = get(s:float_constants, val, val) + elseif has_key(s:float_constants, val) + throw 'vital: Web.JSON: Invalid float value: ' . val endif - elseif type(a:val) == 3 - return s:_encode_list(a:val, settings) - elseif type(a:val) == 4 - return s:_encode_dict(a:val, settings) - else - return string(a:val) + return val + elseif t ==# s:t.bool || t ==# s:t.none + return get(s:special_constants, a:val) + elseif t ==# s:t.blob + return s:_encode_list(s:bytes.from_blob(a:val), a:settings) endif + throw 'vital: Web.JSON: Invalid argument: ' . string(a:val) endfunction " @vimlint(EVL102, 1, l:ns) @@ -151,7 +167,7 @@ function! s:_encode_list(val, settings) abort if empty(a:val) return '[]' elseif !a:settings.indent - let encoded_candidates = map(copy(a:val), 's:encode(v:val, a:settings)') + let encoded_candidates = map(copy(a:val), 's:_encode(v:val, a:settings)') return printf('[%s]', join(encoded_candidates, ',')) else let previous_indent = get(a:settings, '_previous_indent') @@ -161,7 +177,7 @@ function! s:_encode_list(val, settings) abort \}) let encoded_candidates = map( \ copy(a:val), - \ printf('''%s'' . s:encode(v:val, ns)', repeat(' ', indent)), + \ printf('''%s'' . s:_encode(v:val, ns)', repeat(' ', indent)), \) return printf( \ "[\n%s\n%s]", @@ -178,7 +194,7 @@ function! s:_encode_dict(val, settings) abort return '{}' elseif !a:settings.indent let encoded_candidates = map(keys(a:val), - \ 's:encode(v:val, a:settings) . '':'' . s:encode(a:val[v:val], a:settings)' + \ 's:_encode(v:val, a:settings) . '':'' . s:_encode(a:val[v:val], a:settings)' \) return printf('{%s}', join(encoded_candidates, ',')) else @@ -189,7 +205,7 @@ function! s:_encode_dict(val, settings) abort \}) let encoded_candidates = map(keys(a:val), \ printf( - \ '''%s'' . s:encode(v:val, ns) . '': '' . s:encode(a:val[v:val], ns)', + \ '''%s'' . s:_encode(v:val, ns) . '': '' . s:_encode(a:val[v:val], ns)', \ repeat(' ', indent), \ ), \) diff --git a/doc/vital/Web/JSON.txt b/doc/vital/Web/JSON.txt index 65dce8edf..184cce31e 100644 --- a/doc/vital/Web/JSON.txt +++ b/doc/vital/Web/JSON.txt @@ -23,34 +23,39 @@ INTERFACE *Vital.Web.JSON-interface* CONSTS *Vital.Web.JSON-consts* true *Vital.Web.JSON.true* - It is used to indicate 'true' in JSON string. It is represented as a - |Funcref| thus if you assign the value to a variable which name does not - start with a capital, "s:", "w:", "t:" or "b:" will raise an exception. - This returns 1 when you use it as a function. + It is |v:true|. This is left for backward compatibility. false *Vital.Web.JSON.false* - It is used to indicate 'false' in JSON string. It is represented as a - |Funcref| thus if you assign the value to a variable which name does not - start with a capital, "s:", "w:", "t:" or "b:" will raise an exception. - This returns 0 when you use it as a function. + It is |v:false|. This is left for backward compatibility. null *Vital.Web.JSON.null* - It is used to indicate 'null' in JSON string. It is represented as a - |Funcref| thus if you assign the value to a variable which name does not - start with a capital, "s:", "w:", "t:" or "b:" will raise an exception. - This returns 0 when you use it as a function. + It is |v:null|. This is left for backward compatibility. ------------------------------------------------------------------------------ FUNCTIONS *Vital.Web.JSON-functions* encode({object}[, {settings}]) *Vital.Web.JSON.encode()* - Encode an object into a JSON string. Special tokens - (e.g. |Vital.Web.JSON.true|) are encoded into corresponding javascript - tokens (e.g. 'true'). -> - echo s:JSON.encode([s:JSON.true, s:JSON.false, s:JSON.null]) - " => '[true, false, null]' -< + Encode an object into a JSON string. + Vim values are converted as follows: + |Number| decimal number + |Float| floating point number + Float nan "NaN" + Float inf "Infinity" + Float -inf "-Infinity" + |String| in double quotes (possibly null) + |List| as an array (possibly null); when + used recursively: [] + |Dict| as an object (possibly null); when + used recursively: {} + |Blob| as an array of the individual bytes + v:false "false" + v:true "true" + v:none "null" + v:null "null" + |Funcref| not possible, error + |job| not possible, error + |channel| not possible, error + {settings} is a |Dictionary| which allows the following: 'indent' @@ -65,21 +70,45 @@ encode({object}[, {settings}]) *Vital.Web.JSON.encode()* " "a": 0, " "b": 1 " }' +< + 'allow_nan' + If 'allows_nan' is 0, it will raise an exception when serializing + out of range float values (nan, inf, -inf). + Otherwise 'NaN', 'Infinity' or '-Infinity' are used to represent these. + The default value is 1. + Note that NaN and Infinity are not the JSON standard. +> + echo s:JSON.encode([0.0/0, 1.0/0, -1.0/0]) + " => [NaN,Infinity,-Infinity] + echo s:JSON.encode([0.0/0], {'allow_nan': 0}) + " => vital: Web.JSON: Invalid float value: nan +< + 'ensure_ascii' + If 'ensure_ascii' is 0, all characters without control-chars + (0x01-0x1f) will be output as-is. + Otherwise the output is guaranteed to have all incoming non-ASCII + characters escaped. + The default value is 0. +> + echo s:JSON.encode(["foo", "bár", "\n"]) + " => '["foo","bár","\n"]' + echo s:JSON.encode(["foo", "bár", "\n"], {'ensure_ascii': 1}) + " => '["foo","b\u00e1r","\n"]' < decode({json}[, {settings}]) *Vital.Web.JSON.decode()* Decode a JSON string into an object that vim can treat. {settings} is a |Dictionary| which allows the following: - 'use_token' - Use special tokens (e.g. |Vital.Web.JSON.true|) to represent - corresponding javascript tokens (e.g. 'true'). - Otherwise 1 or 0 are used to represent these. - The default value is 0. + 'allow_nan' + If 'allows_nan' is 0, it will raise an exception when deserializing + float constants ('NaN', 'Infinity', '-Infinity'). + Otherwise nan, inf or -inf are used to represent these. + The default value is 1. > - echo s:JSON.decode('[true, false, null]') - " => [1, 0, 0] - echo s:JSON.decode('[true, false, null]', {'use_token': 1}) - " => [s:JSON.true, s:JSON.false, s:JSON.null] + echo s:JSON.decode('[NaN, Infinity, -Infinity]') + " => [nan, inf, -inf] + echo s:JSON.decode('[NaN]', {'allow_nan': 0}) + " => E121: Undefined variable: NaN < ============================================================================= diff --git a/test/Web/JSON.vimspec b/test/Web/JSON.vimspec index 669f3c7d3..9b35e740d 100644 --- a/test/Web/JSON.vimspec +++ b/test/Web/JSON.vimspec @@ -5,9 +5,9 @@ Describe Web.JSON Describe .constants() It should have constant variables which indicate the special tokens - Assert Match(string(JSON.true), "function('\.*_true')") - Assert Match(string(JSON.false), "function('\.*_false')") - Assert Match(string(JSON.null), "function('\.*_null')") + Assert Same(JSON.true, v:true) + Assert Same(JSON.false, v:false) + Assert Same(JSON.null, v:null) End End @@ -42,6 +42,20 @@ Describe Web.JSON Assert Equals(JSON.decode('"\ud83c\u00a0"'), "\ud83c\u00a0") End + It decodes strings with character encoding + let expected_utf8 = "sあc漢g" + Assert Equals(JSON.decode('"sあc漢g"'), expected_utf8) + + let expected_sjis = iconv("sあc漢g", 'utf-8', 'sjis') + try + let &encoding = 'sjis' + let res = JSON.decode('"sあc漢g"') + finally + let &encoding = 'utf-8' + endtry + Assert Equals(res, expected_sjis) + End + It decodes lists Assert Equals(JSON.decode('[]'), []) Assert Equals(JSON.decode('[0, 1, 2]'), [0, 1, 2]) @@ -71,15 +85,20 @@ Describe Web.JSON End It decodes special tokens (true/false/null) - " true/false/null - Assert Equals(JSON.decode('true'), 1) - Assert Equals(JSON.decode('false'), 0) - Assert Equals(JSON.decode('null'), 0) + Assert Same(JSON.decode('true'), v:true) + Assert Same(JSON.decode('false'), v:false) + Assert Same(JSON.decode('null'), v:null) + End - let s = { 'use_token': 1 } - Assert Equals(JSON.decode('true', s), JSON.true) - Assert Equals(JSON.decode('false', s), JSON.false) - Assert Equals(JSON.decode('null', s), JSON.null) + It decodes special floats (NaN/Infinity/-Infinity) + Assert Equals(string(JSON.decode('NaN')), 'nan') + Assert Equals(string(JSON.decode('Infinity')), 'inf') + Assert Equals(string(JSON.decode('-Infinity')), '-inf') + + let s = { 'allow_nan': 0 } + Throws /^Vim(\w\+):E121:/ JSON.decode('NaN', s) + Throws /^Vim(\w\+):E121:/ JSON.decode('Infinity', s) + Throws /^Vim(\w\+):E121:/ JSON.decode('-Infinity', s) End End @@ -112,6 +131,31 @@ Describe Web.JSON Assert Equals(JSON.encode("\xf0\x9f\x8d\xa3"), "\"\xf0\x9f\x8d\xa3\"") End + It encodes strings with character encoding + let str_utf8 = "sあc漢g" + Assert Equals(JSON.encode(str_utf8), '"sあc漢g"') + + let str_sjis = iconv("sあc漢g", 'utf-8', 'sjis') + try + let &encoding = 'sjis' + let res = JSON.encode(str_sjis) + finally + let &encoding = 'utf-8' + endtry + Assert Equals(res, '"sあc漢g"') + + let s = { 'from_encoding': 'sjis' } + Assert Equals(JSON.encode(str_sjis, s), '"sあc漢g"') + End + + It encodes strings with {settings.ensure_ascii} = true + let s = { 'ensure_ascii': v:true } + " multibyte + Assert Equals(JSON.encode("s¢cĴgё", s), '"s\u00a2c\u0134g\u0451"') + " UTF-16 surrogate pair + Assert Equals(JSON.encode("su\xf0\x9f\x8d\xa3shi", s), '"su\ud83c\udf63shi"') + End + It encodes lists Assert Equals(JSON.encode([]), '[]') Assert Equals(JSON.encode([0, 1, 2]), '[0,1,2]') @@ -230,6 +274,51 @@ Describe Web.JSON Assert Equals(JSON.encode(JSON.true), 'true') Assert Equals(JSON.encode(JSON.false), 'false') Assert Equals(JSON.encode(JSON.null), 'null') + + Assert Equals(JSON.encode(v:true), 'true') + Assert Equals(JSON.encode(v:false), 'false') + Assert Equals(JSON.encode(v:null), 'null') + Assert Equals(JSON.encode(v:none), 'null') + End + + It encodes special floats (NaN/Infinity/-Infinity) + Assert Equals(JSON.encode(0.0/0), 'NaN') + Assert Equals(JSON.encode(1.0/0), 'Infinity') + Assert Equals(JSON.encode(-1.0/0), '-Infinity') + + let s = { 'allow_nan': 0 } + Throws /^vital: Web.JSON:/ JSON.encode(0.0/0, s) + Throws /^vital: Web.JSON:/ JSON.encode(1.0/0, s) + Throws /^vital: Web.JSON:/ JSON.encode(-1.0/0, s) + End + + It encodes blob + if !has('patch-8.1.0735') + Skip no available blob type. + endif + Assert Equals(JSON.encode(0z), '[]') + Assert Equals(JSON.encode(0zFF.00.ED.01.5D.AF), '[255,0,237,1,93,175]') + End + + It throws exception when encodes funcref + let Funcref = function('get') + Throws /^vital: Web\.JSON: Invalid argument: / JSON.encode(Funcref) + End + + It throws exception when encodes job + if !exists('*test_null_job') + Skip no available job type. + endif + let job = test_null_job() + Throws /^vital: Web\.JSON: Invalid argument: / JSON.encode(job) + End + + It throws exception when encodes channel + if !exists('*test_null_channel') + Skip no available channel type. + endif + let ch = test_null_channel() + Throws /^vital: Web\.JSON: Invalid argument: / JSON.encode(ch) End End End