-- lib/clod.lua -- -- Configuration Language Organised (by) Dots -- -- Copyright 2012 Daniel Silverstone -- local type = type local getinfo = debug.getinfo local pcall = pcall local loadstring = loadstring local tconcat = table.concat local setmetatable = setmetatable local setfenv = setfenv local pairs = pairs -- metatable for clod config operations local clod_mt = {} -- metadata for clod config instances local metadata = setmetatable({}, {__mode = "k"}) -- metatable for settings objects local settings_mt = {} local gen_settings function settings_mt:__index(subkey) local meta = metadata[self] local confmeta = metadata[meta.conf] local key = subkey if meta.prefix then key = meta.prefix .. "." .. subkey end if confmeta.settings[key] then return confmeta.settings[key].value end for k in pairs(confmeta.settings) do if k:sub(1,#key) == key then return gen_settings(self, key) end end end local function insert_after(entry, new_entry) -- Interject new_entry after entry if entry.next then entry.next.prev = new_entry end new_entry.next = entry.next new_entry.prev = entry entry.next = new_entry -- Now sort the line numbers out local lineno = entry.lineno while entry do entry.lineno = lineno lineno = lineno + 1 entry = entry.next end end local function delete_entry(entry) if entry.prev then entry.prev.next = entry.next end if entry.next then entry.next.prev = entry.prev end -- Shuffle all lines from here on down away entry = entry.next while entry do entry.lineno = entry.lineno - 1 entry = entry.next end end function settings_mt:__newindex(subkey, value) local meta = metadata[self] local confmeta = metadata[meta.conf] local key = subkey if meta.prefix then key = meta.prefix .. "." .. subkey end if value == nil then -- removing an entry... if confmeta.settings[key] then -- Need to remove *this* entry local entry = confmeta.settings[key] local prev = entry.prev local next = entry.next delete_entry(entry) if prev and next then -- Also delete 'next' if prev is also blank if not prev.key and not next.key then delete_entry(next) end elseif prev and not next then -- Also delete prev, if it's not the zeroth sentinel -- and it's blank, since we've removed the last line if not prev.key and prev.lineno > 0 then delete_entry(prev) end end end elseif confmeta.settings[key] then -- Replacing extant entry confmeta.settings[key].value = value else -- Inventing a new entry, let's try and find a good -- spot for it. -- -- Search the list, looking for the longest common prefix. -- Place the new element at the end of any section of that -- longest common prefix, or else at the end. -- If placing at the end, insert a blank line if necessary -- to separate it from something without a common prefix. local longest_prefix = 0 local longest_prefix_found_at = nil local entry = confmeta.entries while entry do if entry.key then local maxpos = 0 for i = (#key < #entry.key and #key or #entry.key), 1, -1 do if key:sub(1,i) == entry.key:sub(1,i) then if key:sub(1,i):find("%.") then maxpos = i break end end end if maxpos > longest_prefix then longest_prefix = maxpos longest_prefix_found_at = entry end end entry = entry.next end local insert_blank = false if longest_prefix == 0 then local last = confmeta.entries while last.next do last = last.next end longest_prefix_found_at = last if last.key then insert_blank = true end end local before = longest_prefix_found_at if insert_blank then insert_after(before, {}) before = before.next end insert_after(before, { key = key, value = value, }) confmeta.settings[key] = before.next end end -- Helper routines function gen_settings(tab, prefix) local meta = metadata[tab] local clodconf = meta.conf or tab local newmeta = { conf = clodconf, prefix = prefix } local ret = setmetatable({}, settings_mt) metadata[ret] = newmeta return ret end -- Methods for clod instances local methods = {} function methods:serialise() local entries = metadata[self].entries local retstr = {} local function serialise_entry(entry) local key, value, line = entry.key, entry.value, "" if entry.key then local vtype = type(value) if vtype == "string" then line = ("%s %q"):format(key, value) elseif vtype == "number" then line = ("%s = %d"):format(key, value) elseif vtype == "boolean" then line = ("%s = %s"):format(key, value and "true" or "false") else line = ("-- %s was a %s. Cannot serialise"):format(key, vtype) end end retstr[#retstr+1] = line end while entries do if entries.lineno ~= 0 then serialise_entry(entries) end entries = entries.next end serialise_entry({}) return tconcat(retstr, "\n") end -- Metamethods for clod instances function clod_mt:__index(key) if key == "settings" then return gen_settings(self) elseif methods[key] then return methods[key] end end local function parse_config(conf, confname) local ret = {} local settings = {} local last_entry = {lineno = 0} local front_entry = last_entry local keys = {} local parse_mt = {} local function gen_hook(key) local ret = setmetatable({}, parse_mt) keys[ret] = key return ret end function parse_mt:__index(key) local prefix = keys[self] if not prefix then -- This is a global indexing, so return a fresh entry return gen_hook(key) end -- A 'local' indexing, so combine with the key return gen_hook(("%s.%s"):format(prefix, key)) end function parse_mt:__newindex(key, value) -- This is the equivalent of 'foo = "bar"' instead of 'foo "bar"' return self[key](value) end function parse_mt:__call(value) local key = assert(keys[self]) local curline = getinfo(2, "Snlf").currentline local entry = { key = key, value = value, lineno = curline } while last_entry.lineno < (curline - 1) do local empty = { lineno = last_entry.lineno + 1, prev = last_entry } last_entry.next = empty last_entry = empty end last_entry.next = entry entry.prev = last_entry last_entry = entry settings[key] = entry end local func, msg = loadstring(conf, ("@%s"):format(confname or "clod-config")) if not func then return nil, msg end local globs = setmetatable({}, parse_mt) setfenv(func, globs) local ok, err = pcall(func) if not ok then return nil, err end -- Successfully loaded the settings, they're in settings and front_entry -- points to line zero which we keep, for cleanliness -- Construct a return object ready for magic local ret = setmetatable({}, clod_mt) metadata[ret] = { settings = settings, entries = front_entry, } return ret end return { parse = parse_config, }