Replies: 1 comment
-
|
[I moved the previous content of this comment to https://github.com/spraakbanken/korp-backend/issues/14#issuecomment-2220687551 as it related to Korp backend plugins, not frontend ones, and I had added it here only mistakenly.] |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Preface
This issue tries to describe the plugin facility I have implemented for the Korp frontend used in the Language Bank of Finland and that I’d propose as the basis for a plugin facility to be included in main Korp frontend code. All feedback on the proposal is welcome.
Disclaimer: I have virtually no prior experience in plugin architectures, and also my knowledge of the Web technologies used in the Korp frontend – such as AngularJS, Webpack and Pug – is almost solely based on the Korp frontend and on what I have learned when modifying it, so I hope I’m not on a completely wrong track with this plugin facility. I also don’t know how future-proof this plugin approach is if or when Korp is eventually ported to Vue.js. The features of the plugin facility reflect the modifications we’ve made to Korp for the Language Bank of Finland, so something might be implemented in a too specific way or be completely missing. Thus, I’d be glad in particular if you pointed out if something in the plugin facility should definitely be done differently.
The code for the plugin facility is currently on top of Språkbanken’s Korp frontend code at commit 46374ad2 of 2022-02-02, but I’ll rebase it on top of the up-to-date code in the near future. (Mechanical Git rebasing won’t be enough; instead, the code will have to be modified by hand. Some plugins may have to be completely rewritten and some may become obsolete.)
The plugin facility is currently documented only in source code comments, so this issue description serves as documentation for the moment. I’ll eventually add some separate documentation, based on the comments and this issue.
I’m sorry that this is rather long for a GitHub issue description.
Korp frontend plugin facility (proposal)
Overview
The aim of the Korp frontend plugin facility is to make it easier to tailor Korp for different sites without having to modify main Korp code. To make this possible, the main Korp code needs some support for plugins and callback hook points in appropriate places in the JavaScript code.
In addition to JavaScript code, Korp frontend plugins may contain Pug code for content, (S)CSS stylesheets and JSON translation files.
Plugin locations
Plugins are searched from three places, in this order:
${configDir}/plugins/: configuration-specific plugins${pluginDir}/: separately distributed, general-purpose pluginsapp/plugins/: plugins possibly distributed along with the main Korp frontend codeconfigDiris the Korp frontend configuration directory specified inrun_config.json, andpluginDiris specified there similarly.pluginDircould typically be top_dir/app/plugins.Plugin structure
Each plugin should have its own subdirectory under a plugin directory. The subdirectory structure of each plugin should follow that of the Korp frontend code:
scripts/: JavaScript scriptsstyles/: (S)CSS stylesheetsincludes/: Pug files (snippets) to be includedtranslations/: JSON translation filesJavaScript plugins
Plugin objects and callbacks
The implementation of the JavaScript class
Pluginsfor plugins is inplugin.js. Plugins are managed through the globalwindow.pluginsobject.Korp frontend JavaScript plugins are objects containing function properties which correspond to plugin callback functions for named hook points. A plugin can be either a simple object or an object created from a prototype. Each plugin must be registered with
plugins.register(plugin).Callback functions in a plugin object may be mapped to hook points in two ways:
callbacks, the keys ofcallbacksare regarded as hook point names and their values are the callback functions in the plugin to be registered for the hook point. The values may be either single functions (function references), lists of them or strings (names of function properties in the plugin).callbacks, all function properties whose names do not begin or end with an underscore in the plugin and its direct prototype are added as callbacks, taking the function name as the name of the hook point.Calling callbacks
Callback functions for a hook point are called via
plugins.callActions(for functions not returning a value or whose return value is discarded) orplugins.callFilters(functions returning a value):callActions(hookPoint, ...args): Call plugin callback functions (actions) registered forhookPoint(a string), with the (optional) arguments...args. The possible return value of the functions is ignored.callFilters(hookPoint, arg1, ...rest): Call plugin callback functions (filters) registered forhookPoint, with the argumentarg1and optional...restEach callback function gets as itsarg1the return value of the previous function. This function returns the value returned by the last callback function.Callback call order
Plugin callbacks are called in the order in which the plugins have been registered. A rudimentary way of controlling the registration order is to specify arrays
providesFeaturesandrequiresFeaturesin the plugin objects: if a plugin has a value (string) inrequiresFeaturesthat has not yet appeared in theprovidesFeaturesof a registered plugin, registering it is deferred until a plugin providing the feature has been registered.Enabling and disabling plugins
If a plugin has the property
name(a string), it can be explicitly enabled or disabled by including the name in the arraysettings.pluginsEnabledorsettings.pluginsDisabled, respectively. Ifsettings.pluginsEnabledis defined, only the plugins listed in it are enabled.A plugin can also be disabled by setting its property
disabledtotruein the plugin class constructor. This can be used to disable a plugin if the configuration does not contain settings required by the plugin, for example.A plugin may contain method
initialize, which is called (without arguments) only for enabled plugins, unlikeconstructor, which is called at object creation time, regardless of whether the plugin is enabled or not.JavaScript plugin callback hook points
The following JavaScript plugin hook points are currently defined, but more can be added as needs arise, and some may become obsolete because of changes in Korp:
Actions (called via
plugins.callActions):beforeShowLogin: Act before actually showing the login modal, e.g., to handle a SSO login.finishLogout: Act on logout.onAuthRequestDone: Act based on the data returned by the authentication proxy, giving access to the scope.modifyHeaderController: Modify the header controller, giving access to the scopemodifySidebarContent: Modify sidebar content, giving access to the controller.beforeLoadApp: Act before loading the actual application, passing some data (currentlyjStorage) that may be used or modified.modifyLocation: Modify location URL at the very beginning.modifyCorpusConfigs: Modifysettings.corporaandsettings.corporafolders.modifyLocationOnDomReady: Modify location URL after the DOM is ready.onDomReady: Act when the DOM is ready.onCorpusChooserChange: Modify the corpus chooser.modifyCorpusConfigsList: Modifysettings.corpusListing.corpora.modifyCorpusFolderConfigs: Modifysettings.corpusfolders.Filters (called via
plugins.callFilters):filterLoginNeededFor: Filter (or otherwise act on) the list of the corpora for which login is needed.filterModalParams: Modify modal parameters.formatSidebarCorpusInfo: Format corpus information to be displayed in the sidebar, based on corpus object.filterLoginInfo: Filter login object, possibly modifying it based on the data returned by the authentication proxy.filterCorpusChooserSingleSelectedCorpusName: Filter the name of a single selected corpus, typically to remove HTML formatting or added information.formatPopupFolderTitle: Format the folder title shown in the corpus info popup and in the corpus chooser, based on folder configuration.formatPopupFolderInfo: Format the folder description shown in the popup, based on folder configuration.formatCorpusChooserCorpusTitle: Format the corpus title for the corpus chooser.formatPopupCorpusInfo: Format the corpus description shown in the corpus info popup, based on corpus configuration.formatPopupCorpusTitle: Format the corpus description shown in the popup, based on corpus configuration.AngularJS module in a plugin
A plugin can also create an AngularJS module by calling
registerAngularModule(name, ...args), which creates and returns an AngularJS module name with args and registers it to be added as a depencency to the Korp app. Plugins should typically useregisterAngularModuleinstead ofangular.module.registerAngularModuleshould be called from theinitializemethod of a plugin to allow enabling and disabling the plugin. This approach was inspired by https://stackoverflow.com/a/17944566Example
An example of a simple plugin for modifying the template (HTML) of the “About Korp” modal (original code):
Pug includes
To allow plugins to modify the HTML content of the Korp frontend, they may contain Pug snippets. To make that work, a Pug plugin is defined in
webpack.common.jsto extend theincludedirective to allow searching for include files in multiple directories and to allow ignoring non-existent includes.To enable the non-default include behaviour of Pug, the filename of the
includedirective needs to be prefixed with options between two colons, separated by commas; for example:include :search,optional:includes/file. The following options are recognized:search: Search all (plugin) paths for the file and include the first one found, unless the optionallis also specified.all: Include all the files found in the order of paths (only in conjunction withsearch).optional: No error if no file is found.The paths to be searched for includes are currently the following, in this order (
**denotes subdirectories recursively):${configDir}/${configDir}/plugins/**${pluginDir}/**app/plugins/**app/The possible directory in the
includedirective is included in the path. Moreover, if the extendedincludeis used in a file in a subdirectory, the subdirectory is included in the path from which to search includes. For example, if the fileapp/includes/header.pugcontainsinclude :search:main_menu, it is searched for in theincludessubdirectories of the paths listed above.Currently, the code uses the extended
includeto include two files inapp/includes/header.pug:top_header(optional, include all): An optional “top header” placed above all other elements; for example, a banner.main_menu: Allows overriding the main menu from a plugin or a site configuration.More uses of this feature are expected: for example, a plugin could add extra search options.
Stylesheets
Plugin stylesheets are loaded in
index.jsfrom.cssand.scssfiles in the following directories, in this order:app/plugins/**${pluginDir}/**${configDir}/plugins/**JSON translation files
Plugins also need to support JSON translation files, as they may contain items that should be localized or they may override default translations. The following locations are searched for
locale*.jsonfilesapp/translations/app/plugins/**/translations/${pluginDir}/**translations/${configDir}/translations/${configDir}/plugins/**/translations/The files for each locale are then merged, translations from later files overriding those from earlier ones.
Implemented plugins
The plugins currently used in the Korp of the Language Bank of Finland are available in the branch
plugins/masterof our Korp frontend fork. (This branch is a merge of individual plugin branches.) Please note that some of the plugins may be obsoleted by the move of corpus configurations to the backend, and most if not all plugins will need to be updated for the most recent version of Korp.The current plugins are the following:
about_modifier: Modify the template (HTML) of the “About Korp” modal.config_augment_info: Augment corpus (and folder) information based on site-configurable properties in the corpus (folder) configuration.config_corpus_features: Initialize properties insettings.corpora.*based on their propertyfeaturesand the properties insettings.corpusFeatures.config_corpusinfo_copier: Copy (extra) corpus information in theinfoproperty of a corpus configuration to top-level properties to propagate the information in a corpus folder to its corpora.config_corpus_settings_modifier: Allow corpus and corpus folder modifications to be defined in the configuration in functionssettings.modifyCorpusSettingsandsettings.modifyFolderSettings.config_licence_category: Add propertieslicenceTypeandlimitedAccessto corpus configurations based on licence name.config_logical_corpora: Add propertieslogicalCorpusandcorpusTypeto corpus configurations andinfo.folderTypeto corpus folder configurations.config_sidebar_order: Initialize theorderproperty of all attributes of all the corpora insettings.corporabased onsettings.defaultSidebarDisplayOrderand the corpus-specificsidebarDisplayOrderproperties.config_url_opts: Add apatternproperty for rendering a link to attribute configurations with the appropriateurlOptsproperty.corpus_aliases: Modify the location (parametercorpus), mapping possible corpus id aliases (defined insettings.corpusAliases) to actual corpus ids.corpuschooser_item_formatter: Allow formatting corpus and folder titles in the corpus chooser according to the type of corpus or folder by formatting functions defined insettings.formatCorpusChooserItem.corpuschooser_prompt_empty: Add a prompt (“Select corpora”) to the top frame of the corpus chooser if no corpora are selected.corpusinfo_formatter: Format (extra) corpus information in the corpus information popup and sidebar.news_banner: Implement an AngularJS module for a closeable news banner for important news (based on Korp’snewsdesk.js).shibboleth_auth: Implement authentication and authorization via Shibboleth.sidebar_link_section: Initialize thelinkAttributesproperty in corpus configurations and render it in the sidebar.Missing features (to-do)
This plugin proposal lacks many features, at least the following:
Allow specifying the order in which individual callbacks are called, as callback A of plugin X might need to be called before that of plugin Y, but callback B of plugin Y before that of plugin X.
When including the contents of all found Pug files at a certain point, sort them somehow. A way to do this could be to suffix file names with a double-digit number:
include :search,all:filewould includefile-10.pugandfile-20.pug, in this order. Another option could be to list names (or patterns) for paths in the order in which they should be considered as an argument to optionall; for example,include :search,all=plugin1+plugin2; non-matching ones would be included in an arbitrary order after the matching ones (or maybe optionally not included).Beta Was this translation helpful? Give feedback.
All reactions