Process the ``task_topic_list`` node to generate a rendered listing of
    Task, Configurable, or Config topics (as determined by the types
    key of the ``task_topic_list`` node).

    This is called during the "doctree-resolved" phase so that the
    ``lsst_task_topcs`` environment attribute is fully set.

def process_task_topic_list(app, doctree, fromdocname):
    """Process the ``task_topic_list`` node to generate a rendered listing of
    Task, Configurable, or Config topics (as determined by the types
    key of the ``task_topic_list`` node).

    This is called during the "doctree-resolved" phase so that the
    ``lsst_task_topcs`` environment attribute is fully set.
    """
    logger = getLogger(__name__)
    logger.debug('Started process_task_list')

    env = app.builder.env

    for node in doctree.traverse(task_topic_list):
        try:
            topics = env.lsst_task_topics
        except AttributeError:
            message = (
                "Environment does not have 'lsst_task_topics', "
                "can't process the listing."
            )
            logger.warning(message)
            node.replace_self(nodes.paragraph(text=message))
            continue

        root = node['root_namespace']

        # Sort tasks by the topic's class name.
        # NOTE: if the presentation of the link is changed to the fully
        # qualified name, with full Python namespace, then the topic_names
        # should be changed to match that.
        topic_keys = [k for k, topic in topics.items()
                      if topic['type'] in node['types']
                      if topic['fully_qualified_name'].startswith(root)]
        topic_names = [topics[k]['fully_qualified_name'].split('.')[-1]
                       for k in topic_keys]
        topic_keys = [
            k for k, _ in
            sorted(zip(topic_keys, topic_names), key=lambda pair: pair[1])]

        if len(topic_keys) == 0:
            # Fallback if no topics are found
            p = nodes.paragraph(text='No topics.')
            node.replace_self(p)
            continue

        dl = nodes.definition_list()
        for key in topic_keys:
            topic = topics[key]
            class_name = topic['fully_qualified_name'].split('.')[-1]
            summary_text = topic['summary_node'][0].astext()

            # Each topic in the listing is a definition list item. The term is
            # the linked class name and the description is the summary
            # sentence from the docstring _or_ the content of the
            # topic directive
            dl_item = nodes.definition_list_item()

            # Can insert an actual reference since the doctree is resolved.
            ref_node = nodes.reference('', '')
            ref_node['refdocname'] = topic['docname']
            ref_node['refuri'] = app.builder.get_relative_uri(
                fromdocname, topic['docname'])
            # NOTE: Not appending an anchor to the URI because task topics
            # are designed to occupy an entire page.
            link_label = nodes.Text(class_name, class_name)
            ref_node += link_label

            term = nodes.term()
            term += ref_node
            dl_item += term

            # We're degrading the summary to plain text to avoid syntax issues
            # and also because it may be distracting
            def_node = nodes.definition()
            def_node += nodes.paragraph(text=summary_text)
            dl_item += def_node
            dl += dl_item

        # Replace the task_list node (a placeholder) with this renderable
        # content
        node.replace_self(dl)