asfindex plugin for site index (#26)

* asfindex content generation

* Settings for asfindex plugin / scope on index

* Abandon Menu Generation

While index generation is generalizable menu generation is not.
diff --git a/Dockerfile b/Dockerfile
index 3c180c1..e7cbe40 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -78,7 +78,7 @@
 COPY --from=pelican-asf /tmp/pelican-asf .
 
 COPY requirements.txt .
-RUN pip install -r requirements.txt --no-deps
+RUN pip install -r requirements.txt
 
 # If the site needs authtokens to build, copy them into the file .authtokens
 # and it will be picked up at build time
diff --git a/bin/buildsite.py b/bin/buildsite.py
index b6c1063..8f416c3 100755
--- a/bin/buildsite.py
+++ b/bin/buildsite.py
@@ -246,6 +246,10 @@
         if 'use' in ydata['plugins']:
             tdata['use'] = ydata['plugins']['use']
 
+    tdata['uses_index'] = None
+    if 'index' in tdata:
+        tdata['uses_index'] = 'yes'  # ezt.boolean
+
     if 'genid' in ydata:
         class GenIdParams:
             def setbool(self, name):
diff --git a/bin/pelican.auto.ezt b/bin/pelican.auto.ezt
index abc1b1c..74c0d4f 100644
--- a/bin/pelican.auto.ezt
+++ b/bin/pelican.auto.ezt
@@ -91,3 +91,9 @@
 [if-any uses_copy]
 ASF_COPY = [ [for copy]'[copy]', [end] ]
 [end]
+[if-any uses_index]
+# Configure the asfindex plugin
+ASF_INDEX = {
+    'index': '[index]',
+}
+[end]
diff --git a/plugins/asfindex.py b/plugins/asfindex.py
new file mode 100644
index 0000000..267e8d7
--- /dev/null
+++ b/plugins/asfindex.py
@@ -0,0 +1,151 @@
+#!/usr/bin/python -B
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+#
+# asfindex.py - Pelican plugin that runs shell scripts during initialization
+#
+
+import sys
+import subprocess
+import shlex
+import io
+import os
+import os.path
+import traceback
+
+import pelican.plugins.signals
+import pelican.settings
+from pelican.contents import Article, Page, Static
+from pelican.generators import (ArticlesGenerator,  # noqa: I100
+                                PagesGenerator, SourceFileGenerator,
+                                StaticGenerator, TemplatePagesGenerator)
+
+
+# get setting
+#  Settings are for the whole pelican environment.
+def get_setting(generators, setting):
+    try:
+        for g in generators:
+            if isinstance(g, PagesGenerator):
+                return g.settings[setting]
+    except Exception:
+        return None
+
+
+# set context
+#  Context are the processed settings and other environment which is made available to the JINJA template.
+#  Changes to the settings have no effect as those are already copied to each generator's context.
+def set_context(generators, setting, value):
+    for g in generators:
+        if isinstance(g, PagesGenerator):
+            g.context[setting] = value
+            return value
+    return None
+
+
+# get pages
+#  The PagesGenerator has a list of pages. Retrieve a sorted array of page information
+def get_pages(generators):
+    site_index = []
+    for g in generators:
+        if isinstance(g, PagesGenerator):
+            for p in g.pages:
+                # use an absolute path 
+                save_as = '/' + p.save_as
+                if save_as.endswith('/index.html'):
+                    # use "/" for the filename of index.html files assuring that they are first in a folder's list
+                    save_as = save_as[:-10]
+                # extract the path name
+                path, page = os.path.split(save_as)
+                site_index.append((path, save_as, p.title))
+    site_index.sort()
+    return site_index
+
+
+# get site index
+def get_index(site_index, scope):
+    current_folder = None
+    started = False
+    site_listing = ''
+    if not scope:
+        return
+    scoped = False
+    if scope != '**':
+        scoped = True
+    for p in site_index:
+        path, page = os.path.split(p[0])
+        folder = page.capitalize()
+        if not scoped or (scoped and p[0].startswith(scope)):
+            if folder != current_folder:
+                if started:
+                    site_listing += '</ol>\n'
+                started = True
+                site_listing += f'<h3><a href="{p[1]}">{p[2]}</a></h3>\n'
+                site_listing += '<ol>\n'
+                current_folder = folder
+            else:
+                # menu item for page
+                site_listing += f'<li><a href="{p[1]}">{p[2]}</a></li>\n'
+    if started:
+        site_listing += '</ol>\n'
+    return site_listing
+
+
+# get site menu
+# def get_menu(site_index, menus):
+#     currrent_menu = None
+#     site_menu = ''
+#     if menus:
+#         for f in menus:
+#             path, page = os.path.split(f)
+#             folder = page.capitalize()
+#             site_menu += '<li class="nav-item active dropdown">\n'
+#             site_menu += f'<a class="nav-link dropdown-toggle" href="#" id="dropdown{folder}" '
+#             site_menu += f'role="button" data-toggle="dropdown" aria-expanded="false">{folder}</a>\n'
+#             site_menu += f'<ul class="dropdown-menu" aria-labelledby="dropdown{folder}">\n'
+#             for p in site_index:
+#                 if p[0] == f:
+#                     # menu item for page
+#                     site_menu += f'<li><a class="dropdownitem" href="{p[1]}">{p[2]}</a></li>\n'
+#             site_menu += '</ul></li>\n'
+#     return site_menu
+#
+#
+# show pages
+def show_pages(generators):
+    site_index = get_pages(generators)
+    asf_index = get_setting(generators, 'ASF_INDEX')
+    print(asf_index)
+    # Not currently interested in menus this way as it is not generalizable
+    # set_context(generators, 'SITE_MENU', get_menu(site_index, asf_index['menus']))
+    set_context(generators, 'SITE_INDEX', get_index(site_index, asf_index['index']))
+
+
+def tb_finalized(generators):
+    """ Print any exception, before Pelican chews it into nothingness."""
+    try:
+        show_pages(generators)
+    except Exception:
+        print('-----', file=sys.stderr)
+        traceback.print_exc()
+        # exceptions here stop the build
+        raise
+
+
+def register():
+    pelican.plugins.signals.all_generators_finalized.connect(tb_finalized)