Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Web.JSON improve to v8 #695

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
140 changes: 78 additions & 62 deletions autoload/vital/__vital__/Web/JSON.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"), '')
Expand All @@ -99,59 +96,78 @@ 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)
Milly marked this conversation as resolved.
Show resolved Hide resolved
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)
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')
Expand All @@ -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]",
Expand All @@ -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
Expand All @@ -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),
\ ),
\)
Expand Down
85 changes: 57 additions & 28 deletions doc/vital/Web/JSON.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This point is incompatible changing. need appending item to Changes.

Please Changes appending to commit after all changes are done, and before PR merging.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the point you say Add 'allow_nan' or Remove 'use_token'?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

I think, Adding phrase to Changes for example Improve and *use_token* remove, and option renewal to support *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
<

=============================================================================
Expand Down
Loading