separate docs stubs Includes introducing a cool new script to turn lua-language-server's json output into markdown documentation.custom
parent
beb1f65495
commit
36a19182be
@ -1,13 +0,0 @@ |
||||
--- Module for showing transient popups over the current screen. |
||||
-- @module alerts |
||||
|
||||
local alerts = {} |
||||
|
||||
--- Shows a new alert, replacing any already visible alerts. |
||||
-- @tparam function constructor Called to create the UI for the alert. A new default root object and group will be set before calling this function.i Alerts are non-interactable; the group created for the constructor will not be granted focus. |
||||
function alerts.show(constructor) end |
||||
|
||||
--- Dismisses any visible alerts, removing them from the screen. |
||||
function alerts.hide() end |
||||
|
||||
return alerts |
@ -1,13 +0,0 @@ |
||||
--- Module for adding and removing screens from the system's backstack. |
||||
-- @module backstack |
||||
|
||||
local backstack = {} |
||||
|
||||
--- Pushes a new screen onto the backstack. |
||||
-- @tparam function constructor Called to create the UI for the new screen. A new default root object and group will be set before calling this function. The function provided should return a table holding any bindings used by this screen; the returned value is retained so long as this screen is present in the backstack. |
||||
function backstack.push(constructor) end |
||||
|
||||
--- Removes the currently active screen, and instead shows the screen underneath it on the backstack. Does nothing if this is the only existing screen. |
||||
function backstack.pop() end |
||||
|
||||
return backstack |
@ -1,13 +0,0 @@ |
||||
--- Properties and functions for handling Bluetooth connectivity |
||||
-- @module bluetooth |
||||
local bluetooth = {} |
||||
|
||||
--- Whether or not the Bluetooth stack is currently enabled. This property is writeable, and can be used to enable or disable Bluetooth. |
||||
-- @see types.Property |
||||
bluetooth.enabled = types.Property |
||||
|
||||
--- Whether or not there is an active connection to another Bluetooth device. |
||||
-- @see types.Property |
||||
bluetooth.connected = types.Property |
||||
|
||||
return bluetooth |
@ -1,59 +0,0 @@ |
||||
--- Module for accessing and updating data about the user's library of tracks. |
||||
-- @module database |
||||
|
||||
local database = {} |
||||
|
||||
--- Returns a list of all indexes in the database. |
||||
-- @treturn Array(Index) |
||||
function database.indexes() end |
||||
|
||||
--- An iterator is a userdata type that behaves like an ordinary Lua iterator. |
||||
-- @type Iterator |
||||
local Iterator = {} |
||||
|
||||
--- A TrackId is a unique identifier, representing a playable track in the |
||||
--- user's library. |
||||
-- @type TrackId |
||||
local TrackId = {} |
||||
|
||||
--- A record is an item within an Index, representing some value at a specific |
||||
--- depth. |
||||
-- @type Record |
||||
local Record = {} |
||||
|
||||
--- Gets the human-readable text representing this record. The `__tostring` |
||||
--- metatable function is an alias of this function. |
||||
-- @treturn string |
||||
function Record:title() end |
||||
|
||||
--- Returns the value that this record represents. This may be either a track |
||||
--- id, for records which uniquely identify a track, or it may be a new |
||||
--- Iterator representing the next level of depth for the current index. |
||||
--- |
||||
--- For example, each Record in the "All Albums" index corresponds to an entire |
||||
--- album of tracks; the 'contents' of such a Record is an iterator returning |
||||
--- each track in the album represented by the Record. The contents of each of |
||||
--- the returned 'track' Records would be a full Track, as there is no further |
||||
--- disambiguation needed. |
||||
-- @treturn TrackId|Iterator(Record) |
||||
function Record:contents() end |
||||
|
||||
--- An index is heirarchical, sorted, view of the tracks within the database. |
||||
--- For example, the 'All Albums' index contains, first, a sorted list of every |
||||
--- album name in the library. Then, at the second level of the index, a sorted |
||||
--- list of every track within each album. |
||||
-- @type Index |
||||
local Index = {} |
||||
|
||||
--- Gets the human-readable name of this index. This is typically something |
||||
--- like "All Albums", or "Albums by Artist". The `__tostring` metatable |
||||
--- function is an alias of this function. |
||||
-- @treturn string |
||||
function Index:name() end |
||||
|
||||
--- Returns a new iterator that can be used to access every record within the |
||||
--- first level of this index. |
||||
-- @treturn Iterator(Record) |
||||
function Index:iter() end |
||||
|
||||
return database |
@ -1,19 +0,0 @@ |
||||
--- Properties for interacting with the audio playback system |
||||
-- @module playback |
||||
|
||||
local playback = {} |
||||
|
||||
--- Whether or not any audio is *allowed* to be played. If there is a current track, then this is essentially an indicator of whether playback is paused or unpaused. |
||||
-- @see types.Property |
||||
playback.playing = types.Property |
||||
|
||||
--- Rich information about the currently playing track. |
||||
-- @see types.Property |
||||
-- @see types.Track |
||||
playback.track = types.Property |
||||
|
||||
--- The current playback position within the current track, in seconds. |
||||
-- @see types.Property |
||||
playback.position = types.Property |
||||
|
||||
return playback |
@ -1,18 +0,0 @@ |
||||
--- Properties and functions that deal with the device's battery and power state |
||||
-- @module power |
||||
|
||||
local power = {} |
||||
|
||||
--- The battery's current charge as a percentage |
||||
-- @see types.Property |
||||
power.battery_pct = types.Property |
||||
|
||||
--- The battery's current voltage, in millivolts. |
||||
-- @see types.Property |
||||
power.battery_millivolts = types.Property |
||||
|
||||
--- Whether or not the device is currently receiving external power |
||||
-- @see types.Property |
||||
power.plugged_in = types.Property |
||||
|
||||
return power |
@ -1,32 +0,0 @@ |
||||
--- Properties and functions for inspecting and manipulating the track playback queue |
||||
-- @module queue |
||||
|
||||
local queue = {} |
||||
|
||||
--- The index in the queue of the currently playing track. This may be zero if the queue is empty. |
||||
-- @see types.Property |
||||
queue.position = types.Property |
||||
|
||||
--- The total number of tracks in the queue, including tracks which have already been played. |
||||
-- @see types.Property |
||||
queue.size = types.Property |
||||
|
||||
--- Determines whether or not the queue will be restarted after the final track is played. |
||||
-- @see types.Property |
||||
queue.replay = types.Property |
||||
|
||||
-- Determines whether or not the current track will repeat indefinitely |
||||
-- @see types.Property |
||||
queue.repeat_track = types.Property |
||||
|
||||
--- Determines whether, when progressing to the next track in the queue, the next track will be chosen randomly. The random selection algorithm used is a Miller Shuffle, which guarantees that no repeat selections will be made until every item in the queue has been played. |
||||
-- @see types.Property |
||||
queue.random = types.Property |
||||
|
||||
--- Moves forward in the play queue, looping back around to the beginning if repeat is on. |
||||
function queue.next() end |
||||
|
||||
--- Moves backward in the play queue, looping back around to the end if repeat is on. |
||||
function queue.previous() end |
||||
|
||||
return queue |
@ -1,35 +0,0 @@ |
||||
--- Userdata-based types used throughout the rest of the API. These types are |
||||
--- not generally constructable within Lua code. |
||||
-- @module types |
||||
local types = {} |
||||
|
||||
--- A observable value, owned by the C++ firmware. |
||||
-- @type Property |
||||
types.Property = {} |
||||
|
||||
--- Gets the current value |
||||
-- @return The property's current value. |
||||
function Property:get() end |
||||
|
||||
--- Sets a new value. Not all properties may be set from within Lua code. For |
||||
--- example, it makes little sense to attempt to override the current battery |
||||
--- level. |
||||
-- @param val The new value. This should generally be of the same type as the existing value. |
||||
-- @return true if the new value was applied, or false if the backing C++ code rejected the new value (e.g. if it was out of range, or the wrong type). |
||||
function Property:set(val) end |
||||
|
||||
--- Invokes the given function once immediately with the current value, and then again whenever the value changes. |
||||
--- The function is invoked for *all* changes; both from the underlying C++ data, and from calls to `set` (if this is a Lua-writeable property). |
||||
--- The binding will be active **only** so long as the given function remains in scope. |
||||
-- @param fn callback function to apply property values. Must accept one argument; the updated value. |
||||
-- @return fn, for more ergonmic use with anonymous closures. |
||||
function Property:bind(fn) end |
||||
|
||||
--- Table containing information about a track. Most fields are optional. |
||||
-- @type Track |
||||
types.Track = {} |
||||
|
||||
--- The title of the track, or the filename if no title is available. |
||||
types.Track.title = "" |
||||
|
||||
return Property |
@ -1,14 +0,0 @@ |
||||
--- Module for interacting with playback volume. The Bluetooth and wired outputs store their current volume separately; this API only allows interacting with the volume of the currently used output device. |
||||
-- @module volume |
||||
|
||||
local volume = {} |
||||
|
||||
--- The current volume as a percentage of the current volume limit. |
||||
-- @see types.Property |
||||
volume.current_pct = types.Property |
||||
|
||||
--- The current volume in terms of decibels relative to line level. |
||||
-- @see types.Property |
||||
volume.current_db = types.Property |
||||
|
||||
return volume |
@ -1,11 +1,15 @@ |
||||
--- @meta |
||||
|
||||
--- The `alerts` module contains functions for showing transient popups over |
||||
--- the current screen. |
||||
--- @class alerts |
||||
local alerts = {} |
||||
|
||||
--- @param constructor function |
||||
--- Shows a new alert, replacing any other alerts. |
||||
--- @param constructor function Called to create the UI for the alert. A new default root object and group will be set before calling this function.i Alerts are non-interactable; the group created for the constructor will not be granted focus. |
||||
function alerts.show(constructor) end |
||||
|
||||
--- Dismisses any visible alerts, removing them from the screen. |
||||
function alerts.hide() end |
||||
|
||||
return alerts |
||||
|
@ -1,11 +1,20 @@ |
||||
--- @meta |
||||
|
||||
--- The `backstack` module contains functions that can be used to implement a |
||||
--- basic stack-based navigation hierarchy. See also the `screen` module, which |
||||
--- provides a class prototype meant for use with this module. |
||||
--- @class backstack |
||||
local backstack = {} |
||||
|
||||
--- @param constructor function |
||||
function backstack.push(constructor) end |
||||
--- Displays the given screen to the user. If there was already a screen being |
||||
--- displayed, then the current screen is removed from the display, and added |
||||
--- to the backstack. |
||||
--- @param screen screen The screen to display. |
||||
function backstack.push(screen) end |
||||
|
||||
--- Removes the current screen from the display, then replaces it with the |
||||
--- screen that is at the top of the backstack. This function does nothing if |
||||
--- there are no other screens in the stack. |
||||
function backstack.pop() end |
||||
|
||||
return backstack |
||||
|
@ -1,8 +1,12 @@ |
||||
--- @meta |
||||
|
||||
--- The 'bluetooth' module contains Properties and functions for interacting |
||||
--- with the device's Bluetooth capabilities. |
||||
--- @class bluetooth |
||||
--- @field enabled Property |
||||
--- @field connected Property |
||||
--- @field enabled Property Whether or not the Bluetooth stack is currently enabled. This property is writeable, and can be used to enable or disable Bluetooth. |
||||
--- @field connected Property Whether or not there is an active connection to another Bluetooth device. |
||||
--- @field paired_device Property The device that is currently paired. The bluetooth stack will automatically connected to this device if possible. |
||||
--- @field devices Property Devices nearby that have been discovered. |
||||
local bluetooth = {} |
||||
|
||||
return bluetooth |
||||
|
@ -0,0 +1,12 @@ |
||||
--- @meta |
||||
|
||||
--- The `controls` module contains Properties relating to the device's physical |
||||
--- controls. These controls include the touchwheel, the lock switch, and the |
||||
--- side buttons. |
||||
--- @class controls |
||||
--- @field scheme Property The currently configured control scheme |
||||
--- @field scroll_sensitivity Property How much rotational motion is required on the touchwheel per scroll tick. |
||||
--- @field lock_switch Property The current state of the device's lock switch. |
||||
local controls = {} |
||||
|
||||
return controls |
@ -1,33 +1,60 @@ |
||||
--- @meta |
||||
|
||||
--- The `database` module contains Properties and functions for working with |
||||
--- the device's LevelDB-backed track database. |
||||
--- @class database |
||||
--- @field updating Property Whether or not a database re-index is currently in progress. |
||||
local database = {} |
||||
|
||||
--- Returns a list of all indexes in the database. |
||||
--- @return Index[] |
||||
function database.indexes() end |
||||
|
||||
--- An iterator is a userdata type that behaves like an ordinary Lua iterator. |
||||
--- @class Iterator |
||||
local Iterator = {} |
||||
|
||||
--- A TrackId is a unique identifier, representing a playable track in the |
||||
--- user's library. |
||||
--- @class TrackId |
||||
local TrackId = {} |
||||
|
||||
--- Gets the human-readable text representing this record. The `__tostring` |
||||
--- metatable function is an alias of this function. |
||||
--- @class Record |
||||
local Record = {} |
||||
|
||||
--- @return string |
||||
function Record:title() end |
||||
|
||||
--- @return TrackId|Iterator(Record) |
||||
--- Returns the value that this record represents. This may be either a track |
||||
--- id, for records which uniquely identify a track, or it may be a new |
||||
--- Iterator representing the next level of depth for the current index. |
||||
--- |
||||
--- For example, each Record in the "All Albums" index corresponds to an entire |
||||
--- album of tracks; the 'contents' of such a Record is an iterator returning |
||||
--- each track in the album represented by the Record. The contents of each of |
||||
--- the returned 'track' Records would be a full Track, as there is no further |
||||
--- disambiguation needed. |
||||
--- @return TrackId|Iterator r A track id if this is a leaf record, otherwise a new iterator for the next level of this index. |
||||
function Record:contents() end |
||||
|
||||
--- An index is heirarchical, sorted, view of the tracks within the database. |
||||
--- For example, the 'All Albums' index contains, first, a sorted list of every |
||||
--- album name in the library. Then, at the second level of the index, a sorted |
||||
--- list of every track within each album. |
||||
--- @class Index |
||||
local Index = {} |
||||
|
||||
--- Gets the human-readable name of this index. This is typically something |
||||
--- like "All Albums", or "Albums by Artist". The `__tostring` metatable |
||||
--- function is an alias of this function. |
||||
--- @return string |
||||
function Index:name() end |
||||
|
||||
--- @return Iterator(Record) |
||||
--- Returns a new iterator that can be used to access every record within the |
||||
--- first level of this index. |
||||
--- @return Iterator it An iterator that yields `Record`s. |
||||
function Index:iter() end |
||||
|
||||
return database |
||||
|
@ -0,0 +1,9 @@ |
||||
--- @meta |
||||
|
||||
--- The `display` module contains Properties relating to the device's physical |
||||
--- display. |
||||
--- @class display |
||||
--- @field brightness Property The screen's current brightness, as a gamma-corrected percentage value from 0 to 100. |
||||
local display = {} |
||||
|
||||
return display |
@ -0,0 +1,25 @@ |
||||
--- @meta |
||||
|
||||
--- A distinct full-screen UI. Each screen has an associated LVGL UI tree and |
||||
--- group, and can be shown on-screen using the 'backstack' module. |
||||
--- Screens make use of prototype inheritance in order to provide a consistent |
||||
--- interface for the C++ firmware to work with. |
||||
--- See [Programming in Lua, chapter 16](https://www.lua.org/pil/16.2.html). |
||||
--- @class screen |
||||
local screen = {} |
||||
|
||||
--- Creates a new screen instance. |
||||
function screen:new(params) end |
||||
|
||||
--- Called just before this screen is first displayed to the user. |
||||
function screen:createUi() end |
||||
|
||||
--- Called whenever this screen is displayed to the user. |
||||
function screen:onShown() end |
||||
|
||||
--- Called whenever this screen is being hidden by the user; either because a |
||||
--- new screen is being pushed on top of this way, or because this screen has |
||||
--- been popped off of the stack. |
||||
function screen:onHidden() end |
||||
|
||||
return screen |
@ -0,0 +1,12 @@ |
||||
--- @meta |
||||
|
||||
--- The `time` module contains functions for dealing with the current time |
||||
--- since boot. |
||||
--- @class time |
||||
local time = {} |
||||
|
||||
--- Returns the time in milliseconds since the device booted. |
||||
--- @return integer |
||||
function time.ticks() end |
||||
|
||||
return time |
@ -1,13 +1,23 @@ |
||||
--- @meta |
||||
|
||||
--- A observable value, owned by the C++ firmware. |
||||
---@class Property |
||||
local property = {} |
||||
|
||||
--- @return integer|string|table|boolean val Returns the property's current value |
||||
function property:get() end |
||||
|
||||
--- Sets a new value. Not all properties may be set from within Lua code. For |
||||
--- example, it makes little sense to attempt to override the current battery |
||||
--- level. |
||||
--- @param val? integer|string|table|boolean The new value. Optional; if not argument is passed, the property will be set to 'nil'. |
||||
--- @return boolean success whether or not the new value was accepted |
||||
function property:set(val) end |
||||
|
||||
--- @param fn function |
||||
--- Invokes the given function once immediately with the current value, and then again whenever the value changes. |
||||
--- The function is invoked for *all* changes; both from the underlying C++ data, and from calls to `set` (if this is a Lua-writeable property). |
||||
--- The binding will be active **only** so long as the given function remains in scope. |
||||
--- @param fn function callback to apply property values. Must accept one argument; the updated value. |
||||
function property:bind(fn) end |
||||
|
||||
return property |
||||
|
@ -1,8 +1,11 @@ |
||||
--- @meta |
||||
|
||||
--- Module for interacting with playback volume. The Bluetooth and wired outputs store their current volume separately; this API only allows interacting with the volume of the currently used output device. |
||||
--- @class volume |
||||
--- @field current_pct Property |
||||
--- @field current_db Property |
||||
--- @field current_pct Property The current volume as a percentage of the current volume limit. |
||||
--- @field current_db Property The current volume in terms of decibels relative to line level (only applicable to headphone output) |
||||
--- @field left_bias Property An additional modifier in decibels to apply to the left channel (only applicable to headphone output) |
||||
--- @field limit_db Property The maximum allowed output volume, in terms of decibels relative to line level (only applicable to headphone output) |
||||
local volume = {} |
||||
|
||||
return volume |
||||
|
@ -0,0 +1,143 @@ |
||||
#!/usr/bin/env lua |
||||
|
||||
local json = require "json" |
||||
|
||||
if #arg > 0 then |
||||
print("usage:", arg[0]) |
||||
print([[ |
||||
reads a lua-language-server json doc output from stdin, converts it into |
||||
markdown, and writes the result to stdout]]) |
||||
return |
||||
end |
||||
|
||||
local raw_data = io.read("*all") |
||||
local parsed = json.decode(raw_data) |
||||
|
||||
local definitions_per_module = {} |
||||
|
||||
for _, class in ipairs(parsed) do |
||||
if not class.defines or not class.defines[1] then goto continue end |
||||
|
||||
-- Filter out any definitions that didn't come from us. |
||||
local path = class.defines[1].file |
||||
if not string.find(path, "/luals-stubs/", 1, true) then goto continue end |
||||
|
||||
local module_name = string.gsub(path, ".*/(%a*)%.lua", "%1") |
||||
local module = definitions_per_module[module_name] or {} |
||||
module[class.name] = class |
||||
definitions_per_module[module_name] = module |
||||
|
||||
::continue:: |
||||
end |
||||
|
||||
local function sortedPairs(t) |
||||
local keys = {} |
||||
for key in pairs(t) do |
||||
table.insert(keys, key) |
||||
end |
||||
table.sort(keys) |
||||
local generator = coroutine.create(function() |
||||
for _, key in ipairs(keys) do |
||||
coroutine.yield(key, t[key]) |
||||
end |
||||
end) |
||||
return function() |
||||
local _, key, val = coroutine.resume(generator) |
||||
return key, val |
||||
end |
||||
end |
||||
|
||||
local function printHeading(level, text) |
||||
local hashes = "" |
||||
for _ = 1, level do hashes = hashes .. "#" end |
||||
print(hashes .. " " .. text) |
||||
end |
||||
|
||||
local function filterArgs(field) |
||||
if not field.extends.args then return {} end |
||||
local ret = {} |
||||
for _, arg in ipairs(field.extends.args) do |
||||
if arg.name ~= "self" then table.insert(ret, arg) end |
||||
end |
||||
return ret |
||||
end |
||||
|
||||
local function filterReturns(field) |
||||
if not field.extends.returns then return {} end |
||||
local ret = {} |
||||
for _, r in ipairs(field.extends.returns) do |
||||
if r.desc then table.insert(ret, r) end |
||||
end |
||||
return ret |
||||
end |
||||
|
||||
local function emitField(level, prefix, field) |
||||
printHeading(level, "`" .. prefix .. "." .. field.name .. "`") |
||||
print() |
||||
print("`" .. field.extends.view .. "`") |
||||
print() |
||||
|
||||
if field.rawdesc then |
||||
print(field.rawdesc) |
||||
print() |
||||
end |
||||
|
||||
local args = filterArgs(field) |
||||
if #args > 0 then |
||||
printHeading(level + 1, "Arguments") |
||||
print() |
||||
|
||||
for _, arg in ipairs(args) do |
||||
print(string.format(" - *%s*: %s", arg.name, arg.desc)) |
||||
end |
||||
|
||||
print() |
||||
end |
||||
|
||||
local rets = filterReturns(field) |
||||
if #rets > 0 then |
||||
printHeading(level + 1, "Returns") |
||||
print() |
||||
|
||||
for _, ret in ipairs(rets) do |
||||
if #rets > 1 then |
||||
print(" - " .. ret.desc) |
||||
else |
||||
print(ret.desc) |
||||
end |
||||
end |
||||
|
||||
print() |
||||
end |
||||
end |
||||
|
||||
local function emitClass(level, prefix, class) |
||||
if not class.name then return end |
||||
|
||||
printHeading(level, "`" .. prefix .. "." .. class.name .. "`") |
||||
if class.desc then print(class.desc) end |
||||
|
||||
for _, field in ipairs(class.fields) do |
||||
emitField(level + 1, class.name, field) |
||||
end |
||||
end |
||||
|
||||
local initial_level = 3 |
||||
|
||||
for name, module in sortedPairs(definitions_per_module) do |
||||
printHeading(initial_level, "`" .. name .. "`") |
||||
|
||||
local top_level_class = module[name] |
||||
if top_level_class then |
||||
if top_level_class.desc then print(top_level_class.desc) end |
||||
for _, field in ipairs(top_level_class.fields) do |
||||
emitField(initial_level + 1, name, field) |
||||
end |
||||
end |
||||
|
||||
for _, class in sortedPairs(module) do |
||||
if class.name ~= name then |
||||
emitClass(initial_level + 1, name, class) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,388 @@ |
||||
-- |
||||
-- json.lua |
||||
-- |
||||
-- Copyright (c) 2020 rxi |
||||
-- |
||||
-- Permission is hereby granted, free of charge, to any person obtaining a copy of |
||||
-- this software and associated documentation files (the "Software"), to deal in |
||||
-- the Software without restriction, including without limitation the rights to |
||||
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
-- of the Software, and to permit persons to whom the Software is furnished to do |
||||
-- so, subject to the following conditions: |
||||
-- |
||||
-- The above copyright notice and this permission notice shall be included in all |
||||
-- copies or substantial portions of the Software. |
||||
-- |
||||
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
-- SOFTWARE. |
||||
-- |
||||
|
||||
local json = { _version = "0.1.2" } |
||||
|
||||
------------------------------------------------------------------------------- |
||||
-- Encode |
||||
------------------------------------------------------------------------------- |
||||
|
||||
local encode |
||||
|
||||
local escape_char_map = { |
||||
[ "\\" ] = "\\", |
||||
[ "\"" ] = "\"", |
||||
[ "\b" ] = "b", |
||||
[ "\f" ] = "f", |
||||
[ "\n" ] = "n", |
||||
[ "\r" ] = "r", |
||||
[ "\t" ] = "t", |
||||
} |
||||
|
||||
local escape_char_map_inv = { [ "/" ] = "/" } |
||||
for k, v in pairs(escape_char_map) do |
||||
escape_char_map_inv[v] = k |
||||
end |
||||
|
||||
|
||||
local function escape_char(c) |
||||
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) |
||||
end |
||||
|
||||
|
||||
local function encode_nil(val) |
||||
return "null" |
||||
end |
||||
|
||||
|
||||
local function encode_table(val, stack) |
||||
local res = {} |
||||
stack = stack or {} |
||||
|
||||
-- Circular reference? |
||||
if stack[val] then error("circular reference") end |
||||
|
||||
stack[val] = true |
||||
|
||||
if rawget(val, 1) ~= nil or next(val) == nil then |
||||
-- Treat as array -- check keys are valid and it is not sparse |
||||
local n = 0 |
||||
for k in pairs(val) do |
||||
if type(k) ~= "number" then |
||||
error("invalid table: mixed or invalid key types") |
||||
end |
||||
n = n + 1 |
||||
end |
||||
if n ~= #val then |
||||
error("invalid table: sparse array") |
||||
end |
||||
-- Encode |
||||
for i, v in ipairs(val) do |
||||
table.insert(res, encode(v, stack)) |
||||
end |
||||
stack[val] = nil |
||||
return "[" .. table.concat(res, ",") .. "]" |
||||
|
||||
else |
||||
-- Treat as an object |
||||
for k, v in pairs(val) do |
||||
if type(k) ~= "string" then |
||||
error("invalid table: mixed or invalid key types") |
||||
end |
||||
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) |
||||
end |
||||
stack[val] = nil |
||||
return "{" .. table.concat(res, ",") .. "}" |
||||
end |
||||
end |
||||
|
||||
|
||||
local function encode_string(val) |
||||
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' |
||||
end |
||||
|
||||
|
||||
local function encode_number(val) |
||||
-- Check for NaN, -inf and inf |
||||
if val ~= val or val <= -math.huge or val >= math.huge then |
||||
error("unexpected number value '" .. tostring(val) .. "'") |
||||
end |
||||
return string.format("%.14g", val) |
||||
end |
||||
|
||||
|
||||
local type_func_map = { |
||||
[ "nil" ] = encode_nil, |
||||
[ "table" ] = encode_table, |
||||
[ "string" ] = encode_string, |
||||
[ "number" ] = encode_number, |
||||
[ "boolean" ] = tostring, |
||||
} |
||||
|
||||
|
||||
encode = function(val, stack) |
||||
local t = type(val) |
||||
local f = type_func_map[t] |
||||
if f then |
||||
return f(val, stack) |
||||
end |
||||
error("unexpected type '" .. t .. "'") |
||||
end |
||||
|
||||
|
||||
function json.encode(val) |
||||
return ( encode(val) ) |
||||
end |
||||
|
||||
|
||||
------------------------------------------------------------------------------- |
||||
-- Decode |
||||
------------------------------------------------------------------------------- |
||||
|
||||
local parse |
||||
|
||||
local function create_set(...) |
||||
local res = {} |
||||
for i = 1, select("#", ...) do |
||||
res[ select(i, ...) ] = true |
||||
end |
||||
return res |
||||
end |
||||
|
||||
local space_chars = create_set(" ", "\t", "\r", "\n") |
||||
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") |
||||
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") |
||||
local literals = create_set("true", "false", "null") |
||||
|
||||
local literal_map = { |
||||
[ "true" ] = true, |
||||
[ "false" ] = false, |
||||
[ "null" ] = nil, |
||||
} |
||||
|
||||
|
||||
local function next_char(str, idx, set, negate) |
||||
for i = idx, #str do |
||||
if set[str:sub(i, i)] ~= negate then |
||||
return i |
||||
end |
||||
end |
||||
return #str + 1 |
||||
end |
||||
|
||||
|
||||
local function decode_error(str, idx, msg) |
||||
local line_count = 1 |
||||
local col_count = 1 |
||||
for i = 1, idx - 1 do |
||||
col_count = col_count + 1 |
||||
if str:sub(i, i) == "\n" then |
||||
line_count = line_count + 1 |
||||
col_count = 1 |
||||
end |
||||
end |
||||
error( string.format("%s at line %d col %d", msg, line_count, col_count) ) |
||||
end |
||||
|
||||
|
||||
local function codepoint_to_utf8(n) |
||||
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa |
||||
local f = math.floor |
||||
if n <= 0x7f then |
||||
return string.char(n) |
||||
elseif n <= 0x7ff then |
||||
return string.char(f(n / 64) + 192, n % 64 + 128) |
||||
elseif n <= 0xffff then |
||||
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) |
||||
elseif n <= 0x10ffff then |
||||
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, |
||||
f(n % 4096 / 64) + 128, n % 64 + 128) |
||||
end |
||||
error( string.format("invalid unicode codepoint '%x'", n) ) |
||||
end |
||||
|
||||
|
||||
local function parse_unicode_escape(s) |
||||
local n1 = tonumber( s:sub(1, 4), 16 ) |
||||
local n2 = tonumber( s:sub(7, 10), 16 ) |
||||
-- Surrogate pair? |
||||
if n2 then |
||||
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) |
||||
else |
||||
return codepoint_to_utf8(n1) |
||||
end |
||||
end |
||||
|
||||
|
||||
local function parse_string(str, i) |
||||
local res = "" |
||||
local j = i + 1 |
||||
local k = j |
||||
|
||||
while j <= #str do |
||||
local x = str:byte(j) |
||||
|
||||
if x < 32 then |
||||
decode_error(str, j, "control character in string") |
||||
|
||||
elseif x == 92 then -- `\`: Escape |
||||
res = res .. str:sub(k, j - 1) |
||||
j = j + 1 |
||||
local c = str:sub(j, j) |
||||
if c == "u" then |
||||
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) |
||||
or str:match("^%x%x%x%x", j + 1) |
||||
or decode_error(str, j - 1, "invalid unicode escape in string") |
||||
res = res .. parse_unicode_escape(hex) |
||||
j = j + #hex |
||||
else |
||||
if not escape_chars[c] then |
||||
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") |
||||
end |
||||
res = res .. escape_char_map_inv[c] |
||||
end |
||||
k = j + 1 |
||||
|
||||
elseif x == 34 then -- `"`: End of string |
||||
res = res .. str:sub(k, j - 1) |
||||
return res, j + 1 |
||||
end |
||||
|
||||
j = j + 1 |
||||
end |
||||
|
||||
decode_error(str, i, "expected closing quote for string") |
||||
end |
||||
|
||||
|
||||
local function parse_number(str, i) |
||||
local x = next_char(str, i, delim_chars) |
||||
local s = str:sub(i, x - 1) |
||||
local n = tonumber(s) |
||||
if not n then |
||||
decode_error(str, i, "invalid number '" .. s .. "'") |
||||
end |
||||
return n, x |
||||
end |
||||
|
||||
|
||||
local function parse_literal(str, i) |
||||
local x = next_char(str, i, delim_chars) |
||||
local word = str:sub(i, x - 1) |
||||
if not literals[word] then |
||||
decode_error(str, i, "invalid literal '" .. word .. "'") |
||||
end |
||||
return literal_map[word], x |
||||
end |
||||
|
||||
|
||||
local function parse_array(str, i) |
||||
local res = {} |
||||
local n = 1 |
||||
i = i + 1 |
||||
while 1 do |
||||
local x |
||||
i = next_char(str, i, space_chars, true) |
||||
-- Empty / end of array? |
||||
if str:sub(i, i) == "]" then |
||||
i = i + 1 |
||||
break |
||||
end |
||||
-- Read token |
||||
x, i = parse(str, i) |
||||
res[n] = x |
||||
n = n + 1 |
||||
-- Next token |
||||
i = next_char(str, i, space_chars, true) |
||||
local chr = str:sub(i, i) |
||||
i = i + 1 |
||||
if chr == "]" then break end |
||||
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end |
||||
end |
||||
return res, i |
||||
end |
||||
|
||||
|
||||
local function parse_object(str, i) |
||||
local res = {} |
||||
i = i + 1 |
||||
while 1 do |
||||
local key, val |
||||
i = next_char(str, i, space_chars, true) |
||||
-- Empty / end of object? |
||||
if str:sub(i, i) == "}" then |
||||
i = i + 1 |
||||
break |
||||
end |
||||
-- Read key |
||||
if str:sub(i, i) ~= '"' then |
||||
decode_error(str, i, "expected string for key") |
||||
end |
||||
key, i = parse(str, i) |
||||
-- Read ':' delimiter |
||||
i = next_char(str, i, space_chars, true) |
||||
if str:sub(i, i) ~= ":" then |
||||
decode_error(str, i, "expected ':' after key") |
||||
end |
||||
i = next_char(str, i + 1, space_chars, true) |
||||
-- Read value |
||||
val, i = parse(str, i) |
||||
-- Set |
||||
res[key] = val |
||||
-- Next token |
||||
i = next_char(str, i, space_chars, true) |
||||
local chr = str:sub(i, i) |
||||
i = i + 1 |
||||
if chr == "}" then break end |
||||
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end |
||||
end |
||||
return res, i |
||||
end |
||||
|
||||
|
||||
local char_func_map = { |
||||
[ '"' ] = parse_string, |
||||
[ "0" ] = parse_number, |
||||
[ "1" ] = parse_number, |
||||
[ "2" ] = parse_number, |
||||
[ "3" ] = parse_number, |
||||
[ "4" ] = parse_number, |
||||
[ "5" ] = parse_number, |
||||
[ "6" ] = parse_number, |
||||
[ "7" ] = parse_number, |
||||
[ "8" ] = parse_number, |
||||
[ "9" ] = parse_number, |
||||
[ "-" ] = parse_number, |
||||
[ "t" ] = parse_literal, |
||||
[ "f" ] = parse_literal, |
||||
[ "n" ] = parse_literal, |
||||
[ "[" ] = parse_array, |
||||
[ "{" ] = parse_object, |
||||
} |
||||
|
||||
|
||||
parse = function(str, idx) |
||||
local chr = str:sub(idx, idx) |
||||
local f = char_func_map[chr] |
||||
if f then |
||||
return f(str, idx) |
||||
end |
||||
decode_error(str, idx, "unexpected character '" .. chr .. "'") |
||||
end |
||||
|
||||
|
||||
function json.decode(str) |
||||
if type(str) ~= "string" then |
||||
error("expected argument of type string, got " .. type(str)) |
||||
end |
||||
local res, idx = parse(str, next_char(str, 1, space_chars, true)) |
||||
idx = next_char(str, idx, space_chars, true) |
||||
if idx <= #str then |
||||
decode_error(str, idx, "trailing garbage") |
||||
end |
||||
return res |
||||
end |
||||
|
||||
|
||||
return json |
Loading…
Reference in new issue