blob: 65d631d9cdc3a65492ed7c2ab49825b93766b9bf [file] [log] [blame]
#!/usr/bin/env python3.8
# Copyright 2021 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file
"""Generate _toc.yaml for a given dart doc directory index.json.
This script takes in an index.json representing a dartdoc
directory and creates a _toc.yaml.
"""
import argparse
import collections
import json
import os
import sys
import yaml
# Dart types that can contain members.
ENCLOSING_TYPES = ["library", "class", "enum", "mixin", "extension"]
TYPE_HEADINGS = {
"class": "Classes",
"enum": "Enums",
"extension": "Extensions",
"mixin": "Mixins",
"constant": "Constants",
"constructor": "Constructors",
"function": "Functions",
"method": "Methods",
"property": "Properties",
"top-level constant": "Top-level Constants",
"top-level property": "Top-level Properties",
"typedef": "Typedefs",
}
def configure_yaml():
# TODO: (https://fxbug.dev/83891)
# Reevaluate whether hack described below is required moving forward.
# yaml.dump by default sorts keys alphabetically, but most of the time we
# need a specific order. One workaround is by passing sort_keys=False, but
# there is another problem: Python itself prior to 3.7 does not guarantee
# dict key order. Therefore we use collections.OrderedDict, and we set up a
# yaml representer to emit keys & values without the
# "!!python/object/apply:collections.OrderedDict" tag.
def represent_dictionary_order(dumper, data):
return dumper.represent_mapping("tag:yaml.org,2002:map", data.items())
yaml.add_representer(collections.OrderedDict, represent_dictionary_order)
def build_toc_item(title, path=None, sub_items=None):
"""Create an item in the TOC.
Args:
title (str) - Title for this item.
path (str) - Path to the page for this item.
sub_items (list) - Items to place in a collapsible subsection.
"""
item = collections.OrderedDict()
item["title"] = title
if path:
item["path"] = path
if sub_items:
item["section"] = sub_items
return item
def build_toc_content(index_file):
"""Build a TOC hierarchy from a json index produced by dart doc.
Args:
index_file (str) - JSON file which represents dartdoc file.
"""
with open(index_file) as f:
data = json.load(f)
libraries = treeify_index(data)
toc_items = [build_toc_item("Overview", path="/reference/dart/index.md")]
if libraries:
if "packageName" in libraries[0]:
# Organize by packageName, then alphabetically
libraries.sort(key=lambda lib: (lib["packageName"], lib["name"]))
current_package = None
for lib in libraries:
# Insert a heading when we encounter a new packageName.
if current_package != lib["packageName"]:
current_package = lib["packageName"]
toc_items.append({"heading": current_package})
toc_items.append(element_to_toc_item(lib))
else:
# Older versions of Dartdoc do not provide packageName in the index.
# TODO: (https://fxbug.dev/83893)
# Determine if this else branch is still needed for older dartdoc versions.
toc_items.append({"heading": "Libraries"})
toc_items.extend(element_to_toc_item(lib) for lib in libraries)
return {"toc": toc_items}
def treeify_index(data):
"""Convert the index data to a tree and return a list of libraries.
Index data is a flat list where each element identifies its enclosing
element, if applicable. Libraries are not enclosed by anything and are
effectively the root(s) of the tree. Other elements are added to a list
named 'members' in their enclosing element. All other data is unchanged.
Args:
data (list) - list of libraries with possible nested elements.
"""
assert type(data) is list
libraries = []
lookup = {}
for element in data:
if element["type"] == "library":
libraries.append(element)
if element["type"] in ENCLOSING_TYPES:
# In some cases qualifiedName is not unique so we use packageName too.
lookup_key = "%s-%s" % (
element["qualifiedName"], element["packageName"])
lookup[lookup_key] = element
element["members"] = []
if "enclosedBy" in element:
# enclosedBy['name'] is not specific enough for lookup (e.g. two
# libraries could both define a class 'Foo', and if we encounter
# something enclosed by 'Foo', we wouldn't know which is the correct
# 'Foo'). Instead we use this element's qualifiedName up to but not
# including the last dot-for uniqueness we also use packageName too.
qual_name = element["qualifiedName"]
lookup_key = "%s-%s" % (
qual_name[:qual_name.rfind(".")], element["packageName"])
enclosingElement = lookup[lookup_key]
enclosingElement["members"].append(element)
return libraries
def element_to_toc_item(element):
"""Convert an element to a TOC item, recursively converting children.
Args:
element (dict) - tree element represented as a dict.
"""
sub_items = []
if "members" in element:
# Group members by type, then alphabetically.
element["members"].sort(
key=lambda member: (member["type"], member["name"]))
current_type = None
for member in element["members"]:
# Insert a heading when we encounter a new type grouping.
if current_type != member["type"]:
current_type = member["type"]
sub_items.append({"heading": TYPE_HEADINGS[current_type]})
sub_items.append(element_to_toc_item(member))
return build_toc_item(
title=element["name"],
path="/reference/dart/%s" % element["href"],
sub_items=sub_items,
)
def no_args_main(index_file, outfile):
"""Modified main function which generates a toc.yaml.
Args:
index_file (str) - index.json file representing generated docs
outfile (str) - output toc.yaml file
"""
if not os.path.isfile(index_file):
raise RuntimeError("%s does not exist or is not a file" % index_file)
configure_yaml()
content = build_toc_content(index_file)
if not content:
raise RuntimeError("Unable to generate toc content. Build failed.")
with open(outfile, "w") as f:
yaml.dump(content, f, default_flow_style=False)