diff --git a/ldoc-stubs/alerts.lua b/ldoc-stubs/alerts.lua deleted file mode 100644 index 6fecdd7c..00000000 --- a/ldoc-stubs/alerts.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/backstack.lua b/ldoc-stubs/backstack.lua deleted file mode 100644 index d4807d37..00000000 --- a/ldoc-stubs/backstack.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/bluetooth.lua b/ldoc-stubs/bluetooth.lua deleted file mode 100644 index 3160ef7e..00000000 --- a/ldoc-stubs/bluetooth.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/database.lua b/ldoc-stubs/database.lua deleted file mode 100644 index 97359ab1..00000000 --- a/ldoc-stubs/database.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/playback.lua b/ldoc-stubs/playback.lua deleted file mode 100644 index 07ed65f6..00000000 --- a/ldoc-stubs/playback.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/power.lua b/ldoc-stubs/power.lua deleted file mode 100644 index 466cafed..00000000 --- a/ldoc-stubs/power.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/queue.lua b/ldoc-stubs/queue.lua deleted file mode 100644 index b3000040..00000000 --- a/ldoc-stubs/queue.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/types.lua b/ldoc-stubs/types.lua deleted file mode 100644 index 3480610c..00000000 --- a/ldoc-stubs/types.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/volume.lua b/ldoc-stubs/volume.lua deleted file mode 100644 index 7eff24c5..00000000 --- a/ldoc-stubs/volume.lua +++ /dev/null @@ -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 diff --git a/luals-stubs/alerts.lua b/luals-stubs/alerts.lua index d430f12d..420194eb 100644 --- a/luals-stubs/alerts.lua +++ b/luals-stubs/alerts.lua @@ -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 diff --git a/luals-stubs/backstack.lua b/luals-stubs/backstack.lua index 2e4eccb3..b39fcbf2 100644 --- a/luals-stubs/backstack.lua +++ b/luals-stubs/backstack.lua @@ -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 diff --git a/luals-stubs/bluetooth.lua b/luals-stubs/bluetooth.lua index 09fc7606..a2dd476d 100644 --- a/luals-stubs/bluetooth.lua +++ b/luals-stubs/bluetooth.lua @@ -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 diff --git a/luals-stubs/controls.lua b/luals-stubs/controls.lua new file mode 100644 index 00000000..7034dc55 --- /dev/null +++ b/luals-stubs/controls.lua @@ -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 diff --git a/luals-stubs/database.lua b/luals-stubs/database.lua index e23c085b..753961fe 100644 --- a/luals-stubs/database.lua +++ b/luals-stubs/database.lua @@ -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 diff --git a/luals-stubs/display.lua b/luals-stubs/display.lua new file mode 100644 index 00000000..74bcdca9 --- /dev/null +++ b/luals-stubs/display.lua @@ -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 diff --git a/luals-stubs/playback.lua b/luals-stubs/playback.lua index cd54ddb3..85392e93 100644 --- a/luals-stubs/playback.lua +++ b/luals-stubs/playback.lua @@ -1,6 +1,7 @@ --- @meta ---- Properties for interacting with the audio playback system +--- The `playback` module contains Properties and functions for interacting +--- the device's audio pipeline. --- @class playback --- @field playing Property Whether or not audio is allowed to be played. if there is a current track, then this indicated whether playback is paused or unpaused. If there is no current track, this determines what will happen when the first track is added to the queue. --- @field track Property The currently playing track. diff --git a/luals-stubs/power.lua b/luals-stubs/power.lua index 226f8200..ac7f15bb 100644 --- a/luals-stubs/power.lua +++ b/luals-stubs/power.lua @@ -1,6 +1,7 @@ --- @meta ---- Properties and functions that deal with the device's battery and power state. +--- The `power` module contains properties and functions that relate to the +--- device's battery and charging state. --- @class power --- @field battery_pct Property The battery's current charge, as a percentage of the maximum charge. --- @field battery_millivolts Property The battery's current voltage, in millivolts. diff --git a/luals-stubs/queue.lua b/luals-stubs/queue.lua index 08247799..353b4823 100644 --- a/luals-stubs/queue.lua +++ b/luals-stubs/queue.lua @@ -1,15 +1,31 @@ --- @meta ---- Properties and functions for inspecting and manipulating the track playback queue +--- The `queue` module contains Properties and functions that relate to the +--- device's playback queue. This is a persistent, disk-backed list of TrackIds +--- that includes the currently playing track, tracks that have been played, +--- and tracks that are scheduled to be played after the current track has +--- finished. --- @class queue --- @field position Property The index in the queue of the currently playing track. This may be zero if the queue is empty. Writeable. --- @field size Property The total number of tracks in the queue, including tracks which have already been played. --- @field replay Property Whether or not the queue will be restarted after the final track is played. Writeable. --- @field repeat_track Property Whether or not the current track will repeat indefinitely. Writeable. ---- @field random 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. Writeable. +--- @field random 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. Writeable. local queue = {} +--- Adds the given track or database iterator to the end of the queue. Database +--- iterators passed to this method will be unnested and expanded into the track +--- ids they contain. +--- @param val TrackId|Iterator +function queue.add(val) end + +--- Removes all tracks from the queue. +function queue.clear() end + +--- 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 diff --git a/luals-stubs/screen.lua b/luals-stubs/screen.lua new file mode 100644 index 00000000..55253f1c --- /dev/null +++ b/luals-stubs/screen.lua @@ -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 diff --git a/luals-stubs/time.lua b/luals-stubs/time.lua new file mode 100644 index 00000000..95bbabdb --- /dev/null +++ b/luals-stubs/time.lua @@ -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 diff --git a/luals-stubs/types.lua b/luals-stubs/types.lua index ecfee29b..c974010d 100644 --- a/luals-stubs/types.lua +++ b/luals-stubs/types.lua @@ -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 diff --git a/luals-stubs/volume.lua b/luals-stubs/volume.lua index 42d65884..50046f66 100644 --- a/luals-stubs/volume.lua +++ b/luals-stubs/volume.lua @@ -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 diff --git a/tools/luals-gendoc/gendoc.lua b/tools/luals-gendoc/gendoc.lua new file mode 100755 index 00000000..15f46b3c --- /dev/null +++ b/tools/luals-gendoc/gendoc.lua @@ -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 diff --git a/tools/luals-gendoc/json.lua b/tools/luals-gendoc/json.lua new file mode 100644 index 00000000..711ef786 --- /dev/null +++ b/tools/luals-gendoc/json.lua @@ -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