mirror of
https://github.com/koreader/koreader
synced 2024-10-31 21:20:20 +00:00
4e5def4282
* Mangle stupid defaults test so that it compares tables, and not a non-deterministic string representation of one. It's still extremely dumb and annoying to update. (i.e., feel free to kill it with fire in a subsequent PR, I think everybody would cheer). * Rewrite DepGraph to be deterministic i.e., fully array based, no more hashes, which means no more pairs randomly re-ordering stuff. Insertion order is now preserved. Pretty sure a couple of bugs have been fixed and/or added along the way ;p. * Resync frontend/apps/filemanager/lib/md.lua w/ upstream And use orderedPairs in the attribute parsing code, just to make that stupid test happy.
261 lines
8.1 KiB
Lua
261 lines
8.1 KiB
Lua
--[[--
|
|
DepGraph module.
|
|
Library for constructing dependency graphs.
|
|
|
|
Example:
|
|
|
|
local dg = DepGraph:new{}
|
|
dg:addNode('a1', {'a2', 'b1'})
|
|
dg:addNode('b1', {'a2', 'c1'})
|
|
dg:addNode('c1')
|
|
-- The return value of dg:serialize() will be:
|
|
-- {'a2', 'c1', 'b1', 'a1'}
|
|
|
|
NOTE: Insertion order is preserved, duplicates are automatically prevented (both as main nodes and as deps).
|
|
|
|
]]
|
|
|
|
local DepGraph = {}
|
|
|
|
function DepGraph:new(new_o)
|
|
local o = new_o or {}
|
|
o.nodes = {}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end
|
|
|
|
-- Check if node exists, and is active
|
|
function DepGraph:checkNode(id)
|
|
for _, n in ipairs(self.nodes) do
|
|
if n.key == id and not n.disabled then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- Returns a (node, node_index) tuple
|
|
function DepGraph:getNode(id)
|
|
for i, n in ipairs(self.nodes) do
|
|
if n.key == id then
|
|
return n, i
|
|
end
|
|
end
|
|
|
|
return nil, nil
|
|
end
|
|
|
|
-- Like getNode, but only for active nodes:
|
|
-- if node is nil but index is set, node is disabled
|
|
function DepGraph:getActiveNode(id)
|
|
local node, index = self:getNode(id)
|
|
if node and node.disabled then
|
|
return nil, index
|
|
end
|
|
|
|
return node, index
|
|
end
|
|
|
|
-- Add a node, with an optional list of dependencies
|
|
-- If dependencies don't exist as proper nodes yet, they'll be created, in order.
|
|
-- If node already exists, the new list of dependencies is *appended* to the existing one, without duplicates.
|
|
function DepGraph:addNode(node_key, deps)
|
|
-- Find main node if it already exists
|
|
local node = self:getNode(node_key)
|
|
|
|
if node then
|
|
-- If it exists, but was disabled, re-enable it
|
|
if node.disabled then
|
|
node.disabled = nil
|
|
end
|
|
else
|
|
-- If it doesn't exist at all, create it
|
|
node = { key = node_key }
|
|
table.insert(self.nodes, node)
|
|
end
|
|
|
|
-- No dependencies? We're done!
|
|
if not deps then
|
|
return
|
|
end
|
|
|
|
-- Create dep nodes if they don't already exist
|
|
local node_deps = node.deps or {}
|
|
for _, dep_node_key in ipairs(deps) do
|
|
local dep_node = self:getNode(dep_node_key)
|
|
|
|
if dep_node then
|
|
-- If it exists, but was disabled, re-enable it
|
|
if dep_node.disabled then
|
|
dep_node.disabled = nil
|
|
end
|
|
else
|
|
-- Create dep node itself if need be
|
|
dep_node = { key = dep_node_key }
|
|
table.insert(self.nodes, dep_node)
|
|
end
|
|
|
|
-- Update deps array the long way 'round, and prevent duplicates, in case deps was funky as hell.
|
|
local exists = false
|
|
for _, k in ipairs(node_deps) do
|
|
if k == dep_node_key then
|
|
exists = true
|
|
break
|
|
end
|
|
end
|
|
if not exists then
|
|
table.insert(node_deps, dep_node_key)
|
|
end
|
|
end
|
|
-- Update main node with its updated deps
|
|
node.deps = node_deps
|
|
end
|
|
|
|
-- Attempt to remove a node, as well as all traces of it from other nodes' deps
|
|
-- If node has deps, it's kept, but marked as disabled, c.f., lenghty comment below.
|
|
function DepGraph:removeNode(node_key)
|
|
-- We shouldn't remove a node if it has dependencies (as these may have been added via addNodeDep
|
|
-- (as opposed to the optional deps list passed to addNode), like what InputContainer does with overrides,
|
|
-- overrides originating from completely *different* nodes,
|
|
-- meaning those other nodes basically add themselves to another's deps).
|
|
-- We don't want to lose the non-native dependency on these other nodes in case we later re-addNode this one
|
|
-- with its stock dependency list.
|
|
local node, index = self:getNode(node_key)
|
|
if node then
|
|
if not node.deps or #node.deps == 0 then
|
|
-- No dependencies, can be wiped safely
|
|
table.remove(self.nodes, index)
|
|
else
|
|
-- Can't remove it, just flag it as disabled instead
|
|
node.disabled = true
|
|
end
|
|
end
|
|
-- On the other hand, we definitely should remove it from the deps of every *other* node.
|
|
for _, curr_node in ipairs(self.nodes) do
|
|
-- Is not the to be removed node, and has deps
|
|
if curr_node.key ~= node_key and curr_node.deps then
|
|
-- Walk that node's deps to check if it depends on us
|
|
for idx, dep_node_key in ipairs(curr_node.deps) do
|
|
-- If it did, wipe ourselves from there
|
|
if dep_node_key == node_key then
|
|
table.remove(curr_node.deps, idx)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Add a single dep_node_key to node_key's deps
|
|
function DepGraph:addNodeDep(node_key, dep_node_key)
|
|
-- Check the main node
|
|
local node = self:getNode(node_key)
|
|
|
|
if node then
|
|
-- If it exists, but was disabled, re-enable it
|
|
if node.disabled then
|
|
node.disabled = nil
|
|
end
|
|
else
|
|
-- If it doesn't exist at all, create it
|
|
node = { key = node_key }
|
|
table.insert(self.nodes, node)
|
|
end
|
|
|
|
-- Then check the dep node
|
|
local dep_node = self:getNode(dep_node_key)
|
|
|
|
if dep_node then
|
|
-- If it exists, but was disabled, re-enable it
|
|
if dep_node.disabled then
|
|
dep_node.disabled = nil
|
|
end
|
|
else
|
|
-- Create dep node itself if need be
|
|
dep_node = { key = dep_node_key }
|
|
table.insert(self.nodes, dep_node)
|
|
end
|
|
|
|
-- If main node currently doesn't have deps, start with an empty array
|
|
if not node.deps then
|
|
node.deps = {}
|
|
end
|
|
|
|
-- Prevent duplicate deps
|
|
local exists = false
|
|
for _, k in ipairs(node.deps) do
|
|
if k == dep_node_key then
|
|
exists = true
|
|
break
|
|
end
|
|
end
|
|
if not exists then
|
|
table.insert(node.deps, dep_node_key)
|
|
end
|
|
end
|
|
|
|
-- Remove a single dep_node_key from node_key's deps
|
|
function DepGraph:removeNodeDep(node_key, dep_node_key)
|
|
local node = self:getNode(node_key)
|
|
if node.deps then
|
|
for idx, dep_key in ipairs(node.deps) do
|
|
if dep_key == dep_node_key then
|
|
table.remove(node.deps, idx)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Return a list (array) of node keys, ordered by insertion order and dependency.
|
|
-- Dependencies come first (and are also ordered by insertion order themselves).
|
|
function DepGraph:serialize()
|
|
local visited = {}
|
|
local ordered_nodes = {}
|
|
|
|
for _, n in ipairs(self.nodes) do
|
|
local node_key = n.key
|
|
if not visited[node_key] then
|
|
local queue = { node_key }
|
|
while #queue > 0 do
|
|
local pos = #queue
|
|
local curr_node_key = queue[pos]
|
|
local curr_node = self:getActiveNode(curr_node_key)
|
|
local all_deps_visited = true
|
|
if curr_node and curr_node.deps then
|
|
for _, dep_node_key in ipairs(curr_node.deps) do
|
|
if not visited[dep_node_key] then
|
|
-- Only insert to queue for later process if node has dependencies
|
|
local dep_node = self:getActiveNode(dep_node_key)
|
|
-- Only if it was active!
|
|
if dep_node then
|
|
if dep_node.deps then
|
|
table.insert(queue, dep_node_key)
|
|
else
|
|
table.insert(ordered_nodes, dep_node_key)
|
|
end
|
|
end
|
|
visited[dep_node_key] = true
|
|
all_deps_visited = false
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if all_deps_visited then
|
|
visited[curr_node_key] = true
|
|
table.remove(queue, pos)
|
|
-- Only if it was active!
|
|
if curr_node then
|
|
table.insert(ordered_nodes, curr_node_key)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return ordered_nodes
|
|
end
|
|
|
|
return DepGraph
|