Standardize exposing methods inside extensions to the outside world
This commit is contained in:
parent
33cb9b3ba9
commit
82b84668b0
|
@ -1,6 +1,9 @@
|
||||||
master
|
master
|
||||||
===
|
===
|
||||||
|
|
||||||
|
* rename `app.add_to_instance` to `Extension.expose_to_application` for adding extension-local methods to the shared app instance.
|
||||||
|
* rename `app.add_to_config_context` to `Extension.expose_to_config` for adding extension-local methods to the sandboxed scope of `config.rb`
|
||||||
|
* Add `Extension.expose_to_templates`, which auto binds copies of extension-local methods into a Template context.
|
||||||
* Remove side-loading of CLI tasks from `tasks/`
|
* Remove side-loading of CLI tasks from `tasks/`
|
||||||
* Add the option of naming `config.rb` as `middleman.rb`.
|
* Add the option of naming `config.rb` as `middleman.rb`.
|
||||||
* Builder extracted from Thor. `after_build` hook now passes an instance of a Builder instead of the Thor CLI.
|
* Builder extracted from Thor. `after_build` hook now passes an instance of a Builder instead of the Thor CLI.
|
||||||
|
|
|
@ -305,14 +305,6 @@ module Middleman
|
||||||
config_context.execute_configure_callbacks(config[:mode])
|
config_context.execute_configure_callbacks(config[:mode])
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_to_instance(name, &func)
|
|
||||||
define_singleton_method(name, &func)
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_to_config_context(name, &func)
|
|
||||||
@config_context.define_singleton_method(name, &func)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Whether we're in server mode
|
# Whether we're in server mode
|
||||||
# @return [Boolean] If we're in dev mode
|
# @return [Boolean] If we're in dev mode
|
||||||
def server?
|
def server?
|
||||||
|
|
|
@ -18,6 +18,20 @@ module Middleman
|
||||||
|
|
||||||
attr_accessor :sitemap_collector, :data_collector, :leaves
|
attr_accessor :sitemap_collector, :data_collector, :leaves
|
||||||
|
|
||||||
|
# Expose `resources`, `data`, and `collection` to config.
|
||||||
|
expose_to_config resources: :sitemap_collector,
|
||||||
|
data: :data_collector,
|
||||||
|
collection: :register_collector
|
||||||
|
|
||||||
|
# Exposes `collection` to templates
|
||||||
|
expose_to_template collection: :collector_value
|
||||||
|
|
||||||
|
helpers do
|
||||||
|
def pagination
|
||||||
|
current_resource.data.pagination
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(app, options_hash={}, &block)
|
def initialize(app, options_hash={}, &block)
|
||||||
super
|
super
|
||||||
|
|
||||||
|
@ -29,13 +43,8 @@ module Middleman
|
||||||
@data_collector = LazyCollectorRoot.new(self)
|
@data_collector = LazyCollectorRoot.new(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
Contract Any
|
|
||||||
def before_configuration
|
def before_configuration
|
||||||
@leaves.clear
|
@leaves.clear
|
||||||
|
|
||||||
app.add_to_config_context :resources, &method(:sitemap_collector)
|
|
||||||
app.add_to_config_context :data, &method(:data_collector)
|
|
||||||
app.add_to_config_context :collection, &method(:register_collector)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Contract Symbol, LazyCollectorStep => Any
|
Contract Symbol, LazyCollectorStep => Any
|
||||||
|
@ -69,16 +78,6 @@ module Middleman
|
||||||
# Inject descriptors
|
# Inject descriptors
|
||||||
resources + ctx.descriptors.map { |d| d.to_resource(app) }
|
resources + ctx.descriptors.map { |d| d.to_resource(app) }
|
||||||
end
|
end
|
||||||
|
|
||||||
helpers do
|
|
||||||
def collection(label)
|
|
||||||
extensions[:collections].collector_value(label)
|
|
||||||
end
|
|
||||||
|
|
||||||
def pagination
|
|
||||||
current_resource.data.pagination
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,6 +9,12 @@ module Middleman
|
||||||
class Data < Extension
|
class Data < Extension
|
||||||
attr_reader :data_store
|
attr_reader :data_store
|
||||||
|
|
||||||
|
# Make the internal `data_store` method available as `app.data`
|
||||||
|
expose_to_application data: :data_store
|
||||||
|
|
||||||
|
# Exposes `data` to templates
|
||||||
|
expose_to_template data: :data_store
|
||||||
|
|
||||||
# The regex which tells Middleman which files are for data
|
# The regex which tells Middleman which files are for data
|
||||||
DATA_FILE_MATCHER = /^(.*?)[\w-]+\.(yml|yaml|json)$/
|
DATA_FILE_MATCHER = /^(.*?)[\w-]+\.(yml|yaml|json)$/
|
||||||
|
|
||||||
|
@ -18,8 +24,6 @@ module Middleman
|
||||||
@data_store = DataStore.new(app, DATA_FILE_MATCHER)
|
@data_store = DataStore.new(app, DATA_FILE_MATCHER)
|
||||||
app.config.define_setting :data_dir, 'data', 'The directory data files are stored in'
|
app.config.define_setting :data_dir, 'data', 'The directory data files are stored in'
|
||||||
|
|
||||||
app.add_to_instance(:data, &method(:data_store))
|
|
||||||
|
|
||||||
start_watching(app.config[:data_dir])
|
start_watching(app.config[:data_dir])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -42,12 +46,6 @@ module Middleman
|
||||||
@watcher.update_path(app.config[:data_dir])
|
@watcher.update_path(app.config[:data_dir])
|
||||||
end
|
end
|
||||||
|
|
||||||
helpers do
|
|
||||||
def data
|
|
||||||
extensions[:data].data_store
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# The core logic behind the data extension.
|
# The core logic behind the data extension.
|
||||||
class DataStore
|
class DataStore
|
||||||
include Contracts
|
include Contracts
|
||||||
|
|
|
@ -9,6 +9,12 @@ module Middleman
|
||||||
Contract IsA['Middleman::Sources']
|
Contract IsA['Middleman::Sources']
|
||||||
attr_reader :sources
|
attr_reader :sources
|
||||||
|
|
||||||
|
# Make the internal `sources` method available as `app.files`
|
||||||
|
expose_to_application files: :sources
|
||||||
|
|
||||||
|
# Make the internal `sources` method available in config as `files`
|
||||||
|
expose_to_config files: :sources
|
||||||
|
|
||||||
# The default list of ignores.
|
# The default list of ignores.
|
||||||
IGNORES = {
|
IGNORES = {
|
||||||
emacs_files: /(^|\/)\.?#/,
|
emacs_files: /(^|\/)\.?#/,
|
||||||
|
@ -34,10 +40,6 @@ module Middleman
|
||||||
|
|
||||||
# Watch current source.
|
# Watch current source.
|
||||||
start_watching(app.config[:source])
|
start_watching(app.config[:source])
|
||||||
|
|
||||||
# Expose API to app and config.
|
|
||||||
app.add_to_instance(:files, &method(:sources))
|
|
||||||
app.add_to_config_context(:files, &method(:sources))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Before we config, find initial files.
|
# Before we config, find initial files.
|
||||||
|
|
|
@ -7,6 +7,9 @@ class Middleman::CoreExtensions::Internationalization < ::Middleman::Extension
|
||||||
option :mount_at_root, nil, 'Mount a specific language at the root of the site'
|
option :mount_at_root, nil, 'Mount a specific language at the root of the site'
|
||||||
option :data, 'locales', 'The directory holding your locale configurations'
|
option :data, 'locales', 'The directory holding your locale configurations'
|
||||||
|
|
||||||
|
# Exposes `langs` to templates
|
||||||
|
expose_to_template :langs
|
||||||
|
|
||||||
def after_configuration
|
def after_configuration
|
||||||
# See https://github.com/svenfuchs/i18n/wiki/Fallbacks
|
# See https://github.com/svenfuchs/i18n/wiki/Fallbacks
|
||||||
unless options[:no_fallbacks]
|
unless options[:no_fallbacks]
|
||||||
|
@ -41,12 +44,6 @@ class Middleman::CoreExtensions::Internationalization < ::Middleman::Extension
|
||||||
::I18n.t(*args)
|
::I18n.t(*args)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Access the list of languages supported by this Middleman application
|
|
||||||
# @return [Array<Symbol>]
|
|
||||||
def langs
|
|
||||||
extensions[:i18n].langs
|
|
||||||
end
|
|
||||||
|
|
||||||
def locate_partial(partial_name, try_static=false)
|
def locate_partial(partial_name, try_static=false)
|
||||||
locals_dir = extensions[:i18n].options[:templates_dir]
|
locals_dir = extensions[:i18n].options[:templates_dir]
|
||||||
|
|
||||||
|
|
|
@ -6,16 +6,15 @@ module Middleman
|
||||||
# so it can add metadata to any pages generated by other extensions
|
# so it can add metadata to any pages generated by other extensions
|
||||||
self.resource_list_manipulator_priority = 80
|
self.resource_list_manipulator_priority = 80
|
||||||
|
|
||||||
|
# Expose the `page` method to config.
|
||||||
|
expose_to_config :page
|
||||||
|
|
||||||
def initialize(app, options_hash={}, &block)
|
def initialize(app, options_hash={}, &block)
|
||||||
super
|
super
|
||||||
|
|
||||||
@page_configs = Set.new
|
@page_configs = Set.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def before_configuration
|
|
||||||
app.add_to_config_context(:page, &method(:page))
|
|
||||||
end
|
|
||||||
|
|
||||||
# @return Array<Middleman::Sitemap::Resource>
|
# @return Array<Middleman::Sitemap::Resource>
|
||||||
Contract ResourceList => ResourceList
|
Contract ResourceList => ResourceList
|
||||||
def manipulate_resource_list(resources)
|
def manipulate_resource_list(resources)
|
||||||
|
|
|
@ -82,6 +82,24 @@ module Middleman
|
||||||
# @return [Array<Module>] a list of all the helper modules this extension provides. Set these using {#helpers}.
|
# @return [Array<Module>] a list of all the helper modules this extension provides. Set these using {#helpers}.
|
||||||
class_attribute :defined_helpers, instance_reader: false, instance_writer: false
|
class_attribute :defined_helpers, instance_reader: false, instance_writer: false
|
||||||
|
|
||||||
|
# @!attribute exposed_to_application
|
||||||
|
# @!scope class
|
||||||
|
# @api private
|
||||||
|
# @return [Hash<Symbol, Symbol>] a list of all the methods modules this extension exposes to app. Set these using {#expose_to_application}.
|
||||||
|
class_attribute :exposed_to_application, instance_reader: false, instance_writer: false
|
||||||
|
|
||||||
|
# @!attribute exposed_to_config
|
||||||
|
# @!scope class
|
||||||
|
# @api private
|
||||||
|
# @return [Hash<Symbol, Symbol>] a list of all the methods modules this extension exposes to config. Set these using {#expose_to_config}.
|
||||||
|
class_attribute :exposed_to_config, instance_reader: false, instance_writer: false
|
||||||
|
|
||||||
|
# @!attribute exposed_to_template
|
||||||
|
# @!scope class
|
||||||
|
# @api private
|
||||||
|
# @return [Hash<Symbol, Symbol>] a list of all the methods modules this extension exposes to templates. Set these using {#expose_to_template}.
|
||||||
|
class_attribute :exposed_to_template, instance_reader: false, instance_writer: false
|
||||||
|
|
||||||
# @!attribute ext_name
|
# @!attribute ext_name
|
||||||
# @!scope class
|
# @!scope class
|
||||||
# @return [Symbol] the name this extension is registered under. This is the symbol used to activate the extension.
|
# @return [Symbol] the name this extension is registered under. This is the symbol used to activate the extension.
|
||||||
|
@ -138,6 +156,68 @@ module Middleman
|
||||||
self.defined_helpers += modules
|
self.defined_helpers += modules
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Takes a method within this extension and exposes it globally
|
||||||
|
# on the main `app` instance. Used for very low-level extensions
|
||||||
|
# which many other extensions depend upon. Such as Data and
|
||||||
|
# File watching.
|
||||||
|
# @example with Hash:
|
||||||
|
# expose_to_application global_name: :local_name
|
||||||
|
# @example with Array:
|
||||||
|
# expose_to_application :method1, :method2
|
||||||
|
# @param [Array<Sumbol>, Hash<Symbol, Symbol>] symbols An optional list of symbols representing instance methods to exposed.
|
||||||
|
# @return [void]
|
||||||
|
def expose_to_application(*symbols)
|
||||||
|
self.exposed_to_application ||= {}
|
||||||
|
|
||||||
|
if symbols.first && symbols.first.is_a?(Hash)
|
||||||
|
self.exposed_to_application.merge!(symbols.first)
|
||||||
|
elsif symbols.is_a? Array
|
||||||
|
symbols.each do |sym|
|
||||||
|
self.exposed_to_application[sym] = sym
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Takes a method within this extension and exposes it inside the scope
|
||||||
|
# of the config.rb sandbox.
|
||||||
|
# @example with Hash:
|
||||||
|
# expose_to_config global_name: :local_name
|
||||||
|
# @example with Array:
|
||||||
|
# expose_to_config :method1, :method2
|
||||||
|
# @param [Array<Sumbol>, Hash<Symbol, Symbol>] symbols An optional list of symbols representing instance methods to exposed.
|
||||||
|
# @return [void]
|
||||||
|
def expose_to_config(*symbols)
|
||||||
|
self.exposed_to_config ||= {}
|
||||||
|
|
||||||
|
if symbols.first && symbols.first.is_a?(Hash)
|
||||||
|
self.exposed_to_config.merge!(symbols.first)
|
||||||
|
elsif symbols.is_a? Array
|
||||||
|
symbols.each do |sym|
|
||||||
|
self.exposed_to_config[sym] = sym
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Takes a method within this extension and exposes it inside the scope
|
||||||
|
# of the templating engine. Like `helpers`, but scoped.
|
||||||
|
# @example with Hash:
|
||||||
|
# expose_to_template global_name: :local_name
|
||||||
|
# @example with Array:
|
||||||
|
# expose_to_template :method1, :method2
|
||||||
|
# @param [Array<Sumbol>, Hash<Symbol, Symbol>] symbols An optional list of symbols representing instance methods to exposed.
|
||||||
|
# @return [void]
|
||||||
|
def expose_to_template(*symbols)
|
||||||
|
self.exposed_to_template ||= {}
|
||||||
|
|
||||||
|
if symbols.first && symbols.first.is_a?(Hash)
|
||||||
|
self.exposed_to_template.merge!(symbols.first)
|
||||||
|
elsif symbols.is_a? Array
|
||||||
|
symbols.each do |sym|
|
||||||
|
self.exposed_to_template[sym] = sym
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Reset all {Extension.after_extension_activated} callbacks.
|
# Reset all {Extension.after_extension_activated} callbacks.
|
||||||
# @api private
|
# @api private
|
||||||
# @return [void]
|
# @return [void]
|
||||||
|
@ -191,6 +271,7 @@ module Middleman
|
||||||
@_helpers = []
|
@_helpers = []
|
||||||
@app = app
|
@app = app
|
||||||
|
|
||||||
|
expose_methods
|
||||||
setup_options(options_hash, &block)
|
setup_options(options_hash, &block)
|
||||||
|
|
||||||
# Bind app hooks to local methods
|
# Bind app hooks to local methods
|
||||||
|
@ -227,8 +308,28 @@ module Middleman
|
||||||
# @param [Array<Sitemap::Resource>] resources A list of all the resources known to the sitemap.
|
# @param [Array<Sitemap::Resource>] resources A list of all the resources known to the sitemap.
|
||||||
# @return [Array<Sitemap::Resource>] The transformed list of resources.
|
# @return [Array<Sitemap::Resource>] The transformed list of resources.
|
||||||
|
|
||||||
|
def add_exposed_to_context(context)
|
||||||
|
(self.class.exposed_to_template || {}).each do |k, v|
|
||||||
|
context.define_singleton_method(k, &method(v))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def expose_methods
|
||||||
|
(self.class.exposed_to_application || {}).each do |k, v|
|
||||||
|
app.define_singleton_method(k, &method(v))
|
||||||
|
end
|
||||||
|
|
||||||
|
(self.class.exposed_to_config || {}).each do |k, v|
|
||||||
|
app.config_context.define_singleton_method(k, &method(v))
|
||||||
|
end
|
||||||
|
|
||||||
|
(self.class.defined_helpers || []).each do |m|
|
||||||
|
app.template_context_class.send(:include, m)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# @yield An optional block that can be used to customize options before the extension is activated.
|
# @yield An optional block that can be used to customize options before the extension is activated.
|
||||||
# @yieldparam Middleman::Configuration::ConfigurationManager] options Extension options
|
# @yieldparam Middleman::Configuration::ConfigurationManager] options Extension options
|
||||||
def setup_options(options_hash)
|
def setup_options(options_hash)
|
||||||
|
|
|
@ -68,13 +68,15 @@ module Middleman
|
||||||
end
|
end
|
||||||
|
|
||||||
instances.each do |ext|
|
instances.each do |ext|
|
||||||
# Forward Extension helpers to TemplateContext
|
|
||||||
Array(ext.class.defined_helpers).each do |m|
|
|
||||||
@app.template_context_class.send(:include, m)
|
|
||||||
end
|
|
||||||
|
|
||||||
::Middleman::Extension.activated_extension(ext)
|
::Middleman::Extension.activated_extension(ext)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_exposed_to_context(context)
|
||||||
|
@activated.each do |(_, ext)|
|
||||||
|
ext.add_exposed_to_context(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -48,6 +48,13 @@ module Middleman
|
||||||
# Add haml helpers to context
|
# Add haml helpers to context
|
||||||
::Middleman::TemplateContext.send :include, ::Haml::Helpers
|
::Middleman::TemplateContext.send :include, ::Haml::Helpers
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_exposed_to_context(context)
|
||||||
|
super
|
||||||
|
|
||||||
|
context.init_haml_helpers if context.respond_to?(:init_haml_helpers)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,12 +3,15 @@ module Middleman
|
||||||
module Extensions
|
module Extensions
|
||||||
# Class to handle managing ignores
|
# Class to handle managing ignores
|
||||||
class Ignores < Extension
|
class Ignores < Extension
|
||||||
|
# Expose `create_ignore` as `app.ignore`
|
||||||
|
expose_to_application ignore: :create_ignore
|
||||||
|
|
||||||
|
# Expose `create_ignore` to config as `ignore`
|
||||||
|
expose_to_config ignore: :create_ignore
|
||||||
|
|
||||||
def initialize(app, config={}, &block)
|
def initialize(app, config={}, &block)
|
||||||
super
|
super
|
||||||
|
|
||||||
@app.add_to_config_context(:ignore, &method(:create_ignore))
|
|
||||||
@app.define_singleton_method(:ignore, &method(:create_ignore))
|
|
||||||
|
|
||||||
# Array of callbacks which can ass ignored
|
# Array of callbacks which can ass ignored
|
||||||
@ignored_callbacks = Set.new
|
@ignored_callbacks = Set.new
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,15 @@ module Middleman
|
||||||
# Manages the list of proxy configurations and manipulates the sitemap
|
# Manages the list of proxy configurations and manipulates the sitemap
|
||||||
# to include new resources based on those configurations
|
# to include new resources based on those configurations
|
||||||
class Proxies < Extension
|
class Proxies < Extension
|
||||||
|
# Expose `create_proxy` as `app.proxy`
|
||||||
|
expose_to_application proxy: :create_proxy
|
||||||
|
|
||||||
|
# Expose `create_proxy` to config as `proxy`
|
||||||
|
expose_to_config proxy: :create_proxy
|
||||||
|
|
||||||
def initialize(app, config={}, &block)
|
def initialize(app, config={}, &block)
|
||||||
super
|
super
|
||||||
|
|
||||||
@app.add_to_config_context(:proxy, &method(:create_proxy))
|
|
||||||
@app.define_singleton_method(:proxy, &method(:create_proxy))
|
|
||||||
|
|
||||||
@proxy_configs = Set.new
|
@proxy_configs = Set.new
|
||||||
@post_config = false
|
@post_config = false
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,11 +7,13 @@ module Middleman
|
||||||
# Manages the list of proxy configurations and manipulates the sitemap
|
# Manages the list of proxy configurations and manipulates the sitemap
|
||||||
# to include new resources based on those configurations
|
# to include new resources based on those configurations
|
||||||
class Redirects < Extension
|
class Redirects < Extension
|
||||||
|
|
||||||
|
# Expose `create_redirect` to config as `redirect`
|
||||||
|
expose_to_config redirect: :create_redirect
|
||||||
|
|
||||||
def initialize(app, config={}, &block)
|
def initialize(app, config={}, &block)
|
||||||
super
|
super
|
||||||
|
|
||||||
@app.add_to_config_context(:redirect, &method(:create_redirect))
|
|
||||||
|
|
||||||
@redirects = {}
|
@redirects = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,15 @@ module Middleman
|
||||||
module Sitemap
|
module Sitemap
|
||||||
module Extensions
|
module Extensions
|
||||||
class RequestEndpoints < Extension
|
class RequestEndpoints < Extension
|
||||||
|
|
||||||
|
# Expose `create_endpoint` to config as `endpoint`
|
||||||
|
expose_to_config endpoint: :create_endpoint
|
||||||
|
|
||||||
# Manages the list of proxy configurations and manipulates the sitemap
|
# Manages the list of proxy configurations and manipulates the sitemap
|
||||||
# to include new resources based on those configurations
|
# to include new resources based on those configurations
|
||||||
def initialize(app, config={}, &block)
|
def initialize(app, config={}, &block)
|
||||||
super
|
super
|
||||||
|
|
||||||
@app.add_to_config_context(:endpoint, &method(:create_endpoint))
|
|
||||||
|
|
||||||
@endpoints = {}
|
@endpoints = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -58,8 +58,8 @@ module Middleman
|
||||||
# Sandboxed class for template eval
|
# Sandboxed class for template eval
|
||||||
context = @app.template_context_class.new(@app, locals, options)
|
context = @app.template_context_class.new(@app, locals, options)
|
||||||
|
|
||||||
# TODO: Only for HAML files
|
# Add extension helpers to context.
|
||||||
context.init_haml_helpers if context.respond_to?(:init_haml_helpers)
|
@app.extensions.add_exposed_to_context(context)
|
||||||
|
|
||||||
content = _render_with_all_renderers(path, locs, context, opts, &block)
|
content = _render_with_all_renderers(path, locs, context, opts, &block)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue