jazzy 0.14.4 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/Tests.yml +6 -5
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +40 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile.lock +62 -47
- data/README.md +115 -5
- data/bin/sourcekitten +0 -0
- 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 +231 -237
- 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 +5 -2
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 { { 'module' => _1 } }
|
721
|
+
end
|
722
|
+
|
723
|
+
# Allow per-module overrides of only some config options
|
724
|
+
attrs_by_conf_key, attrs_by_name =
|
725
|
+
grouped_attributes { _1.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
|