diff --git a/frontend/pluginloader.lua b/frontend/pluginloader.lua index 6db12dc4f..68102ee94 100644 --- a/frontend/pluginloader.lua +++ b/frontend/pluginloader.lua @@ -1,5 +1,6 @@ local lfs = require("libs/libkoreader-lfs") local logger = require("logger") +local util = require("util") local _ = require("gettext") local DEFAULT_PLUGIN_PATH = "plugins" @@ -22,6 +23,22 @@ local DEPRECATION_MESSAGES = { 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 s if type(field) == "table" then @@ -76,12 +93,17 @@ local PluginLoader = { all_plugins = nil, } -function PluginLoader:loadPlugins() - if self.enabled_plugins then return self.enabled_plugins, self.disabled_plugins end +function PluginLoader:_discover() + 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 = {} - self.disabled_plugins = {} - self.loaded_plugins = {} + local discovered = {} local lookup_path_list = { DEFAULT_PLUGIN_PATH } local extra_paths = G_reader_settings:readSetting("extra_plugin_paths") if extra_paths then @@ -106,21 +128,8 @@ function PluginLoader:loadPlugins() table.insert(lookup_path_list, extra_path) 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 - 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 local plugin_root = lookup_path.."/"..entry local mode = lfs.attributes(plugin_root, "mode") @@ -128,36 +137,76 @@ function PluginLoader:loadPlugins() if mode == "directory" and entry:find(".+%.koplugin$") then local mainfile = plugin_root.."/main.lua" local metafile = plugin_root.."/_meta.lua" + local disabled = false if plugins_disabled and plugins_disabled[entry:sub(1, -10)] then mainfile = metafile + disabled = true end - package.path = string.format("%s/?.lua;%s", plugin_root, package_path) - package.cpath = string.format("%s/lib/?.so;%s", plugin_root, package_cpath) - local ok, plugin_module = pcall(dofile, mainfile) - if not ok or not plugin_module then - logger.warn("Error when loading", mainfile, plugin_module) - elseif type(plugin_module.disabled) ~= "boolean" or not plugin_module.disabled then - plugin_module.path = plugin_root - plugin_module.name = plugin_module.name or plugin_root:match("/(.-)%.koplugin") - if (plugins_disabled and plugins_disabled[entry:sub(1, -10)]) then - table.insert(self.disabled_plugins, plugin_module) - else - local ok_meta, plugin_metamodule = pcall(dofile, metafile) - if ok_meta and plugin_metamodule then - for k,v in pairs(plugin_metamodule) do plugin_module[k] = v end - end - sandboxPluginEventHandlers(plugin_module) - table.insert(self.enabled_plugins, plugin_module) - end - else - logger.dbg("Plugin", mainfile, "has been disabled.") - end - package.path = package_path - package.cpath = package_cpath + 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.cpath = string.format("%s/lib/?.so;%s", plugin_root, package_cpath) + local ok, plugin_module = pcall(dofile, mainfile) + if not ok or not plugin_module then + logger.warn("Error when loading", mainfile, plugin_module) + elseif type(plugin_module.disabled) ~= "boolean" or not plugin_module.disabled then + plugin_module.path = plugin_root + plugin_module.name = plugin_module.name or plugin_root:match("/(.-)%.koplugin") + if disabled then + table.insert(self.disabled_plugins, plugin_module) + else + local ok_meta, plugin_metamodule = pcall(dofile, metafile) + if ok_meta and plugin_metamodule then + for k, module in pairs(plugin_metamodule) do + plugin_module[k] = module + end + end + sandboxPluginEventHandlers(plugin_module) + table.insert(self.enabled_plugins, plugin_module) + logger.dbg("Plugin loaded", plugin_module.name) + end + end + end + package.path = package_path + package.cpath = package_cpath + +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 for _, plugin in ipairs(self.enabled_plugins) do package.path = string.format("%s;%s/?.lua", package.path, plugin.path) diff --git a/frontend/provider.lua b/frontend/provider.lua new file mode 100644 index 000000000..df4297942 --- /dev/null +++ b/frontend/provider.lua @@ -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 diff --git a/plugins/exporter.koplugin/main.lua b/plugins/exporter.koplugin/main.lua index 65805f1df..81a6284f6 100644 --- a/plugins/exporter.koplugin/main.lua +++ b/plugins/exporter.koplugin/main.lua @@ -31,6 +31,7 @@ local Dispatcher = require("dispatcher") local InfoMessage = require("ui/widget/infomessage") local MyClipping = require("clip") local NetworkMgr = require("ui/network/manager") +local Provider = require("provider") local ReaderHighlight = require("apps/reader/modules/readerhighlight") local UIManager = require("ui/uimanager") local WidgetContainer = require("ui/widget/container/widgetcontainer") @@ -99,27 +100,43 @@ local function updateMyClippings(clippings, new_clippings) return clippings end +local targets = { + html = require("target/html"), + joplin = require("target/joplin"), + json = require("target/json"), + markdown = require("target/markdown"), + my_clippings = require("target/my_clippings"), + nextcloud = require("target/nextcloud"), + readwise = require("target/readwise"), + text = require("target/text"), + 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", - targets = { - html = require("target/html"), - joplin = require("target/joplin"), - json = require("target/json"), - markdown = require("target/markdown"), - my_clippings = require("target/my_clippings"), - nextcloud = require("target/nextcloud"), - readwise = require("target/readwise"), - text = require("target/text"), - xmnote = require("target/xmnote"), - }, } function Exporter:init() migrateSettings() self.parser = MyClipping:new{} - for _, v in pairs(self.targets) do - v.path = self.path - end + self.targets = genExportersTable(self.path) self.ui.menu:registerToMainMenu(self) self:onDispatcherRegisterActions() end diff --git a/setupkoenv.lua b/setupkoenv.lua index ff1feb8c7..2c92f97db 100644 --- a/setupkoenv.lua +++ b/setupkoenv.lua @@ -1,6 +1,6 @@ -- Set search path for `require()`. package.path = - "common/?.lua;frontend/?.lua;" .. + "common/?.lua;frontend/?.lua;plugins/exporter.koplugin/?.lua;" .. package.path package.cpath = "common/?.so;common/?.dll;/usr/lib/lua/?.so;" .. diff --git a/spec/unit/provider_spec.lua b/spec/unit/provider_spec.lua new file mode 100644 index 000000000..7e3d9ae2d --- /dev/null +++ b/spec/unit/provider_spec.lua @@ -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)