jazzy 0.14.4 → 0.15.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/Tests.yml +6 -5
- data/.rubocop.yml +19 -0
- data/CHANGELOG.md +56 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile.lock +66 -49
- data/README.md +115 -5
- data/bin/sourcekitten +0 -0
- data/jazzy.gemspec +1 -1
- data/js/package-lock.json +6 -6
- data/lib/jazzy/config.rb +156 -24
- data/lib/jazzy/doc.rb +2 -2
- data/lib/jazzy/doc_builder.rb +55 -29
- data/lib/jazzy/doc_index.rb +185 -0
- data/lib/jazzy/docset_builder/info_plist.mustache +1 -1
- data/lib/jazzy/docset_builder.rb +44 -13
- data/lib/jazzy/extensions/katex/css/katex.min.css +1 -1
- data/lib/jazzy/extensions/katex/js/katex.min.js +1 -1
- data/lib/jazzy/gem_version.rb +1 -1
- data/lib/jazzy/grouper.rb +130 -0
- data/lib/jazzy/podspec_documenter.rb +1 -1
- data/lib/jazzy/source_declaration/type.rb +10 -2
- data/lib/jazzy/source_declaration.rb +69 -8
- data/lib/jazzy/source_document.rb +5 -1
- data/lib/jazzy/source_module.rb +13 -11
- data/lib/jazzy/sourcekitten.rb +232 -236
- data/lib/jazzy/symbol_graph/ext_key.rb +37 -0
- data/lib/jazzy/symbol_graph/ext_node.rb +23 -6
- data/lib/jazzy/symbol_graph/graph.rb +31 -19
- data/lib/jazzy/symbol_graph/relationship.rb +21 -3
- data/lib/jazzy/symbol_graph/sym_node.rb +10 -22
- data/lib/jazzy/symbol_graph/symbol.rb +28 -0
- data/lib/jazzy/symbol_graph.rb +19 -16
- data/lib/jazzy/themes/apple/assets/css/jazzy.css.scss +10 -7
- data/lib/jazzy/themes/apple/assets/js/typeahead.jquery.js +3 -2
- data/lib/jazzy/themes/apple/templates/doc.mustache +8 -1
- data/lib/jazzy/themes/fullwidth/assets/css/jazzy.css.scss +5 -5
- data/lib/jazzy/themes/fullwidth/assets/js/typeahead.jquery.js +3 -2
- data/lib/jazzy/themes/fullwidth/templates/doc.mustache +8 -1
- data/lib/jazzy/themes/jony/assets/css/jazzy.css.scss +6 -5
- data/lib/jazzy/themes/jony/templates/doc.mustache +9 -2
- data/spec/integration_spec.rb +8 -1
- metadata +16 -7
data/lib/jazzy/config.rb
CHANGED
@@ -13,16 +13,17 @@ module Jazzy
|
|
13
13
|
# rubocop:disable Naming/AccessorMethodName
|
14
14
|
class Attribute
|
15
15
|
attr_reader :name, :description, :command_line, :config_file_key,
|
16
|
-
:default, :parse
|
16
|
+
:default, :parse, :per_module
|
17
17
|
|
18
18
|
def initialize(name, description: nil, command_line: nil,
|
19
|
-
default: nil, parse: ->(x) { x })
|
19
|
+
default: nil, parse: ->(x) { x }, per_module: false)
|
20
20
|
@name = name.to_s
|
21
21
|
@description = Array(description)
|
22
22
|
@command_line = Array(command_line)
|
23
23
|
@default = default
|
24
24
|
@parse = parse
|
25
25
|
@config_file_key = full_command_line_name || @name
|
26
|
+
@per_module = per_module
|
26
27
|
end
|
27
28
|
|
28
29
|
def get(config)
|
@@ -134,22 +135,26 @@ module Jazzy
|
|
134
135
|
config_attr :objc_mode,
|
135
136
|
command_line: '--[no-]objc',
|
136
137
|
description: 'Generate docs for Objective-C.',
|
137
|
-
default: false
|
138
|
+
default: false,
|
139
|
+
per_module: true
|
138
140
|
|
139
141
|
config_attr :umbrella_header,
|
140
142
|
command_line: '--umbrella-header PATH',
|
141
143
|
description: 'Umbrella header for your Objective-C framework.',
|
142
|
-
parse: ->(uh) { expand_path(uh) }
|
144
|
+
parse: ->(uh) { expand_path(uh) },
|
145
|
+
per_module: true
|
143
146
|
|
144
147
|
config_attr :framework_root,
|
145
148
|
command_line: '--framework-root PATH',
|
146
149
|
description: 'The root path to your Objective-C framework.',
|
147
|
-
parse: ->(fr) { expand_path(fr) }
|
150
|
+
parse: ->(fr) { expand_path(fr) },
|
151
|
+
per_module: true
|
148
152
|
|
149
153
|
config_attr :sdk,
|
150
154
|
command_line: '--sdk [iphone|watch|appletv][os|simulator]|macosx',
|
151
155
|
description: 'The SDK for which your code should be built.',
|
152
|
-
default: 'macosx'
|
156
|
+
default: 'macosx',
|
157
|
+
per_module: true
|
153
158
|
|
154
159
|
config_attr :hide_declarations,
|
155
160
|
command_line: '--hide-declarations [objc|swift] ',
|
@@ -175,6 +180,13 @@ module Jazzy
|
|
175
180
|
command_line: ['-b', '--build-tool-arguments arg1,arg2,…argN', Array],
|
176
181
|
description: 'Arguments to forward to xcodebuild, swift build, or ' \
|
177
182
|
'sourcekitten.',
|
183
|
+
default: [],
|
184
|
+
per_module: true
|
185
|
+
|
186
|
+
config_attr :modules,
|
187
|
+
command_line: ['--modules Mod1,Mod2,…ModN', Array],
|
188
|
+
description: 'List of modules to document. Use the config file to set per-module ' \
|
189
|
+
"build flags, see 'Documenting multiple modules' in the README.",
|
178
190
|
default: []
|
179
191
|
|
180
192
|
alias_config_attr :xcodebuild_arguments, :build_tool_arguments,
|
@@ -185,19 +197,23 @@ module Jazzy
|
|
185
197
|
command_line: ['-s', '--sourcekitten-sourcefile filepath1,…filepathN',
|
186
198
|
Array],
|
187
199
|
description: 'File(s) generated from sourcekitten output to parse',
|
188
|
-
parse: ->(paths) { [paths].flatten.map { |path| expand_path(path) } }
|
200
|
+
parse: ->(paths) { [paths].flatten.map { |path| expand_path(path) } },
|
201
|
+
default: [],
|
202
|
+
per_module: true
|
189
203
|
|
190
204
|
config_attr :source_directory,
|
191
205
|
command_line: '--source-directory DIRPATH',
|
192
206
|
description: 'The directory that contains the source to be documented',
|
193
207
|
default: Pathname.pwd,
|
194
|
-
parse: ->(sd) { expand_path(sd) }
|
208
|
+
parse: ->(sd) { expand_path(sd) },
|
209
|
+
per_module: true
|
195
210
|
|
196
211
|
config_attr :symbolgraph_directory,
|
197
212
|
command_line: '--symbolgraph-directory DIRPATH',
|
198
213
|
description: 'A directory containing a set of Swift Symbolgraph files ' \
|
199
214
|
'representing the module to be documented',
|
200
|
-
parse: ->(sd) { expand_path(sd) }
|
215
|
+
parse: ->(sd) { expand_path(sd) },
|
216
|
+
per_module: true
|
201
217
|
|
202
218
|
config_attr :excluded_files,
|
203
219
|
command_line: ['-e', '--exclude filepath1,filepath2,…filepathN', Array],
|
@@ -261,7 +277,8 @@ module Jazzy
|
|
261
277
|
config_attr :module_name,
|
262
278
|
command_line: ['-m', '--module MODULE_NAME'],
|
263
279
|
description: 'Name of module being documented. (e.g. RealmSwift)',
|
264
|
-
default: ''
|
280
|
+
default: '',
|
281
|
+
per_module: true
|
265
282
|
|
266
283
|
config_attr :version,
|
267
284
|
command_line: '--module-version VERSION',
|
@@ -284,6 +301,10 @@ module Jazzy
|
|
284
301
|
description: 'The path to a markdown README file',
|
285
302
|
parse: ->(rp) { expand_path(rp) }
|
286
303
|
|
304
|
+
config_attr :readme_title,
|
305
|
+
command_line: '--readme-title TITLE',
|
306
|
+
description: 'The title for the README in the generated documentation'
|
307
|
+
|
287
308
|
config_attr :documentation_glob,
|
288
309
|
command_line: '--documentation GLOB',
|
289
310
|
description: 'Glob that matches available documentation',
|
@@ -317,6 +338,13 @@ module Jazzy
|
|
317
338
|
command_line: '--docset-path DIRPATH',
|
318
339
|
description: 'The relative path for the generated docset'
|
319
340
|
|
341
|
+
config_attr :docset_title,
|
342
|
+
command_line: '--docset-title TITLE',
|
343
|
+
description: 'The title of the generated docset. A simplified version ' \
|
344
|
+
'is used for the filenames associated with the docset. If the ' \
|
345
|
+
'option is not set then the name of the module being documented is ' \
|
346
|
+
'used as the docset title.'
|
347
|
+
|
320
348
|
# ──────── URLs ────────
|
321
349
|
|
322
350
|
config_attr :root_url,
|
@@ -488,6 +516,23 @@ module Jazzy
|
|
488
516
|
'--min-acl is set to `public` or `open`.',
|
489
517
|
default: false
|
490
518
|
|
519
|
+
MERGE_MODULES = %w[all extensions none].freeze
|
520
|
+
|
521
|
+
config_attr :merge_modules,
|
522
|
+
command_line: "--merge-modules #{MERGE_MODULES.join(' | ')}",
|
523
|
+
description: 'Control how to display declarations from multiple ' \
|
524
|
+
'modules. `all`, the default, places all declarations of the ' \
|
525
|
+
"same kind together. `none` keeps each module's declarations " \
|
526
|
+
'separate. `extensions` is like `none` but merges ' \
|
527
|
+
'cross-module extensions into their extended type.',
|
528
|
+
default: 'all',
|
529
|
+
parse: ->(merge) do
|
530
|
+
return merge.to_sym if MERGE_MODULES.include?(merge)
|
531
|
+
|
532
|
+
raise "Unsupported merge_modules #{merge}, " \
|
533
|
+
"supported values: #{MERGE_MODULES.join(', ')}"
|
534
|
+
end
|
535
|
+
|
491
536
|
# rubocop:enable Layout/ArgumentAlignment
|
492
537
|
|
493
538
|
def initialize
|
@@ -507,12 +552,7 @@ module Jazzy
|
|
507
552
|
config.parse_config_file
|
508
553
|
PodspecDocumenter.apply_config_defaults(config.podspec, config)
|
509
554
|
|
510
|
-
|
511
|
-
config.dash_url ||= URI.join(
|
512
|
-
config.root_url,
|
513
|
-
"docsets/#{config.module_name}.xml",
|
514
|
-
)
|
515
|
-
end
|
555
|
+
config.set_module_configs
|
516
556
|
|
517
557
|
config.validate
|
518
558
|
|
@@ -569,11 +609,13 @@ module Jazzy
|
|
569
609
|
puts "Using config file #{config_path}"
|
570
610
|
config_file = read_config_file(config_path)
|
571
611
|
|
572
|
-
attrs_by_conf_key, attrs_by_name =
|
573
|
-
self.class.all_config_attrs.group_by(&prop)
|
574
|
-
end
|
612
|
+
attrs_by_conf_key, attrs_by_name = grouped_attributes
|
575
613
|
|
576
|
-
config_file
|
614
|
+
parse_config_hash(config_file, attrs_by_conf_key, attrs_by_name)
|
615
|
+
end
|
616
|
+
|
617
|
+
def parse_config_hash(hash, attrs_by_conf_key, attrs_by_name, override: false)
|
618
|
+
hash.each do |key, value|
|
577
619
|
unless attr = attrs_by_conf_key[key]
|
578
620
|
message = "Unknown config file attribute #{key.inspect}"
|
579
621
|
if matching_name = attrs_by_name[key]
|
@@ -583,11 +625,19 @@ module Jazzy
|
|
583
625
|
warning message
|
584
626
|
next
|
585
627
|
end
|
586
|
-
|
587
|
-
attr.first.
|
628
|
+
setter = override ? :set : :set_if_unconfigured
|
629
|
+
attr.first.method(setter).call(self, value)
|
588
630
|
end
|
631
|
+
end
|
589
632
|
|
590
|
-
|
633
|
+
# Find keyed versions of the attributes, by config file key and then name-in-code
|
634
|
+
# Optional block allows filtering/overriding of attribute list.
|
635
|
+
def grouped_attributes
|
636
|
+
attrs = self.class.all_config_attrs
|
637
|
+
attrs = yield attrs if block_given?
|
638
|
+
%i[config_file_key name].map do |property|
|
639
|
+
attrs.group_by(&property)
|
640
|
+
end
|
591
641
|
end
|
592
642
|
|
593
643
|
def validate
|
@@ -598,6 +648,19 @@ module Jazzy
|
|
598
648
|
'`source_host_url` or `source_host_files_url`.'
|
599
649
|
end
|
600
650
|
|
651
|
+
if modules_configured && module_name_configured
|
652
|
+
raise 'Options `modules` and `module` are both set which is not supported. ' \
|
653
|
+
'To document multiple modules, use just `modules`.'
|
654
|
+
end
|
655
|
+
|
656
|
+
if modules_configured && podspec_configured
|
657
|
+
raise 'Options `modules` and `podspec` are both set which is not supported.'
|
658
|
+
end
|
659
|
+
|
660
|
+
module_configs.each(&:validate_module)
|
661
|
+
end
|
662
|
+
|
663
|
+
def validate_module
|
601
664
|
if objc_mode &&
|
602
665
|
build_tool_arguments_configured &&
|
603
666
|
(framework_root_configured || umbrella_header_configured)
|
@@ -608,6 +671,76 @@ module Jazzy
|
|
608
671
|
|
609
672
|
# rubocop:enable Metrics/MethodLength
|
610
673
|
|
674
|
+
# Module Configs
|
675
|
+
#
|
676
|
+
# The user can enter module information in three different ways. This
|
677
|
+
# consolidates them into one view for the rest of the code.
|
678
|
+
#
|
679
|
+
# 1) Single module, back-compatible
|
680
|
+
# --module Foo etc etc (or not given at all)
|
681
|
+
#
|
682
|
+
# 2) Multiple modules, simple, sharing build params
|
683
|
+
# --modules Foo,Bar,Baz --source-directory Xyz
|
684
|
+
#
|
685
|
+
# 3) Multiple modules, custom, different build params but
|
686
|
+
# inheriting others from the top level.
|
687
|
+
# This is config-file only.
|
688
|
+
# - modules
|
689
|
+
# - module: Foo
|
690
|
+
# source_directory: Xyz
|
691
|
+
# build_tool_arguments: [a, b, c]
|
692
|
+
#
|
693
|
+
# After this we're left with `config.module_configs` that is an
|
694
|
+
# array of `Config` objects.
|
695
|
+
|
696
|
+
attr_reader :module_configs
|
697
|
+
attr_reader :module_names
|
698
|
+
|
699
|
+
def set_module_configs
|
700
|
+
@module_configs = parse_module_configs
|
701
|
+
@module_names = module_configs.map(&:module_name)
|
702
|
+
@module_names_set = Set.new(module_names)
|
703
|
+
end
|
704
|
+
|
705
|
+
def module_name?(name)
|
706
|
+
@module_names_set.include?(name)
|
707
|
+
end
|
708
|
+
|
709
|
+
def multiple_modules?
|
710
|
+
@module_names.count > 1
|
711
|
+
end
|
712
|
+
|
713
|
+
def parse_module_configs
|
714
|
+
return [self] unless modules_configured
|
715
|
+
|
716
|
+
raise 'Config file key `modules` must be an array' unless modules.is_a?(Array)
|
717
|
+
|
718
|
+
if modules.first.is_a?(String)
|
719
|
+
# Massage format (2) into (3)
|
720
|
+
self.modules = modules.map { |mod| { 'module' => mod } }
|
721
|
+
end
|
722
|
+
|
723
|
+
# Allow per-module overrides of only some config options
|
724
|
+
attrs_by_conf_key, attrs_by_name =
|
725
|
+
grouped_attributes { |attr| attr.select(&:per_module) }
|
726
|
+
|
727
|
+
modules.map do |module_hash|
|
728
|
+
mod_name = module_hash['module'] || ''
|
729
|
+
raise 'Missing `modules.module` config key' if mod_name.empty?
|
730
|
+
|
731
|
+
dup.tap do |module_config|
|
732
|
+
module_config.parse_config_hash(
|
733
|
+
module_hash, attrs_by_conf_key, attrs_by_name, override: true
|
734
|
+
)
|
735
|
+
end
|
736
|
+
end
|
737
|
+
end
|
738
|
+
|
739
|
+
# For podspec query
|
740
|
+
def module_name_known?
|
741
|
+
module_name_configured || modules_configured
|
742
|
+
end
|
743
|
+
|
611
744
|
def locate_config_file
|
612
745
|
return config_file if config_file
|
613
746
|
|
@@ -615,7 +748,6 @@ module Jazzy
|
|
615
748
|
candidate = dir.join('.jazzy.yaml')
|
616
749
|
return candidate if candidate.exist?
|
617
750
|
end
|
618
|
-
|
619
751
|
nil
|
620
752
|
end
|
621
753
|
|
data/lib/jazzy/doc.rb
CHANGED
@@ -48,9 +48,9 @@ module Jazzy
|
|
48
48
|
elsif config.version_configured
|
49
49
|
# Fake version for integration tests
|
50
50
|
version = ENV['JAZZY_FAKE_MODULE_VERSION'] || config.version
|
51
|
-
"#{config.module_name} #{version} Docs"
|
51
|
+
"#{config.module_configs.first.module_name} #{version} Docs"
|
52
52
|
else
|
53
|
-
"#{config.module_name} Docs"
|
53
|
+
"#{config.module_configs.first.module_name} Docs"
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
data/lib/jazzy/doc_builder.rb
CHANGED
@@ -67,32 +67,32 @@ module Jazzy
|
|
67
67
|
|
68
68
|
# Build documentation from the given options
|
69
69
|
# @param [Config] options
|
70
|
-
# @return [SourceModule] the documented source module
|
71
70
|
def self.build(options)
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
71
|
+
module_jsons = options.module_configs.map do |module_config|
|
72
|
+
if module_config.podspec_configured
|
73
|
+
# Config#validate guarantees not multi-module here
|
74
|
+
pod_documenter = PodspecDocumenter.new(options.podspec)
|
75
|
+
pod_documenter.sourcekitten_output(options)
|
76
|
+
elsif !module_config.sourcekitten_sourcefile.empty?
|
77
|
+
"[#{module_config.sourcekitten_sourcefile.map(&:read).join(',')}]"
|
78
|
+
elsif module_config.swift_build_tool == :symbolgraph
|
79
|
+
SymbolGraph.build(module_config)
|
80
|
+
else
|
81
|
+
Dir.chdir(module_config.source_directory) do
|
82
|
+
arguments = SourceKitten.arguments_from_options(module_config)
|
83
|
+
SourceKitten.run_sourcekitten(arguments)
|
84
|
+
end
|
83
85
|
end
|
84
86
|
end
|
85
87
|
|
86
|
-
build_docs_for_sourcekitten_output(
|
88
|
+
build_docs_for_sourcekitten_output(module_jsons, options)
|
87
89
|
end
|
88
90
|
|
89
91
|
# Build & write HTML docs to disk from structured docs array
|
90
92
|
# @param [String] output_dir Root directory to write docs
|
91
|
-
# @param [
|
92
|
-
|
93
|
-
|
94
|
-
def self.build_docs(output_dir, docs, source_module)
|
95
|
-
each_doc(output_dir, docs) do |doc, path|
|
93
|
+
# @param [SourceModule] source_module All info to generate docs
|
94
|
+
def self.build_docs(output_dir, source_module)
|
95
|
+
each_doc(output_dir, source_module.docs) do |doc, path|
|
96
96
|
prepare_output_dir(path.parent, false)
|
97
97
|
depth = path.relative_path_from(output_dir).each_filename.count - 1
|
98
98
|
path_to_root = '../' * depth
|
@@ -124,10 +124,13 @@ module Jazzy
|
|
124
124
|
|
125
125
|
docs << SourceDocument.make_index(options.readme_path)
|
126
126
|
|
127
|
-
source_module = SourceModule.new(options, docs, structure, coverage)
|
128
|
-
|
129
127
|
output_dir = options.output
|
130
|
-
|
128
|
+
|
129
|
+
docset_builder = DocsetBuilder.new(output_dir)
|
130
|
+
|
131
|
+
source_module = SourceModule.new(docs, structure, coverage, docset_builder)
|
132
|
+
|
133
|
+
build_docs(output_dir, source_module)
|
131
134
|
|
132
135
|
unless options.disable_search
|
133
136
|
warn 'building search index'
|
@@ -137,7 +140,7 @@ module Jazzy
|
|
137
140
|
copy_extensions(source_module, output_dir)
|
138
141
|
copy_theme_assets(output_dir)
|
139
142
|
|
140
|
-
|
143
|
+
docset_builder.build!(source_module.all_declarations)
|
141
144
|
|
142
145
|
generate_badge(source_module.doc_coverage, options)
|
143
146
|
|
@@ -148,14 +151,12 @@ module Jazzy
|
|
148
151
|
end
|
149
152
|
|
150
153
|
# Build docs given sourcekitten output
|
151
|
-
# @param [String] sourcekitten_output Output of sourcekitten command
|
154
|
+
# @param [Array<String>] sourcekitten_output Output of sourcekitten command for each module
|
152
155
|
# @param [Config] options Build options
|
153
|
-
# @return [SourceModule] the documented source module
|
154
156
|
def self.build_docs_for_sourcekitten_output(sourcekitten_output, options)
|
155
157
|
(docs, stats) = SourceKitten.parse(
|
156
158
|
sourcekitten_output,
|
157
|
-
options
|
158
|
-
options.skip_undocumented,
|
159
|
+
options,
|
159
160
|
DocumentationGenerator.source_docs,
|
160
161
|
)
|
161
162
|
|
@@ -249,7 +250,8 @@ module Jazzy
|
|
249
250
|
doc[:doc_coverage] = source_module.doc_coverage unless
|
250
251
|
Config.instance.hide_documentation_coverage
|
251
252
|
doc[:structure] = source_module.doc_structure
|
252
|
-
doc[:
|
253
|
+
doc[:readme_title] = source_module.readme_title
|
254
|
+
doc[:module_name] = doc[:readme_title]
|
253
255
|
doc[:author_name] = source_module.author_name
|
254
256
|
if source_host = source_module.host
|
255
257
|
doc[:source_host_name] = source_host.name
|
@@ -259,7 +261,8 @@ module Jazzy
|
|
259
261
|
doc[:github_url] = doc[:source_host_url]
|
260
262
|
doc[:github_token_url] = doc[:source_host_item_url]
|
261
263
|
end
|
262
|
-
doc[:dash_url] = source_module.
|
264
|
+
doc[:dash_url] = source_module.dash_feed_url
|
265
|
+
doc[:breadcrumbs] = make_breadcrumbs(doc_model)
|
263
266
|
end
|
264
267
|
end
|
265
268
|
|
@@ -269,7 +272,7 @@ module Jazzy
|
|
269
272
|
# @param [String] path_to_root
|
270
273
|
def self.document_markdown(source_module, doc_model, path_to_root)
|
271
274
|
doc = new_document(source_module, doc_model)
|
272
|
-
name = doc_model.
|
275
|
+
name = doc_model.readme? ? source_module.readme_title : doc_model.name
|
273
276
|
doc[:name] = name
|
274
277
|
doc[:overview] = render(doc_model, doc_model.content(source_module))
|
275
278
|
doc[:path_to_root] = path_to_root
|
@@ -447,5 +450,28 @@ module Jazzy
|
|
447
450
|
doc.render.gsub(ELIDED_AUTOLINK_TOKEN, path_to_root)
|
448
451
|
end
|
449
452
|
# rubocop:enable Metrics/MethodLength
|
453
|
+
|
454
|
+
# Breadcrumbs for a page - doesn't include the top 'readme' crumb
|
455
|
+
def self.make_breadcrumbs(doc_model)
|
456
|
+
return [] if doc_model.readme?
|
457
|
+
|
458
|
+
docs_path = doc_model.docs_path
|
459
|
+
breadcrumbs = docs_path.map do |doc|
|
460
|
+
{
|
461
|
+
name: doc.name,
|
462
|
+
url: doc.url,
|
463
|
+
last: doc == doc_model,
|
464
|
+
}
|
465
|
+
end
|
466
|
+
|
467
|
+
return breadcrumbs if breadcrumbs.count == 1
|
468
|
+
|
469
|
+
# Add the module name to the outer type if not clear from context
|
470
|
+
if docs_path[1].ambiguous_module_name?(docs_path[0].name)
|
471
|
+
breadcrumbs[1][:name] = docs_path[1].fully_qualified_module_name
|
472
|
+
end
|
473
|
+
|
474
|
+
breadcrumbs
|
475
|
+
end
|
450
476
|
end
|
451
477
|
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jazzy
|
4
|
+
# This class stores an index of symbol names for doing name lookup
|
5
|
+
# when resolving custom categories and autolinks.
|
6
|
+
class DocIndex
|
7
|
+
# A node in the index tree. The root has no decl; its children are
|
8
|
+
# per-module indexed by module names. The second level, where each
|
9
|
+
# scope is a module, also has no decl; its children are scopes, one
|
10
|
+
# for each top-level decl in the module. From the third level onwards
|
11
|
+
# the decl is valid.
|
12
|
+
class Scope
|
13
|
+
attr_reader :decl # SourceDeclaration
|
14
|
+
attr_reader :children # String:Scope
|
15
|
+
|
16
|
+
def initialize(decl, children)
|
17
|
+
@decl = decl
|
18
|
+
@children = children
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.new_root(module_decls)
|
22
|
+
new(nil,
|
23
|
+
module_decls.transform_values do |decls|
|
24
|
+
Scope.new_decl(nil, decls)
|
25
|
+
end)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Decl names in a scope are usually unique. The exceptions
|
29
|
+
# are (1) methods and (2) typealias+extension, which historically
|
30
|
+
# jazzy does not merge. The logic here and in `merge()` below
|
31
|
+
# preserves the historical ambiguity-resolution of (1) and tries
|
32
|
+
# to do the best for (2).
|
33
|
+
def self.new_decl(decl, child_decls)
|
34
|
+
child_scopes = {}
|
35
|
+
child_decls.flat_map do |child_decl|
|
36
|
+
child_scope = Scope.new_decl(child_decl, child_decl.children)
|
37
|
+
child_decl.index_names.map do |name|
|
38
|
+
if curr = child_scopes[name]
|
39
|
+
curr.merge(child_scope)
|
40
|
+
else
|
41
|
+
child_scopes[name] = child_scope
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
new(decl, child_scopes)
|
46
|
+
end
|
47
|
+
|
48
|
+
def merge(new_scope)
|
49
|
+
return unless type = decl&.type
|
50
|
+
return unless new_type = new_scope.decl&.type
|
51
|
+
|
52
|
+
if type.swift_typealias? && new_type.swift_extension?
|
53
|
+
@children = new_scope.children
|
54
|
+
elsif type.swift_extension? && new_type.swift_typealias?
|
55
|
+
@decl = new_scope.decl
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Lookup of a name like `Mod.Type.method(arg:)` requires passing
|
60
|
+
# an array of name 'parts' eg. ['Mod', 'Type', 'method(arg:)'].
|
61
|
+
def lookup(parts)
|
62
|
+
return decl if parts.empty?
|
63
|
+
|
64
|
+
children[parts.first]&.lookup(parts[1...])
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get an array of scopes matching the name parts.
|
68
|
+
def lookup_path(parts)
|
69
|
+
[self] +
|
70
|
+
(children[parts.first]&.lookup_path(parts[1...]) || [])
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
attr_reader :root_scope
|
75
|
+
|
76
|
+
def initialize(all_decls)
|
77
|
+
@root_scope = Scope.new_root(all_decls.group_by(&:module_name))
|
78
|
+
end
|
79
|
+
|
80
|
+
# Look up a name and return the matching SourceDeclaration or nil.
|
81
|
+
#
|
82
|
+
# `context` is an optional SourceDeclaration indicating where the text
|
83
|
+
# was found, affects name resolution - see `lookup_context()` below.
|
84
|
+
def lookup(name, context = nil)
|
85
|
+
lookup_name = LookupName.new(name)
|
86
|
+
|
87
|
+
return lookup_fully_qualified(lookup_name) if lookup_name.fully_qualified?
|
88
|
+
return lookup_guess(lookup_name) if context.nil?
|
89
|
+
|
90
|
+
lookup_context(lookup_name, context)
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Look up a fully-qualified name, ie. it starts with the module name.
|
96
|
+
def lookup_fully_qualified(lookup_name)
|
97
|
+
root_scope.lookup(lookup_name.parts)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Look up a top-level name best-effort, searching for a module that
|
101
|
+
# has it before trying the first name-part as a module name.
|
102
|
+
def lookup_guess(lookup_name)
|
103
|
+
root_scope.children.each_value do |module_scope|
|
104
|
+
if result = module_scope.lookup(lookup_name.parts)
|
105
|
+
return result
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
lookup_fully_qualified(lookup_name)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Look up a name from a declaration context, approximately how
|
113
|
+
# Swift resolves names.
|
114
|
+
#
|
115
|
+
# 1 - try and resolve with a common prefix, eg. 'B' from 'T.A'
|
116
|
+
# can match 'T.B', or 'R' from 'S.T.A' can match 'S.R'.
|
117
|
+
# 2 - try and resolve as a top-level symbol from a different module
|
118
|
+
# 3 - (affordance for docs writers) resolve as a child of the context,
|
119
|
+
# eg. 'B' from 'T.A' can match 'T.A.B' *only if* (1,2) fail.
|
120
|
+
# Currently disabled for Swift for back-compatibility.
|
121
|
+
def lookup_context(lookup_name, context)
|
122
|
+
context_scope_path =
|
123
|
+
root_scope.lookup_path(context.fully_qualified_module_name_parts)
|
124
|
+
|
125
|
+
context_scope = context_scope_path.pop
|
126
|
+
context_scope_path.reverse.each do |scope|
|
127
|
+
if decl = scope.lookup(lookup_name.parts)
|
128
|
+
return decl
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
lookup_guess(lookup_name) ||
|
133
|
+
(lookup_name.objc? && context_scope.lookup(lookup_name.parts))
|
134
|
+
end
|
135
|
+
|
136
|
+
# Helper for name lookup, really a cache for information as we
|
137
|
+
# try various strategies.
|
138
|
+
class LookupName
|
139
|
+
attr_reader :name
|
140
|
+
|
141
|
+
def initialize(name)
|
142
|
+
@name = name
|
143
|
+
end
|
144
|
+
|
145
|
+
def fully_qualified?
|
146
|
+
name.start_with?('/')
|
147
|
+
end
|
148
|
+
|
149
|
+
def objc?
|
150
|
+
name.start_with?('-', '+')
|
151
|
+
end
|
152
|
+
|
153
|
+
def parts
|
154
|
+
@parts ||= find_parts
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
# Turn a name as written into a list of components to
|
160
|
+
# be matched.
|
161
|
+
# Swift: Strip out odd characters and split
|
162
|
+
# ObjC: Compound names look like '+[Class(Category) method:]'
|
163
|
+
# and need to become ['Class(Category)', '+method:']
|
164
|
+
def find_parts
|
165
|
+
if name =~ /([+-])\[(\w+(?: ?\(\w+\))?) ([\w:]+)\]/
|
166
|
+
[Regexp.last_match[2],
|
167
|
+
Regexp.last_match[1] + Regexp.last_match[3]]
|
168
|
+
else
|
169
|
+
name
|
170
|
+
.sub(%r{^[@\/]}, '') # ignore custom attribute reference, fully-qualified
|
171
|
+
.gsub(/<.*?>/, '') # remove generic parameters
|
172
|
+
.split(%r{(?<!\.)[/.](?!\.)}) # dot or slash, but not '...'
|
173
|
+
.reject(&:empty?)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
class SourceDeclaration
|
180
|
+
# Names for a symbol. Permits function parameters to be omitted.
|
181
|
+
def index_names
|
182
|
+
[name, name.sub(/\(.*\)/, '(...)')].uniq
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|