diff --git a/gapic-generator/lib/gapic/formatting_utils.rb b/gapic-generator/lib/gapic/formatting_utils.rb index b6627829b..3925843ae 100644 --- a/gapic-generator/lib/gapic/formatting_utils.rb +++ b/gapic-generator/lib/gapic/formatting_utils.rb @@ -21,16 +21,23 @@ module Gapic # Various string formatting utils # module FormattingUtils - @brace_detector = /\A(?
[^`]*(?:`[^`]*`[^`]*)*[^`\\])?\{(?[^\s][^}]*)\}(?.*)\z/m
     @xref_detector = /\A(?
[^`]*(?:`[^`]*`[^`]*)*)?\[(?[\w. `-]+)\]\[(?[\w.]+)\](?.*)\z/m
     @list_element_detector = /\A\s*(?:\*|\+|-|[0-9a-zA-Z]+\.)\s/
     @omit_lines = ["@InputOnly\n", "@OutputOnly\n"]
+    # Built-in YARD meta-data tags as documented in:
+    # https://rubydoc.info/gems/yard/file/docs/Tags.md#Tag_List
+    @known_yard_tags = [
+      "abstract", "api", "attr", "attr_reader", "attr_writer", "author", "deprecated", "example",
+      "note", "option", "overload", "param", "private", "raise", "return", "see", "since", "todo",
+      "version", "yield", "yieldparam", "yieldreturn"
+    ].freeze
 
     class << self
       ##
       # Given an enumerable of lines, performs yardoc formatting, including:
       # * Interpreting cross-references identified as described in AIP 192
       # * Escaping literal braces that look like yardoc type links
+      # * Backticking unknown doc tags so they are not parsed as YARD tags
       #
       # Tries to be smart about exempting preformatted text blocks.
       #
@@ -45,23 +52,27 @@ class << self
       #
       def format_doc_lines api, lines, disable_xrefs: false, transport: nil
         transport ||= api&.default_transport || :grpc
-        # To detect preformatted blocks, this tracks the "expected" base indent
-        # according to Markdown. Specifically, this is the effective indent of
-        # previous block, which is normally 0 except if we're in a list item.
-        # Then, if a block is indented at least 4 spaces past that expected
-        # indent (and as long as it remains so), those lines are considered
-        # preformatted.
+        # Tracks fenced blocks, multiline inline code spans, and indented code blocks.
+        in_fence = false
+        in_code_span = false
         in_block = nil
         base_indent = 0
         (lines - @omit_lines).map do |line|
-          indent = line_indent line
-          if indent.nil?
+          if line =~ /^\s*(?:```|~~~)/
+            in_fence = !in_fence
+            in_code_span = false
             in_block = nil
-          else
-            in_block, base_indent = update_indent_state in_block, base_indent, line, indent
-            if in_block == false
-              line = escape_line_braces line
-              line = format_line_xrefs api, line, disable_xrefs, transport
+          elsif !in_fence
+            indent = line_indent line
+            if indent.nil?
+              in_block = nil
+              in_code_span = false
+            else
+              in_block, base_indent = update_indent_state in_block, base_indent, line, indent
+              if in_block == false
+                line, in_code_span = format_line_content line, in_code_span
+                line = format_line_xrefs api, line, disable_xrefs, transport
+              end
             end
           end
           line
@@ -106,11 +117,40 @@ def line_indent line
         m[1].length
       end
 
-      def escape_line_braces line
-        while (m = @brace_detector.match line)
-          line = "#{m[:pre]}\\\\{#{m[:inside]}}#{m[:post]}"
+      def format_line_content line, in_code_span
+        parts = line.split("`", -1)
+        formatted_parts = parts.each_with_index.map do |part, idx|
+          if in_code_span
+            in_code_span = false if idx < parts.length - 1
+            part
+          else
+            is_followed_by_backtick = idx < parts.length - 1
+            in_code_span = true if is_followed_by_backtick
+            formatted = escape_prose_braces part, is_followed_by_backtick: is_followed_by_backtick
+            sanitize_prose_tags formatted
+          end
+        end
+        [formatted_parts.join("`"), in_code_span]
+      end
+
+      def escape_prose_braces text, is_followed_by_backtick: false
+        # Matches unescaped `{` outside backtick spans followed by non-whitespace.
+        # If `{` is at the end of a non-code chunk (is_followed_by_backtick: true), it is followed
+        # immediately by a backticked code span (starting with a non-whitespace backtick),
+        # so \z (end of string) is also matched.
+        pattern = is_followed_by_backtick ? /(?