require 'json' require 'pathname' require 'shellwords' require 'xcinvoke' require 'cgi' require 'rexml/document' require 'jazzy/config' require 'jazzy/executable' require 'jazzy/highlighter' require 'jazzy/source_declaration' require 'jazzy/source_mark' require 'jazzy/stats' ELIDED_AUTOLINK_TOKEN = '36f8f5912051ae747ef441d6511ca4cb'.freeze def autolink_regex(middle_regex, after_highlight) start_tag_re, end_tag_re = if after_highlight [//, ''] else ['', ''] end /(#{start_tag_re})[ \t]*(#{middle_regex})[ \t]*(#{end_tag_re})/ end class String def autolink_block(doc_url, middle_regex, after_highlight) gsub(autolink_regex(middle_regex, after_highlight)) do original = Regexp.last_match(0) start_tag, raw_name, end_tag = Regexp.last_match.captures link_target = yield(CGI.unescape_html(raw_name)) if link_target && !link_target.type.extension? && link_target.url && link_target.url != doc_url.split('#').first && # Don't link to parent link_target.url != doc_url # Don't link to self start_tag + "" + raw_name + '' + end_tag else original end end end def unindent(count) gsub(/^#{' ' * count}/, '') end end module Jazzy # This module interacts with the sourcekitten command-line executable module SourceKitten def self.undocumented_abstract @undocumented_abstract ||= Markdown.render( Config.instance.undocumented_text, ).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) unlisted_prefix = Config.instance.custom_categories_unlisted_prefix type_categories, uncategorized = group_type_categories( docs, custom_categories.any? ? unlisted_prefix : '' ) custom_categories + merge_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 # Join categories with the same name (eg. ObjC and Swift classes) def self.merge_categories(categories) merged = [] categories.each do |new_category| if existing = merged.find { |c| c.name == new_category.name } existing.children += new_category.children else merged.append(new_category) end end merged 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 # Merge consecutive sections with the same mark into one section def self.merge_consecutive_marks(docs) prev_mark = nil docs.each do |doc| if prev_mark && prev_mark.can_merge?(doc.mark) doc.mark = prev_mark end prev_mark = doc.mark merge_consecutive_marks(doc.children) end end def self.sanitize_filename(doc) unsafe_filename = doc.url_name || doc.name sanitzation_enabled = Config.instance.use_safe_filenames if sanitzation_enabled && !doc.type.name_controlled_manually? return CGI.escape(unsafe_filename).gsub('_', '%5F').tr('%', '_') else return unsafe_filename end end # rubocop:disable Metrics/MethodLength # Generate doc URL by prepending its parents URLs # @return [Hash] input docs with URLs def self.make_doc_urls(docs) docs.each do |doc| if doc.render_as_page? doc.url = ( subdir_for_doc(doc) + [sanitize_filename(doc) + '.html'] ).map { |path| ERB::Util.url_encode(path) }.join('/') doc.children = make_doc_urls(doc.children) else # Don't create HTML page for this doc if it doesn't have children # Instead, make its link a hash-link on its parent's page if doc.typename == '<>' warn 'A compile error prevented ' + doc.fully_qualified_name + ' from receiving a unique USR. Documentation may be ' \ 'incomplete. Please check for compile errors by running ' \ '`xcodebuild` or `swift build` with arguments ' \ "`#{Config.instance.build_tool_arguments.shelljoin}`." end id = doc.usr unless id id = doc.name || 'unknown' warn "`#{id}` has no USR. First make sure all modules used in " \ 'your project have been imported. If all used modules are ' \ 'imported, please report this problem by filing an issue at ' \ 'https://github.com/realm/jazzy/issues along with your Xcode ' \ 'project. If this token is declared in an `#if` block, please ' \ 'ignore this message.' end doc.url = doc.parent_in_docs.url + '#/' + id end end end # rubocop:enable Metrics/MethodLength # Determine the subdirectory in which a doc should be placed. # Guides in the root for back-compatibility. # Declarations under outer namespace type (Structures, Classes, etc.) def self.subdir_for_doc(doc) return [] if doc.type.markdown? top_level_decl = doc.namespace_path.first if top_level_decl.type.name [top_level_decl.type.plural_url_name] + doc.namespace_ancestors.map(&:name) else # Category - in the root [] end end # returns all subdirectories of specified path def self.rec_path(path) path.children.collect do |child| if child.directory? rec_path(child) + [child] end end.select { |x| x }.flatten(1) end def self.use_spm?(options) options.swift_build_tool == :spm || (!options.swift_build_tool_configured && Dir['*.xcodeproj', '*.xcworkspace'].empty? && !options.build_tool_arguments.include?('-project') && !options.build_tool_arguments.include?('-workspace')) end # Builds SourceKitten arguments based on Jazzy options def self.arguments_from_options(options) arguments = ['doc'] if options.objc_mode arguments += objc_arguments_from_options(options) else arguments += ['--spm'] if use_spm?(options) unless options.module_name.empty? arguments += ['--module-name', options.module_name] end arguments += ['--'] end arguments + options.build_tool_arguments end def self.objc_arguments_from_options(options) arguments = [] if options.build_tool_arguments.empty? arguments += ['--objc', options.umbrella_header.to_s, '--', '-x', 'objective-c', '-isysroot', `xcrun --show-sdk-path --sdk #{options.sdk}`.chomp, '-I', options.framework_root.to_s, '-fmodules'] end # add additional -I arguments for each subdirectory of framework_root unless options.framework_root.nil? rec_path(Pathname.new(options.framework_root.to_s)).collect do |child| if child.directory? arguments += ['-I', child.to_s] end end end arguments end # Run sourcekitten with given arguments and return STDOUT def self.run_sourcekitten(arguments) if swift_version = Config.instance.swift_version unless xcode = XCInvoke::Xcode.find_swift_version(swift_version) raise "Unable to find an Xcode with swift version #{swift_version}." end env = xcode.as_env else env = ENV end bin_path = Pathname(__FILE__) + '../../../bin/sourcekitten' output, = Executable.execute_command(bin_path, arguments, true, env: env) output end def self.make_default_doc_info(declaration) # @todo: Fix these declaration.line = nil declaration.column = nil declaration.abstract = '' declaration.parameters = [] declaration.children = [] end def self.availability_attribute?(doc) return false unless doc['key.attributes'] !doc['key.attributes'].select do |attribute| attribute.values.first == 'source.decl.attribute.available' end.empty? end # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/PerceivedComplexity def self.should_document?(doc) return false if doc['key.doc.comment'].to_s.include?(':nodoc:') type = SourceDeclaration::Type.new(doc['key.kind']) # Always document Objective-C declarations. return true unless type.swift_type? # Don't document Swift types if we are hiding Swift return false if Config.instance.hide_swift? # Don't document @available declarations with no USR, since it means # they're unavailable. if availability_attribute?(doc) && !doc['key.usr'] return false end # Document enum elements, since we can't tell their ACL. return true if type.swift_enum_element? # Document extensions if they might have parts covered by the ACL. return should_document_swift_extension?(doc) if type.swift_extension? acl_ok = SourceDeclaration::AccessControlLevel.from_doc(doc) >= @min_acl unless acl_ok @stats.add_acl_skipped @inaccessible_protocols.append(doc['key.name']) if type.swift_protocol? end acl_ok end # rubocop:enable Metrics/CyclomaticComplexity # rubocop:enable Metrics/PerceivedComplexity def self.should_document_swift_extension?(doc) doc['key.inheritedtypes'] || Array(doc['key.substructure']).any? do |subdoc| subtype = SourceDeclaration::Type.new(subdoc['key.kind']) !subtype.mark? && should_document?(subdoc) end end def self.should_mark_undocumented(filepath) source_directory = Config.instance.source_directory.to_s (filepath || '').start_with?(source_directory) end def self.process_undocumented_token(doc, declaration) make_default_doc_info(declaration) filepath = doc['key.filepath'] if !declaration.swift? || should_mark_undocumented(filepath) @stats.add_undocumented(declaration) return nil if @skip_undocumented declaration.abstract = undocumented_abstract else declaration.abstract = Markdown.render(doc['key.doc.comment'] || '', declaration.highlight_language) end declaration end def self.parameters(doc, discovered) (doc['key.doc.parameters'] || []).map do |p| name = p['name'] { name: name, discussion: discovered[name], } end.reject { |param| param[:discussion].nil? } end def self.make_doc_info(doc, declaration) return unless should_document?(doc) highlight_declaration(doc, declaration) make_deprecation_info(doc, declaration) unless doc['key.doc.full_as_xml'] return process_undocumented_token(doc, declaration) end declaration.abstract = Markdown.render(doc['key.doc.comment'] || '', declaration.highlight_language) declaration.discussion = '' declaration.return = Markdown.rendered_returns declaration.parameters = parameters(doc, Markdown.rendered_parameters) @stats.add_documented end def self.highlight_declaration(doc, declaration) if declaration.swift? declaration.declaration = Highlighter.highlight_swift(make_swift_declaration(doc, declaration)) else declaration.declaration = Highlighter.highlight_objc( make_objc_declaration(doc['key.parsed_declaration']), ) declaration.other_language_declaration = Highlighter.highlight_swift(doc['key.swift_declaration']) end end def self.make_deprecation_info(doc, declaration) if declaration.deprecated declaration.deprecation_message = Markdown.render(doc['key.deprecation_message'] || '') end if declaration.unavailable declaration.unavailable_message = Markdown.render(doc['key.unavailable_message'] || '') end end # Strip tags and convert entities def self.xml_to_text(xml) document = REXML::Document.new(xml) REXML::XPath.match(document.root, '//text()').map(&:value).join rescue '' end # Regexp to match an @attribute. Complex to handle @available(). def self.attribute_regexp(name) qstring = /"(?:[^"\\]*|\\.)*"/ %r{@#{name} # @attr name (?:\s*\( # optionally followed by spaces + parens, (?: # containing any number of either.. [^")]*| # normal characters or... #{qstring} # quoted strings. )* # (end parens content) \))? # (end optional parens) }x end # Get all attributes of some name def self.extract_attributes(declaration, name = '\w+') attrs = declaration.scan(attribute_regexp(name)) # Rouge #806 workaround, use unicode lookalike for ')' inside attributes. attrs.map { |str| str.gsub(/\)(?!\s*$)/, "\ufe5a") } end def self.extract_availability(declaration) extract_attributes(declaration, 'available') end # Split leading attributes from a decl, returning both parts. def self.split_decl_attributes(declaration) declaration =~ /^((?:#{attribute_regexp('\w+')}\s*)*)(.*)$/m Regexp.last_match.captures end def self.prefer_parsed_decl?(parsed, annotated, type) return true if annotated.empty? return false unless parsed return false if type.swift_variable? # prefer { get }-style annotated.include?(' = default') || # SR-2608 (parsed.scan(/@autoclosure|@escaping/).count > annotated.scan(/@autoclosure|@escaping/).count) || # SR-6321 parsed.include?("\n") # user formatting end # Apply fixes to improve the compiler's declaration def self.fix_up_compiler_decl(annotated_decl, declaration) annotated_decl. # Replace the fully qualified name of a type with its base name gsub(declaration.fully_qualified_name_regexp, declaration.name). # Workaround for SR-9816 gsub(" {\n get\n }", ''). # Workaround for SR-12139 gsub(/mutating\s+mutating/, 'mutating') end # Find the best Swift declaration def self.make_swift_declaration(doc, declaration) # From compiler 'quick help' style annotated_decl_xml = doc['key.annotated_decl'] return nil unless annotated_decl_xml annotated_decl_attrs, annotated_decl_body = split_decl_attributes(xml_to_text(annotated_decl_xml)) # From source code parsed_decl = doc['key.parsed_declaration'] # Don't present type attributes on extensions return parsed_decl if declaration.type.extension? decl = if prefer_parsed_decl?(parsed_decl, annotated_decl_body, declaration.type) # Strip any attrs captured by parsed version inline_attrs, parsed_decl_body = split_decl_attributes(parsed_decl) parsed_decl_body.unindent(inline_attrs.length) else # Improve the compiler declaration fix_up_compiler_decl(annotated_decl_body, declaration) end # @available attrs only in compiler 'interface' style available_attrs = extract_availability(doc['key.doc.declaration'] || '') available_attrs.concat(extract_attributes(annotated_decl_attrs)) .push(decl) .join("\n") end # Strip default property attributes because libclang # adds them all, even if absent in the original source code. DEFAULT_ATTRIBUTES = %w[atomic readwrite assign unsafe_unretained].freeze def self.make_objc_declaration(declaration) return declaration if Config.instance.keep_property_attributes declaration =~ /\A@property\s+\((.*?)\)/ return declaration unless Regexp.last_match attrs = Regexp.last_match[1].split(',').map(&:strip) - DEFAULT_ATTRIBUTES attrs_text = attrs.empty? ? '' : " (#{attrs.join(', ')})" declaration.sub(/(?<=@property)\s+\(.*?\)/, attrs_text) .gsub(/\s+/, ' ') end def self.make_substructure(doc, declaration) return [] unless subdocs = doc['key.substructure'] make_source_declarations(subdocs, declaration, declaration.mark_for_children) end # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/PerceivedComplexity def self.make_source_declarations(docs, parent = nil, mark = SourceMark.new) declarations = [] current_mark = mark Array(docs).each do |doc| if doc.key?('key.diagnostic_stage') declarations += make_source_declarations( doc['key.substructure'], parent ) next end declaration = SourceDeclaration.new declaration.parent_in_code = parent declaration.type = SourceDeclaration::Type.new(doc['key.kind']) declaration.typename = doc['key.typename'] declaration.objc_name = doc['key.name'] documented_name = if Config.instance.hide_objc? && doc['key.swift_name'] doc['key.swift_name'] else declaration.objc_name end if declaration.type.task_mark?(documented_name) current_mark = SourceMark.new(documented_name) end if declaration.type.swift_enum_case? # Enum "cases" are thin wrappers around enum "elements". declarations += make_source_declarations( doc['key.substructure'], parent, current_mark ) next end next unless declaration.type.should_document? unless declaration.type.name raise 'Please file an issue at ' \ 'https://github.com/realm/jazzy/issues about adding support ' \ "for `#{declaration.type.kind}`." end declaration.file = Pathname(doc['key.filepath']) if doc['key.filepath'] declaration.usr = doc['key.usr'] declaration.modulename = doc['key.modulename'] declaration.name = documented_name declaration.mark = current_mark declaration.access_control_level = SourceDeclaration::AccessControlLevel.from_doc(doc) declaration.line = doc['key.doc.line'] declaration.column = doc['key.doc.column'] declaration.start_line = doc['key.parsed_scope.start'] declaration.end_line = doc['key.parsed_scope.end'] declaration.deprecated = doc['key.always_deprecated'] declaration.unavailable = doc['key.always_unavailable'] declaration.generic_requirements = find_generic_requirements(doc['key.parsed_declaration']) inherited_types = doc['key.inheritedtypes'] || [] declaration.inherited_types = inherited_types.map { |type| type['key.name'] }.compact next unless make_doc_info(doc, declaration) declaration.children = make_substructure(doc, declaration) next if declaration.type.extension? && declaration.children.empty? && !declaration.inherited_types? declarations << declaration end declarations end # rubocop:enable Metrics/PerceivedComplexity # rubocop:enable Metrics/CyclomaticComplexity # rubocop:enable Metrics/MethodLength def self.find_generic_requirements(parsed_declaration) parsed_declaration =~ /\bwhere\s+(.*)$/m return nil unless Regexp.last_match Regexp.last_match[1].gsub(/\s+/, ' ') end # Expands extensions of nested types declared at the top level into # a tree so they can be deduplicated properly def self.expand_extensions(decls) decls.map do |decl| next decl unless decl.type.extension? && decl.name.include?('.') # Don't expand the Swift namespace if we're in ObjC mode. # ex: NS_SWIFT_NAME(Foo.Bar) should not create top-level Foo next decl if decl.swift_objc_extension? && !Config.instance.hide_objc? name_parts = decl.name.split('.') decl.name = name_parts.pop expand_extension(decl, name_parts, decls) end end def self.expand_extension(extension, name_parts, decls) return extension if name_parts.empty? name = name_parts.shift candidates = decls.select { |decl| decl.name == name } SourceDeclaration.new.tap do |decl| make_default_doc_info(decl) decl.name = name decl.modulename = extension.modulename decl.type = extension.type decl.mark = extension.mark decl.usr = candidates.first.usr unless candidates.empty? child = expand_extension(extension, name_parts, candidates.flat_map(&:children).uniq) child.parent_in_code = decl decl.children = [child] end end # Merges multiple extensions of the same entity into a single document. # # Merges extensions into the protocol/class/struct/enum they extend, if it # occurs in the same project. # # Merges redundant declarations when documenting podspecs. def self.deduplicate_declarations(declarations) duplicate_groups = declarations .group_by { |d| deduplication_key(d, declarations) } .values duplicate_groups.flat_map do |group| # Put extended type (if present) before extensions merge_declarations(group) end.compact end # Returns true if an Objective-C declaration is mergeable. def self.mergeable_objc?(decl, root_decls) decl.type.objc_class? \ || (decl.type.objc_category? \ && name_match(decl.objc_category_name[0], root_decls)) end # Returns if a Swift declaration is mergeable. # Start off merging in typealiases to help understand extensions. def self.mergeable_swift?(decl) decl.type.swift_extensible? || decl.type.swift_extension? || decl.type.swift_typealias? end # Two declarations get merged if they have the same deduplication key. def self.deduplication_key(decl, root_decls) # Swift extension of objc class if decl.swift_objc_extension? [decl.swift_extension_objc_name, :objc_class_and_categories] # Swift type or Swift extension of Swift type elsif mergeable_swift?(decl) [decl.usr, decl.name] # Objc categories and classes elsif mergeable_objc?(decl, root_decls) # Using the ObjC name to match swift_objc_extension. name, _ = decl.objc_category_name || decl.objc_name [name, :objc_class_and_categories] # Non-mergable declarations (funcs, typedefs etc...) else [decl.usr, decl.name, decl.type.kind] end end # rubocop:disable Metrics/MethodLength # Merges all of the given types and extensions into a single document. def self.merge_declarations(decls) extensions, typedecls = decls.partition { |d| d.type.extension? } if typedecls.size > 1 warn 'Found conflicting type declarations with the same name, which ' \ 'may indicate a build issue or a bug in Jazzy: ' + typedecls.map { |t| "#{t.type.name.downcase} #{t.name}" } .join(', ') end typedecl = typedecls.first extensions = reject_inaccessible_extensions(typedecl, extensions) if typedecl if typedecl.type.swift_protocol? mark_and_merge_protocol_extensions(typedecl, extensions) extensions.reject! { |ext| ext.children.empty? } end merge_objc_declaration_marks(typedecl, extensions) end # Keep type-aliases separate from any extensions if typedecl && typedecl.type.swift_typealias? [merge_type_and_extensions(typedecls, []), merge_type_and_extensions([], extensions)] else merge_type_and_extensions(typedecls, extensions) end end # rubocop:enable Metrics/MethodLength def self.merge_type_and_extensions(typedecls, extensions) # Constrained extensions at the end constrained, regular_exts = extensions.partition(&:constrained_extension?) decls = typedecls + regular_exts + constrained return nil if decls.empty? move_merged_extension_marks(decls) merge_code_declaration(decls) decls.first.tap do |merged| merged.children = deduplicate_declarations( decls.flat_map(&:children).uniq, ) merged.children.each do |child| child.parent_in_code = merged end end end # Now we know all the public types and all the private protocols, # reject extensions that add public protocols to private types # or add private protocols to public types. def self.reject_inaccessible_extensions(typedecl, extensions) swift_exts, objc_exts = extensions.partition(&:swift?) # Reject extensions that are just conformances to private protocols unwanted_exts, wanted_exts = swift_exts.partition do |ext| ext.children.empty? && !ext.other_inherited_types?(@inaccessible_protocols) end # Given extensions of a type from this module, without the # type itself, the type must be private and the extensions # should be rejected. if !typedecl && wanted_exts.first && wanted_exts.first.type_from_doc_module? unwanted_exts += wanted_exts wanted_exts = [] end # Don't tell the user to document them unwanted_exts.each { |e| @stats.remove_undocumented(e) } objc_exts + wanted_exts end # Protocol extensions. # # If any of the extensions provide default implementations for methods in # the given protocol, merge those members into the protocol doc instead of # keeping them on the extension. These get a “Default implementation” # annotation in the generated docs. Default implementations added by # conditional extensions are annotated but listed separately. # # Protocol methods provided only in an extension and not in the protocol # itself are a special beast: they do not use dynamic dispatch. These get an # “Extension method” annotation in the generated docs. def self.mark_and_merge_protocol_extensions(protocol, extensions) extensions.each do |ext| ext.children = ext.children.select do |ext_member| proto_member = protocol.children.find do |p| p.name == ext_member.name && p.type == ext_member.type end # Extension-only method, keep. unless proto_member ext_member.from_protocol_extension = true next true end # Default impl but constrained, mark and keep. if ext.constrained_extension? ext_member.default_impl_abstract = ext_member.abstract ext_member.abstract = nil next true end # Default impl for all users, merge. proto_member.default_impl_abstract = ext_member.abstract next false end end end # Mark children merged from categories with the name of category # (unless they already have a mark) def self.merge_objc_declaration_marks(typedecl, extensions) return unless typedecl.type.objc_class? extensions.each do |ext| _, category_name = ext.objc_category_name ext.children.each { |c| c.mark.name ||= category_name } end end # For each extension to be merged, move any MARK from the extension # declaration down to the extension contents so it still shows up. def self.move_merged_extension_marks(decls) return unless to_be_merged = decls[1..-1] to_be_merged.each do |ext| child = ext.children.first if child && child.mark.empty? child.mark.copy(ext.mark) end end end # Merge useful information added by extensions into the main # declaration: public protocol conformances and, for top-level extensions, # further conditional extensions of the same type. def self.merge_code_declaration(decls) first = decls.first declarations = decls[1..-1].select do |decl| decl.type.swift_extension? && (decl.other_inherited_types?(@inaccessible_protocols) || (first.type.swift_extension? && decl.constrained_extension?)) end.map(&:declaration) unless declarations.empty? first.declaration = declarations.prepend(first.declaration).uniq.join end end # Apply filtering based on the "included" and "excluded" flags. def self.filter_files(json) json = filter_included_files(json) if Config.instance.included_files.any? json = filter_excluded_files(json) if Config.instance.excluded_files.any? json.map do |doc| key = doc.keys.first doc[key] end.compact end # Filter based on the "included" flag. def self.filter_included_files(json) included_files = Config.instance.included_files json.map do |doc| key = doc.keys.first doc if included_files.detect do |include| File.fnmatch?(include, key) end end.compact end # Filter based on the "excluded" flag. def self.filter_excluded_files(json) excluded_files = Config.instance.excluded_files json.map do |doc| key = doc.keys.first doc unless excluded_files.detect do |exclude| File.fnmatch?(exclude, key) end end.compact end def self.name_match(name_part, docs) return nil unless name_part wildcard_expansion = Regexp.escape(name_part) .gsub('\.\.\.', '[^)]*') .gsub(/<.*>/, '') whole_name_pat = /\A#{wildcard_expansion}\Z/ docs.find do |doc| whole_name_pat =~ doc.name end end # Find the first ancestor of doc whose name matches name_part. def self.ancestor_name_match(name_part, doc) doc.namespace_ancestors.reverse_each do |ancestor| if match = name_match(name_part, ancestor.children) return match end end nil end def self.name_traversal(name_parts, doc) while doc && !name_parts.empty? next_part = name_parts.shift doc = name_match(next_part, doc.children) end doc end # Links recognized top-level declarations within # - inlined code within docs # - method signatures after they've been processed by the highlighter # # The `after_highlight` flag is used to differentiate between the two modes. def self.autolink_text(text, doc, root_decls, after_highlight = false) text.autolink_block(doc.url, '[^\s]+', after_highlight) do |raw_name| parts = raw_name.sub(/^@/, '') # ignore for custom attribute ref .split(/(?