mirror of
https://github.com/koreader/koreader.git
synced 2025-12-13 20:36:53 +01:00
add provider module (#12641)
* implements a Provider singleton, to be used by thirdparty plugins * exporter: support for thirdparty providers * splits plugin loading into two steps: discovery and load 1. get a list of all candidate plugins to load for the different paths 2. sort providers before on the rest of them and try to load them
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
local lfs = require("libs/libkoreader-lfs")
|
local lfs = require("libs/libkoreader-lfs")
|
||||||
local logger = require("logger")
|
local logger = require("logger")
|
||||||
|
local util = require("util")
|
||||||
local _ = require("gettext")
|
local _ = require("gettext")
|
||||||
|
|
||||||
local DEFAULT_PLUGIN_PATH = "plugins"
|
local DEFAULT_PLUGIN_PATH = "plugins"
|
||||||
@@ -22,6 +23,22 @@ local DEPRECATION_MESSAGES = {
|
|||||||
feature = _("The following features are unmaintained and will be removed soon:"),
|
feature = _("The following features are unmaintained and will be removed soon:"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local function isProvider(name)
|
||||||
|
return name:sub(1, 8) == "provider"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sortProvidersFirst(v1, v2)
|
||||||
|
if isProvider(v1.name) and isProvider(v2.name) then
|
||||||
|
return v1.path < v2.path
|
||||||
|
elseif isProvider(v1.name) then
|
||||||
|
return true
|
||||||
|
elseif isProvider(v2.name) then
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
return v1.path < v2.path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function deprecationFmt(field)
|
local function deprecationFmt(field)
|
||||||
local s
|
local s
|
||||||
if type(field) == "table" then
|
if type(field) == "table" then
|
||||||
@@ -76,12 +93,17 @@ local PluginLoader = {
|
|||||||
all_plugins = nil,
|
all_plugins = nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
function PluginLoader:loadPlugins()
|
function PluginLoader:_discover()
|
||||||
if self.enabled_plugins then return self.enabled_plugins, self.disabled_plugins end
|
local plugins_disabled = G_reader_settings:readSetting("plugins_disabled")
|
||||||
|
if type(plugins_disabled) ~= "table" then
|
||||||
|
plugins_disabled = {}
|
||||||
|
end
|
||||||
|
-- disable obsolete plugins
|
||||||
|
for element in pairs(OBSOLETE_PLUGINS) do
|
||||||
|
plugins_disabled[element] = true
|
||||||
|
end
|
||||||
|
|
||||||
self.enabled_plugins = {}
|
local discovered = {}
|
||||||
self.disabled_plugins = {}
|
|
||||||
self.loaded_plugins = {}
|
|
||||||
local lookup_path_list = { DEFAULT_PLUGIN_PATH }
|
local lookup_path_list = { DEFAULT_PLUGIN_PATH }
|
||||||
local extra_paths = G_reader_settings:readSetting("extra_plugin_paths")
|
local extra_paths = G_reader_settings:readSetting("extra_plugin_paths")
|
||||||
if extra_paths then
|
if extra_paths then
|
||||||
@@ -106,21 +128,8 @@ function PluginLoader:loadPlugins()
|
|||||||
table.insert(lookup_path_list, extra_path)
|
table.insert(lookup_path_list, extra_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- keep reference to old value so they can be restored later
|
|
||||||
local package_path = package.path
|
|
||||||
local package_cpath = package.cpath
|
|
||||||
|
|
||||||
local plugins_disabled = G_reader_settings:readSetting("plugins_disabled")
|
|
||||||
if type(plugins_disabled) ~= "table" then
|
|
||||||
plugins_disabled = {}
|
|
||||||
end
|
|
||||||
-- disable obsolete plugins
|
|
||||||
for element in pairs(OBSOLETE_PLUGINS) do
|
|
||||||
plugins_disabled[element] = true
|
|
||||||
end
|
|
||||||
for _, lookup_path in ipairs(lookup_path_list) do
|
for _, lookup_path in ipairs(lookup_path_list) do
|
||||||
logger.info("Loading plugins from directory:", lookup_path)
|
logger.info("Looking for plugins in directory:", lookup_path)
|
||||||
for entry in lfs.dir(lookup_path) do
|
for entry in lfs.dir(lookup_path) do
|
||||||
local plugin_root = lookup_path.."/"..entry
|
local plugin_root = lookup_path.."/"..entry
|
||||||
local mode = lfs.attributes(plugin_root, "mode")
|
local mode = lfs.attributes(plugin_root, "mode")
|
||||||
@@ -128,9 +137,37 @@ function PluginLoader:loadPlugins()
|
|||||||
if mode == "directory" and entry:find(".+%.koplugin$") then
|
if mode == "directory" and entry:find(".+%.koplugin$") then
|
||||||
local mainfile = plugin_root.."/main.lua"
|
local mainfile = plugin_root.."/main.lua"
|
||||||
local metafile = plugin_root.."/_meta.lua"
|
local metafile = plugin_root.."/_meta.lua"
|
||||||
|
local disabled = false
|
||||||
if plugins_disabled and plugins_disabled[entry:sub(1, -10)] then
|
if plugins_disabled and plugins_disabled[entry:sub(1, -10)] then
|
||||||
mainfile = metafile
|
mainfile = metafile
|
||||||
|
disabled = true
|
||||||
end
|
end
|
||||||
|
local __, name = util.splitFilePathName(plugin_root)
|
||||||
|
|
||||||
|
table.insert(discovered, {
|
||||||
|
["main"] = mainfile,
|
||||||
|
["meta"] = metafile,
|
||||||
|
["path"] = plugin_root,
|
||||||
|
["disabled"] = disabled,
|
||||||
|
["name"] = name,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return discovered
|
||||||
|
end
|
||||||
|
|
||||||
|
function PluginLoader:_load(t)
|
||||||
|
-- keep reference to old value so they can be restored later
|
||||||
|
local package_path = package.path
|
||||||
|
local package_cpath = package.cpath
|
||||||
|
|
||||||
|
local mainfile, metafile, plugin_root, disabled
|
||||||
|
for _, v in ipairs(t) do
|
||||||
|
mainfile = v.main
|
||||||
|
metafile = v.meta
|
||||||
|
plugin_root = v.path
|
||||||
|
disabled = v.disabled
|
||||||
package.path = string.format("%s/?.lua;%s", plugin_root, package_path)
|
package.path = string.format("%s/?.lua;%s", plugin_root, package_path)
|
||||||
package.cpath = string.format("%s/lib/?.so;%s", plugin_root, package_cpath)
|
package.cpath = string.format("%s/lib/?.so;%s", plugin_root, package_cpath)
|
||||||
local ok, plugin_module = pcall(dofile, mainfile)
|
local ok, plugin_module = pcall(dofile, mainfile)
|
||||||
@@ -139,25 +176,37 @@ function PluginLoader:loadPlugins()
|
|||||||
elseif type(plugin_module.disabled) ~= "boolean" or not plugin_module.disabled then
|
elseif type(plugin_module.disabled) ~= "boolean" or not plugin_module.disabled then
|
||||||
plugin_module.path = plugin_root
|
plugin_module.path = plugin_root
|
||||||
plugin_module.name = plugin_module.name or plugin_root:match("/(.-)%.koplugin")
|
plugin_module.name = plugin_module.name or plugin_root:match("/(.-)%.koplugin")
|
||||||
if (plugins_disabled and plugins_disabled[entry:sub(1, -10)]) then
|
if disabled then
|
||||||
table.insert(self.disabled_plugins, plugin_module)
|
table.insert(self.disabled_plugins, plugin_module)
|
||||||
else
|
else
|
||||||
local ok_meta, plugin_metamodule = pcall(dofile, metafile)
|
local ok_meta, plugin_metamodule = pcall(dofile, metafile)
|
||||||
if ok_meta and plugin_metamodule then
|
if ok_meta and plugin_metamodule then
|
||||||
for k,v in pairs(plugin_metamodule) do plugin_module[k] = v end
|
for k, module in pairs(plugin_metamodule) do
|
||||||
|
plugin_module[k] = module
|
||||||
|
end
|
||||||
end
|
end
|
||||||
sandboxPluginEventHandlers(plugin_module)
|
sandboxPluginEventHandlers(plugin_module)
|
||||||
table.insert(self.enabled_plugins, plugin_module)
|
table.insert(self.enabled_plugins, plugin_module)
|
||||||
|
logger.dbg("Plugin loaded", plugin_module.name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
else
|
|
||||||
logger.dbg("Plugin", mainfile, "has been disabled.")
|
|
||||||
end
|
end
|
||||||
package.path = package_path
|
package.path = package_path
|
||||||
package.cpath = package_cpath
|
package.cpath = package_cpath
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function PluginLoader:loadPlugins()
|
||||||
|
if self.enabled_plugins then return self.enabled_plugins, self.disabled_plugins end
|
||||||
|
|
||||||
|
self.enabled_plugins = {}
|
||||||
|
self.disabled_plugins = {}
|
||||||
|
self.loaded_plugins = {}
|
||||||
|
|
||||||
|
local t = self:_discover()
|
||||||
|
table.sort(t, sortProvidersFirst)
|
||||||
|
self:_load(t)
|
||||||
-- set package path for all loaded plugins
|
-- set package path for all loaded plugins
|
||||||
for _, plugin in ipairs(self.enabled_plugins) do
|
for _, plugin in ipairs(self.enabled_plugins) do
|
||||||
package.path = string.format("%s;%s/?.lua", package.path, plugin.path)
|
package.path = string.format("%s;%s/?.lua", package.path, plugin.path)
|
||||||
|
|||||||
92
frontend/provider.lua
Normal file
92
frontend/provider.lua
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
--[[
|
||||||
|
|
||||||
|
Provider is a singleton that holds thirdparty implementations for features.
|
||||||
|
|
||||||
|
To be used on plugins, prefixed with "provider-", that implement specific feature APIs.
|
||||||
|
|
||||||
|
]]--
|
||||||
|
|
||||||
|
local util = require("util")
|
||||||
|
|
||||||
|
local function aTable(t)
|
||||||
|
if type(t) ~= "table" then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
|
||||||
|
local Provider = {
|
||||||
|
features = {
|
||||||
|
["exporter"] = {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function Provider:_isValidFeature(s)
|
||||||
|
return self.features[s] ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[--
|
||||||
|
Registers an implementation of a feature.
|
||||||
|
|
||||||
|
@param feature string that identifies the feature
|
||||||
|
@param name string that identifies the provider
|
||||||
|
@param impl table with implementation details
|
||||||
|
@treturn bool registered
|
||||||
|
]]
|
||||||
|
function Provider:register(feature, name, impl)
|
||||||
|
if type(name) ~= "string" or type(feature) ~= "string" or type(impl) ~= "table" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if self:_isValidFeature(feature) then
|
||||||
|
self.features[feature][name] = impl
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[--
|
||||||
|
Unregisters an implementation of a feature.
|
||||||
|
|
||||||
|
@param feature string feature identifier
|
||||||
|
@param name string provider identifier
|
||||||
|
@treturn bool unregistered
|
||||||
|
]]
|
||||||
|
function Provider:unregister(feature, name)
|
||||||
|
if type(name) ~= "string" or type(feature) ~= "string" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if self:_isValidFeature(feature) then
|
||||||
|
self.features[feature][name] = nil
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[--
|
||||||
|
Counts providers for a given feature
|
||||||
|
|
||||||
|
@param feature string feature identifier
|
||||||
|
@treturn int number
|
||||||
|
]]
|
||||||
|
function Provider:size(feature)
|
||||||
|
local count = 0
|
||||||
|
if self:_isValidFeature(feature) then
|
||||||
|
count = util.tableSize(aTable(self.features[feature]))
|
||||||
|
end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[--
|
||||||
|
Get providers for a given feature
|
||||||
|
|
||||||
|
@param feature string feature identifier
|
||||||
|
@treturn table provider/implementation k/v pairs.
|
||||||
|
]]
|
||||||
|
function Provider:getProvidersTable(feature)
|
||||||
|
if self:_isValidFeature(feature) then
|
||||||
|
return aTable(self.features[feature])
|
||||||
|
end
|
||||||
|
return aTable()
|
||||||
|
end
|
||||||
|
|
||||||
|
return Provider
|
||||||
@@ -31,6 +31,7 @@ local Dispatcher = require("dispatcher")
|
|||||||
local InfoMessage = require("ui/widget/infomessage")
|
local InfoMessage = require("ui/widget/infomessage")
|
||||||
local MyClipping = require("clip")
|
local MyClipping = require("clip")
|
||||||
local NetworkMgr = require("ui/network/manager")
|
local NetworkMgr = require("ui/network/manager")
|
||||||
|
local Provider = require("provider")
|
||||||
local ReaderHighlight = require("apps/reader/modules/readerhighlight")
|
local ReaderHighlight = require("apps/reader/modules/readerhighlight")
|
||||||
local UIManager = require("ui/uimanager")
|
local UIManager = require("ui/uimanager")
|
||||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||||
@@ -99,9 +100,7 @@ local function updateMyClippings(clippings, new_clippings)
|
|||||||
return clippings
|
return clippings
|
||||||
end
|
end
|
||||||
|
|
||||||
local Exporter = WidgetContainer:extend{
|
local targets = {
|
||||||
name = "exporter",
|
|
||||||
targets = {
|
|
||||||
html = require("target/html"),
|
html = require("target/html"),
|
||||||
joplin = require("target/joplin"),
|
joplin = require("target/joplin"),
|
||||||
json = require("target/json"),
|
json = require("target/json"),
|
||||||
@@ -111,15 +110,33 @@ local Exporter = WidgetContainer:extend{
|
|||||||
readwise = require("target/readwise"),
|
readwise = require("target/readwise"),
|
||||||
text = require("target/text"),
|
text = require("target/text"),
|
||||||
xmnote = require("target/xmnote"),
|
xmnote = require("target/xmnote"),
|
||||||
},
|
}
|
||||||
|
|
||||||
|
local function genExportersTable(path)
|
||||||
|
local t = {}
|
||||||
|
for k, v in pairs(targets) do
|
||||||
|
t[k] = v
|
||||||
|
end
|
||||||
|
if Provider:size("exporter") > 0 then
|
||||||
|
local tbl = Provider:getProvidersTable("exporter")
|
||||||
|
for k, v in pairs(tbl) do
|
||||||
|
t[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, v in pairs(t) do
|
||||||
|
v.path = path
|
||||||
|
end
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
|
||||||
|
local Exporter = WidgetContainer:extend{
|
||||||
|
name = "exporter",
|
||||||
}
|
}
|
||||||
|
|
||||||
function Exporter:init()
|
function Exporter:init()
|
||||||
migrateSettings()
|
migrateSettings()
|
||||||
self.parser = MyClipping:new{}
|
self.parser = MyClipping:new{}
|
||||||
for _, v in pairs(self.targets) do
|
self.targets = genExportersTable(self.path)
|
||||||
v.path = self.path
|
|
||||||
end
|
|
||||||
self.ui.menu:registerToMainMenu(self)
|
self.ui.menu:registerToMainMenu(self)
|
||||||
self:onDispatcherRegisterActions()
|
self:onDispatcherRegisterActions()
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
-- Set search path for `require()`.
|
-- Set search path for `require()`.
|
||||||
package.path =
|
package.path =
|
||||||
"common/?.lua;frontend/?.lua;" ..
|
"common/?.lua;frontend/?.lua;plugins/exporter.koplugin/?.lua;" ..
|
||||||
package.path
|
package.path
|
||||||
package.cpath =
|
package.cpath =
|
||||||
"common/?.so;common/?.dll;/usr/lib/lua/?.so;" ..
|
"common/?.so;common/?.dll;/usr/lib/lua/?.so;" ..
|
||||||
|
|||||||
41
spec/unit/provider_spec.lua
Normal file
41
spec/unit/provider_spec.lua
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
describe("Provider module", function()
|
||||||
|
local Provider
|
||||||
|
local t
|
||||||
|
|
||||||
|
setup(function()
|
||||||
|
require("commonrequire")
|
||||||
|
Provider = require("provider")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should fail to register an improper provider", function()
|
||||||
|
assert.is_false(Provider:register())
|
||||||
|
end)
|
||||||
|
it("should fail to unregister an improper provider", function()
|
||||||
|
assert.is_false(Provider:unregister())
|
||||||
|
end)
|
||||||
|
it("should register a proper provider with empty implementation", function()
|
||||||
|
assert.is_true(Provider:register("exporter", "test", {}))
|
||||||
|
end)
|
||||||
|
it("should override an implementation for the same name of the same kind", function()
|
||||||
|
assert.is_true(Provider:register("exporter", "test", { test = function() end }))
|
||||||
|
assert.is_true(type(Provider.features["exporter"]["test"].test) == "function")
|
||||||
|
end)
|
||||||
|
it("should unregister a provider", function()
|
||||||
|
assert.is_true(Provider:unregister("exporter", "test"))
|
||||||
|
end)
|
||||||
|
it("should count providers for a specific feature", function()
|
||||||
|
assert.is_true(Provider:register("exporter", "test1", {}))
|
||||||
|
assert.is_true(Provider:register("exporter", "test2", {}))
|
||||||
|
assert.is_true(Provider:register("exporter", "test3", {}))
|
||||||
|
assert.is_true(Provider:size("exporter") == 3)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should dump a table of providers for a specific feature", function()
|
||||||
|
assert.are.same(Provider.features["exporter"],
|
||||||
|
Provider:getProvidersTable("exporter"))
|
||||||
|
end)
|
||||||
|
it("should dump an empty table for an invalid feature", function()
|
||||||
|
t = Provider:getProvidersTable("invalid")
|
||||||
|
assert.is_true(type(t) == "table")
|
||||||
|
end)
|
||||||
|
end)
|
||||||
Reference in New Issue
Block a user