Make hint foundries configurable

Resolves #173

and the milestone from, originally, 1 Apr 2025

Change-Id: Iaa062ae4831187c96f99f339b29cc4374cc31afd
diff --git a/README.md b/README.md
index 5c36b25..f1372ea 100644
--- a/README.md
+++ b/README.md
@@ -192,20 +192,56 @@
 Currently the JavaScript translations are separated and stored in `dev/js/src/loc`.
 To generate assets relying on different locales, add the locale to `Gruntfile.js`.
 
-To localize the annotation helper according to a special corpus environment,
-different annotation foundries can be loaded in `kalamar.conf.js`.
-For example to support `marmot` and `malt`,
-the configuration may look like this:
+#### Annotation Helper Foundries
 
-```js
-require([
-  "hint/foundries/marmot",
-  "hint/foundries/malt"
-]);
+![Experimental](https://img.shields.io/badge/status-experimental-orange)
+
+The annotation helper hints can be configured at runtime via configuration
+file or environment variable. The default foundries are: `base`, `corenlp`,
+`dereko`, `malt`, `marmot`, `opennlp`, `spacy`, `treetagger`. Additionally available is `ud` (udpipe).
+
+To **exclude** specific foundries from the defaults, prefix them with `-`:
+
+```perl
+{
+  Kalamar => {
+    hint_foundries => ['-spacy', '-corenlp']  # Removes spacy and corenlp
+  }
+}
 ```
 
-See `dev/js/src/hint/foundries` for
-more optional foundries.
+```shell
+KALAMAR_HINT_FOUNDRIES=-spacy,-corenlp perl script/kalamar daemon
+```
+
+To **replace** the default list entirely, specify foundries without `-` prefix:
+
+```perl
+{
+  Kalamar => {
+    hint_foundries => ['base', 'marmot', 'malt']  # Only these three
+  }
+}
+```
+
+For Docker deployments:
+
+```shell
+docker run -e KALAMAR_HINT_FOUNDRIES=-spacy korap/kalamar
+```
+
+Or mount a custom configuration file:
+
+```shell
+docker run -v /path/to/my.conf:/kalamar/kalamar.production.conf korap/kalamar
+```
+
+See `dev/js/src/hint/foundries` for available foundries.
+
+
+> **Note:** For build-time customization, `kalamar.conf.js` can still
+> be used to control which foundries are bundled into the JavaScript assets.
+
 
 ### Customization
 
diff --git a/dev/js/src/default.js b/dev/js/src/default.js
index 344feac..39d211e 100644
--- a/dev/js/src/default.js
+++ b/dev/js/src/default.js
@@ -1,5 +1,15 @@
+/**
+ * Default foundries to include in the Kalamar JavaScript bundle.
+ * The actual foundries shown at runtime are controlled by the
+ * hint_foundries configuration or KALAMAR_HINT_FOUNDRIES env var.
+ */
 require([
   "hint/foundries/base",
-  "hint/foundries/ud",
-  "hint/foundries/dereko"
+  "hint/foundries/corenlp",
+  "hint/foundries/dereko",
+  "hint/foundries/malt",
+  "hint/foundries/marmot",
+  "hint/foundries/opennlp",
+  "hint/foundries/spacy",
+  "hint/foundries/treetagger"
 ]);
diff --git a/dev/js/src/hint.js b/dev/js/src/hint.js
index de9c09b..bcdd6a6 100644
--- a/dev/js/src/hint.js
+++ b/dev/js/src/hint.js
@@ -64,6 +64,11 @@
         console.log("No annotationhelper defined");
         return;
       };
+
+      // Apply configured foundry filter from data-hint-foundries attribute
+      if (KorAP.annotationHelper.filterByConfig) {
+        KorAP.annotationHelper.filterByConfig();
+      };
       
       /**
        * @define {regex} Regular expression for context
diff --git a/dev/js/src/hint/foundries.js b/dev/js/src/hint/foundries.js
index 80d7332..053954e 100644
--- a/dev/js/src/hint/foundries.js
+++ b/dev/js/src/hint/foundries.js
@@ -80,5 +80,32 @@
     return '';
   };
 
+  /**
+   * Filter available foundries based on configuration.
+   * Reads from data-hint-foundries attribute on body element.
+   * Each foundry module pushes entries like ["Name", "prefix/", "Description"]
+   * to ah["-"]. The prefix (e.g., "corenlp/") is matched against enabled list.
+   */
+  ah.filterByConfig = function () {
+    const body = document.body;
+    if (!body) return;
+    
+    const configAttr = body.getAttribute('data-hint-foundries');
+    if (!configAttr) return; // No filter - show all
+    
+    const enabledFoundries = configAttr.split(',').map(f => f.trim().toLowerCase());
+    if (enabledFoundries.length === 0) return;
+
+    // Filter the root foundry list ah["-"]
+    // Each entry is ["Name", "prefix/", "Description"]
+    // Match prefix (without trailing /) against enabled list
+    this["-"] = this["-"].filter(entry => {
+      if (!entry || !entry[1]) return false;
+      // Extract foundry name from prefix like "corenlp/" -> "corenlp"
+      const foundryName = entry[1].replace(/\/$/, '').toLowerCase();
+      return enabledFoundries.includes(foundryName);
+    });
+  };
+
   return ah;
 });
diff --git a/kalamar.conf b/kalamar.conf
index e3aa00b..4cffecb 100644
--- a/kalamar.conf
+++ b/kalamar.conf
@@ -77,6 +77,13 @@
     #   root_path => '/plugin/export',
     #   mount => 'http://export-plugin:3333',
     #   service => 'export-plugin-proxy'
-    # }]
+    # }],
+
+    ## Annotation helper foundries
+    ## Controls which foundries are shown in the query hint menu.
+    ## Can also be set via KALAMAR_HINT_FOUNDRIES environment variable.
+    ## Use '-foundry' to exclude from defaults, e.g. ['-spacy', '-corenlp']
+    # hint_foundries => ['base', 'corenlp', 'dereko', 'malt',
+    #                    'marmot', 'opennlp', 'spacy', 'treetagger']
   }
 }
diff --git a/lib/Kalamar.pm b/lib/Kalamar.pm
index 9944e1b..d300aca 100644
--- a/lib/Kalamar.pm
+++ b/lib/Kalamar.pm
@@ -338,6 +338,42 @@
     };
   };
 
+  # Configure hint foundries for annotation layer helper hints
+  # Can be overridden via KALAMAR_HINT_FOUNDRIES environment variable (comma-separated)
+  # or via hint_foundries config option (array)
+  # Items starting with '-' exclude that foundry from defaults (e.g., '-spacy')
+  # If only exclusions are specified, they're removed from defaults
+  # If any positive items exist, they replace the defaults entirely
+  my @default_foundries = qw(base corenlp dereko malt marmot opennlp spacy treetagger);
+  my $hint_foundries;
+  my $config_foundries;
+  
+  if ($ENV{'KALAMAR_HINT_FOUNDRIES'}) {
+    $config_foundries = [split(/\s*,\s*/, $ENV{'KALAMAR_HINT_FOUNDRIES'})];
+  } elsif (exists $conf->{hint_foundries}) {
+    $config_foundries = $conf->{hint_foundries};
+  };
+
+  if ($config_foundries) {
+    # Separate exclusions (starting with '-') from inclusions
+    my @exclusions = map { substr($_, 1) } grep { /^-/ } @$config_foundries;
+    my @inclusions = grep { !/^-/ } @$config_foundries;
+
+    if (@inclusions) {
+      # If there are any positive items, use them as the full list
+      $hint_foundries = \@inclusions;
+    } elsif (@exclusions) {
+      # If only exclusions, remove them from defaults
+      my %exclude = map { lc($_) => 1 } @exclusions;
+      $hint_foundries = [grep { !$exclude{lc($_)} } @default_foundries];
+    } else {
+      $hint_foundries = \@default_foundries;
+    };
+  } else {
+    $hint_foundries = \@default_foundries;
+  };
+  $self->defaults(hint_foundries => $hint_foundries);
+
   # Configure documentation navigation
   my $doc_navi = Mojo::File->new($self->home->child('templates','doc','navigation.json'))->slurp;
   $doc_navi = $doc_navi ? decode_json($doc_navi) : [];
diff --git a/templates/layouts/main.html.ep b/templates/layouts/main.html.ep
index 062a6b3..749dec3 100644
--- a/templates/layouts/main.html.ep
+++ b/templates/layouts/main.html.ep
@@ -58,6 +58,7 @@
         % my $api = url_for('index');
         % $api =~ s!/$!!;
         data-korap-url="<%== $api %>"
+        data-hint-foundries="<%= join(',', @{stash('hint_foundries') // []}) %>"
         itemscope
         itemtype="http://schema.org/<%= stash('schematype') || 'WebApplication' %>">