diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d99ebf83..113419298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,10 @@ ##### Enhancements -* None. +* Add the ability to specify nested categories and create recursive navigation + for all templates. + [Leonardo Galli](https://github.com/galli-leo) + [#624](https://github.com/realm/jazzy/issues/624) ##### Bug Fixes diff --git a/README.md b/README.md index ebbf2f58a..639df30fe 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,40 @@ Note that the `--include` option is applied before the `--exclude` option. For e Declarations with a documentation comment containing `:nodoc:` are excluded from the documentation. +### Custom Categories + +By default, jazzy categorizes everything according to their types. + +Using `custom_categories` inside a jazzy config file, you can specify how symbols are +categorized. This is really useful for larger projects, to keep a clean navigation area. + +A custom category is defined by a dictionary (or hash) containing the following keys: +* `name` (string): The name of the category as it will be displayed. +* `children` (array): The names of the symbols to include in this category. + Additionally, the array can also contain another dictionary + specifying a custom category. This allows for infinite subcategories. + +The `custom_categories` config file option accepts an array of +custom category dictionaries and an example looks like this: + +```yaml +custom_categories: + - name: My awesome Category + children: + - MyClass + - MyEnum + - name: My awesome Subcategory + children: + - MyStruct + - name: My other Category + children: + - MyGlobalFunction(_:) +``` + +A more concrete example can be found under [Siesta Nested Categories Example](https://git.io/fNvGB). + +**Note:** Currently the builtin themes only support up to three levels of nesting with the css. However, that can easily be adjusted. + ## Troubleshooting #### Swift diff --git a/lib/jazzy/config.rb b/lib/jazzy/config.rb index 28ac96511..b47ed5226 100644 --- a/lib/jazzy/config.rb +++ b/lib/jazzy/config.rb @@ -310,7 +310,13 @@ def expand_path(path) description: ['Custom navigation categories to replace the standard '\ '“Classes, Protocols, etc.”', 'Types not explicitly named '\ 'in a custom category appear in generic groups at the end.', - 'Example: https://git.io/v4Bcp'], + 'You can add another category in the children array '\ + 'instead of using a type name, to create ', + 'subcategories. This can be repeated ad infinitum, '\ + 'provided your theme supports it.', + 'Currently all integrated themes support a maximum ', + 'of 3 levels.', + 'Example: https://git.io/fNvGB'], default: [] config_attr :custom_head, diff --git a/lib/jazzy/doc_builder.rb b/lib/jazzy/doc_builder.rb index 1eb59892f..14e594f17 100644 --- a/lib/jazzy/doc_builder.rb +++ b/lib/jazzy/doc_builder.rb @@ -34,22 +34,36 @@ def self.doc_structure_for_docs(docs) children = doc.children .sort_by { |c| [c.nav_order, c.name, c.usr || ''] } .flat_map do |child| - # FIXME: include arbitrarily nested extensible types - [{ name: child.name, url: child.url }] + - Array(child.children.select do |sub_child| - sub_child.type.swift_extensible? || sub_child.type.extension? - end).map do |sub_child| - { name: "– #{sub_child.name}", url: sub_child.url } - end + if child.type.overview? + doc_structure_for_docs([child]) + else + doc_structure_for_child(child) + end end + { section: doc.name, url: doc.url, children: children, + level: doc.level, } end end + # Generate structure for children of a category in sidebar navigation. + # @return [Array] doc structure compromised of + # name and url of the child as well as + # any possible nested children. + def self.doc_structure_for_child(child) + # FIXME: include arbitrarily nested extensible types + [{ name: child.name, url: child.url, children: nil }] + + Array(child.children.select do |sub_child| + sub_child.type.swift_extensible? || sub_child.type.extension? + end).map do |sub_child| + { name: "– #{sub_child.name}", url: sub_child.url, children: nil } + end + end + # Build documentation from the given options # @param [Config] options # @return [SourceModule] the documented source module @@ -383,6 +397,34 @@ def self.render_tasks(source_module, children) end end + def self.render_subsections(subsections, source_module) + subsections.map do |subsection| + overview = render_overview(subsection) + tasks = render_tasks( + source_module, + subsection.children.reject { |c| c.type.overview? }, + ) + + { + name: subsection.name, + overview: overview, + uid: URI.encode(subsection.name), + url: subsection.url, + level: subsection.level, + tasks: tasks, + } + end + end + + def self.render_overview(doc) + overview = (doc.abstract || '') + (doc.discussion || '') + alternative_abstract = doc.alternative_abstract + if alternative_abstract + overview = render(doc, alternative_abstract) + overview + end + overview + end + # rubocop:disable Metrics/MethodLength # Build Mustache document from single parsed doc # @param [Config] options Build options @@ -395,11 +437,7 @@ def self.document(source_module, doc_model, path_to_root) return document_markdown(source_module, doc_model, path_to_root) end - overview = (doc_model.abstract || '') + (doc_model.discussion || '') - alternative_abstract = doc_model.alternative_abstract - if alternative_abstract - overview = render(doc_model, alternative_abstract) + overview - end + overview = render_overview(doc_model) doc = Doc.new # Mustache model instance doc[:custom_head] = Config.instance.custom_head @@ -412,7 +450,10 @@ def self.document(source_module, doc_model, path_to_root) doc[:declaration] = doc_model.display_declaration doc[:overview] = overview doc[:structure] = source_module.doc_structure - doc[:tasks] = render_tasks(source_module, doc_model.children) + categories, children = + doc_model.children.partition { |c| c.type.overview? } + doc[:tasks] = render_tasks(source_module, children) + doc[:subsections] = render_subsections(categories, source_module) doc[:module_name] = source_module.name doc[:author_name] = source_module.author_name doc[:github_url] = source_module.github_url diff --git a/lib/jazzy/source_category.rb b/lib/jazzy/source_category.rb new file mode 100644 index 000000000..a974858e6 --- /dev/null +++ b/lib/jazzy/source_category.rb @@ -0,0 +1,80 @@ +require 'jazzy/source_declaration' +require 'jazzy/config' +require 'jazzy/source_mark' +require 'jazzy/jazzy_markdown' + +module Jazzy + # Category (group, contents) pages generated by jazzy + class SourceCategory < SourceDeclaration + extend Config::Mixin + + def initialize(group, name, abstract, url_name) + super() + self.type = SourceDeclaration::Type.overview + self.name = name + self.url_name = url_name + self.abstract = Markdown.render(abstract) + self.children = group + self.parameters = [] + end + + # Group root-level docs into custom categories or by type + def self.group_docs(docs) + custom_categories, docs = + group_custom_categories(docs, config.custom_categories) + type_categories, uncategorized = group_type_categories( + docs, custom_categories.any? ? 'Other ' : '' + ) + custom_categories + type_categories + uncategorized + end + + def self.group_custom_categories(docs, categories) + group = categories.map do |category| + children = category['children'].flat_map do |child| + if child.is_a?(Hash) + # Nested category, recurse + children, docs = group_custom_categories(docs, [child]) + else + # Doc name, find it + children, docs = docs.partition { |doc| doc.name == child } + if children.empty? + STDERR.puts( + 'WARNING: No documented top-level declarations match ' \ + "name \"#{child}\" specified in categories file", + ) + end + end + children + end + # Category config overrides alphabetization + children.each.with_index { |child, i| child.nav_order = i } + make_group(children, category['name'], '') + end + [group.compact, docs] + end + + def self.group_type_categories(docs, type_category_prefix) + group = SourceDeclaration::Type.all.map do |type| + children, docs = docs.partition { |doc| doc.type == type } + make_group( + children, + type_category_prefix + type.plural_name, + "The following #{type.plural_name.downcase} are available globally.", + type_category_prefix + type.plural_url_name, + ) + end + [group.compact, docs] + end + + def self.make_group(group, name, abstract, url_name = nil) + group.reject! { |doc| doc.name.empty? } + unless group.empty? + SourceCategory.new(group, name, abstract, url_name) + end + end + + def level + documentation_path.length + end + end +end diff --git a/lib/jazzy/source_declaration.rb b/lib/jazzy/source_declaration.rb index 94c9fc46e..f9590cad3 100644 --- a/lib/jazzy/source_declaration.rb +++ b/lib/jazzy/source_declaration.rb @@ -45,6 +45,19 @@ def namespace_ancestors end end + # Chain of parent_in_docs from top level to self. (Includes self.) + def documentation_path + documentation_ancestors + [self] + end + + def documentation_ancestors + if parent_in_docs + parent_in_docs.documentation_path + else + [] + end + end + def fully_qualified_name namespace_path.map(&:name).join('.') end diff --git a/lib/jazzy/source_declaration/type.rb b/lib/jazzy/source_declaration/type.rb index 5f72ecff3..ea0efbd56 100644 --- a/lib/jazzy/source_declaration/type.rb +++ b/lib/jazzy/source_declaration/type.rb @@ -125,6 +125,10 @@ def objc_unexposed? kind == 'sourcekitten.source.lang.objc.decl.unexposed' end + def overview? + Type.overview == self + end + def self.overview Type.new('Overview') end diff --git a/lib/jazzy/sourcekitten.rb b/lib/jazzy/sourcekitten.rb index 046cfae43..7eaabb4da 100644 --- a/lib/jazzy/sourcekitten.rb +++ b/lib/jazzy/sourcekitten.rb @@ -11,6 +11,7 @@ require 'jazzy/source_declaration' require 'jazzy/source_mark' require 'jazzy/stats' +require 'jazzy/source_category' ELIDED_AUTOLINK_TOKEN = '36f8f5912051ae747ef441d6511ca4cb'.freeze @@ -59,58 +60,6 @@ def self.undocumented_abstract ).freeze end - # Group root-level docs by custom categories (if any) and type - def self.group_docs(docs) - custom_categories, docs = group_custom_categories(docs) - type_categories, uncategorized = group_type_categories( - docs, custom_categories.any? ? 'Other ' : '' - ) - custom_categories + type_categories + uncategorized - end - - def self.group_custom_categories(docs) - group = Config.instance.custom_categories.map do |category| - children = category['children'].flat_map do |name| - docs_with_name, docs = docs.partition { |doc| doc.name == name } - if docs_with_name.empty? - STDERR.puts 'WARNING: No documented top-level declarations match ' \ - "name \"#{name}\" specified in categories file" - end - docs_with_name - end - # Category config overrides alphabetization - children.each.with_index { |child, i| child.nav_order = i } - make_group(children, category['name'], '') - end - [group.compact, docs] - end - - def self.group_type_categories(docs, type_category_prefix) - group = SourceDeclaration::Type.all.map do |type| - children, docs = docs.partition { |doc| doc.type == type } - make_group( - children, - type_category_prefix + type.plural_name, - "The following #{type.plural_name.downcase} are available globally.", - type_category_prefix + type.plural_url_name, - ) - end - [group.compact, docs] - end - - def self.make_group(group, name, abstract, url_name = nil) - group.reject! { |doc| doc.name.empty? } - unless group.empty? - SourceDeclaration.new.tap do |sd| - sd.type = SourceDeclaration::Type.overview - sd.name = name - sd.url_name = url_name - sd.abstract = Markdown.render(abstract) - sd.children = group - end - end - end - def self.sanitize_filename(doc) unsafe_filename = doc.url_name || doc.name sanitzation_enabled = Config.instance.use_safe_filenames @@ -168,8 +117,12 @@ def self.subdir_for_doc(doc) # File program elements under top ancestor’s type (Struct, Class, etc.) [top_level_decl.type.plural_url_name] + doc.namespace_ancestors.map(&:name) - else + elsif doc.type.overview? # Categories live in their own directory + # But subcategories live in a directory named after their parent + doc.documentation_ancestors.map(&:name) + else + # Anything else [] end end @@ -806,7 +759,7 @@ def self.parse(sourcekitten_output, min_acl, skip_undocumented, inject_docs) docs = docs.reject { |doc| doc.type.swift_enum_element? } end ungrouped_docs = docs - docs = group_docs(docs) + docs = SourceCategory.group_docs(docs) make_doc_urls(docs) autolink(docs, ungrouped_docs) [docs, @stats] diff --git a/lib/jazzy/themes/fullwidth/assets/css/jazzy.css.scss b/lib/jazzy/themes/fullwidth/assets/css/jazzy.css.scss index fdba1186b..b8f7f7ef1 100644 --- a/lib/jazzy/themes/fullwidth/assets/css/jazzy.css.scss +++ b/lib/jazzy/themes/fullwidth/assets/css/jazzy.css.scss @@ -320,6 +320,12 @@ pre code { .nav-group-tasks { margin: $gutter/2 0; padding: 0 0 0 $gutter/2; + + .nav-group-name { + list-style-type: none; + border: 0px; + padding: 0px; + } } .nav-group-task { @@ -332,6 +338,24 @@ pre code { color: $navigation_task_color; } +@mixin nav_heading($font-size: 16px, $font-weight: 400, $margin: 12px 0px 0px 0px) { + font-size: $font-size; + font-weight: $font-weight; + margin: $margin; +} + +.nav-groups { + h1 { + @include nav_heading(16px, 400, 0px); + } + h2 { + @include nav_heading(); + } + h3 { + @include nav_heading(); + } +} + // =========================================================================== // // Content @@ -358,7 +382,7 @@ pre code { padding: $gutter 0; } -.section-name { +.section-name, .subsection-name { color: #666; display: block; } @@ -379,7 +403,19 @@ pre code { padding-top: 0px; } -.task-name-container { +.nested-tasks { + .task-group { + margin-top: 10px; + } +} + +.nested-tasks-line { + border: none; + border-top: $gray_border; + margin-top: 15px; +} + +.task-name-container, .subsection-name-container { a[name] { &:before { content: ""; @@ -612,4 +648,4 @@ form[role=search] { .tt-suggestion.tt-cursor .doc-parent-name { color: #fff; } -} \ No newline at end of file +} diff --git a/lib/jazzy/themes/fullwidth/templates/nav.mustache b/lib/jazzy/themes/fullwidth/templates/nav.mustache index d8fe9e02c..f0542bf0b 100644 --- a/lib/jazzy/themes/fullwidth/templates/nav.mustache +++ b/lib/jazzy/themes/fullwidth/templates/nav.mustache @@ -1,16 +1,7 @@ diff --git a/lib/jazzy/themes/fullwidth/templates/nav_section.mustache b/lib/jazzy/themes/fullwidth/templates/nav_section.mustache new file mode 100644 index 000000000..4490b9268 --- /dev/null +++ b/lib/jazzy/themes/fullwidth/templates/nav_section.mustache @@ -0,0 +1,15 @@ + diff --git a/lib/jazzy/themes/fullwidth/templates/subsection.mustache b/lib/jazzy/themes/fullwidth/templates/subsection.mustache new file mode 100644 index 000000000..61441a492 --- /dev/null +++ b/lib/jazzy/themes/fullwidth/templates/subsection.mustache @@ -0,0 +1,35 @@ +
+
+ + + + +

{{name}}

+
+
+
+ +
+
+
+
+ {{#overview}} +
+ {{{overview}}} + {{#url}} + See more + {{/url}} +
+ {{/overview}} + {{#tasks.count}} +
+
+ {{#tasks}} + {{> task}} + {{/tasks}} +
+ {{/tasks.count}} +
+ +
+
diff --git a/lib/jazzy/themes/fullwidth/templates/tasks.mustache b/lib/jazzy/themes/fullwidth/templates/tasks.mustache index 16f65e096..46d5091d6 100644 --- a/lib/jazzy/themes/fullwidth/templates/tasks.mustache +++ b/lib/jazzy/themes/fullwidth/templates/tasks.mustache @@ -4,6 +4,10 @@ {{#tasks}} {{> task}} {{/tasks}} + + {{#subsections}} + {{> subsection}} + {{/subsections}} {{/tasks.count}}