From 3a7d1ea013e06d7801d3b72c343f29cf28a63e8d Mon Sep 17 00:00:00 2001 From: Tomo Suzuki Date: Mon, 31 Aug 2026 20:55:07 +0000 Subject: [PATCH 1/7] fix(generator): escape multi-line braces and backtick unknown yard tags --- gapic-generator/lib/gapic/formatting_utils.rb | 33 ++++++-- .../test/gapic/formatting_utils_test.rb | 76 ++++++++++++++++++- 2 files changed, 103 insertions(+), 6 deletions(-) diff --git a/gapic-generator/lib/gapic/formatting_utils.rb b/gapic-generator/lib/gapic/formatting_utils.rb index b6627829b..63cadf72d 100644 --- a/gapic-generator/lib/gapic/formatting_utils.rb +++ b/gapic-generator/lib/gapic/formatting_utils.rb @@ -21,16 +21,20 @@ 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"]
+    @known_yard_tags = [
+      "abstract", "api", "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.
       #
@@ -61,6 +65,7 @@ def format_doc_lines api, lines, disable_xrefs: false, transport: nil
             in_block, base_indent = update_indent_state in_block, base_indent, line, indent
             if in_block == false
               line = escape_line_braces line
+              line = sanitize_line_tags line
               line = format_line_xrefs api, line, disable_xrefs, transport
             end
           end
@@ -107,10 +112,28 @@ def line_indent line
       end
 
       def escape_line_braces line
-        while (m = @brace_detector.match line)
-          line = "#{m[:pre]}\\\\{#{m[:inside]}}#{m[:post]}"
-        end
-        line
+        parts = line.split(/(`[^`]*`)/)
+        parts.map.with_index do |part, idx|
+          if idx.even?
+            part.gsub(/(?
Date: Mon, 31 Aug 2026 21:12:53 +0000
Subject: [PATCH 2/7] fix(generator): handle brace escaping immediately
 followed by code spans

---
 gapic-generator/lib/gapic/formatting_utils.rb       | 3 ++-
 gapic-generator/test/gapic/formatting_utils_test.rb | 9 +++++++++
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/gapic-generator/lib/gapic/formatting_utils.rb b/gapic-generator/lib/gapic/formatting_utils.rb
index 63cadf72d..ffbbb7156 100644
--- a/gapic-generator/lib/gapic/formatting_utils.rb
+++ b/gapic-generator/lib/gapic/formatting_utils.rb
@@ -115,7 +115,8 @@ def escape_line_braces line
         parts = line.split(/(`[^`]*`)/)
         parts.map.with_index do |part, idx|
           if idx.even?
-            part.gsub(/(?
Date: Tue, 1 Sep 2026 18:53:03 +0000
Subject: [PATCH 3/7] docs(generator): add explanatory comments above regexes
 in formatting utils

---
 gapic-generator/lib/gapic/formatting_utils.rb | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/gapic-generator/lib/gapic/formatting_utils.rb b/gapic-generator/lib/gapic/formatting_utils.rb
index ffbbb7156..93145c780 100644
--- a/gapic-generator/lib/gapic/formatting_utils.rb
+++ b/gapic-generator/lib/gapic/formatting_utils.rb
@@ -112,9 +112,14 @@ def line_indent line
       end
 
       def escape_line_braces line
+        # Tokenize by backticks so inline code spans (e.g. `foo {bar}`) are preserved in odd indices.
         parts = line.split(/(`[^`]*`)/)
         parts.map.with_index do |part, idx|
           if idx.even?
+            # Matches unescaped `{` outside backtick spans followed by non-whitespace.
+            # If `{` is at the end of a non-code chunk (idx < parts.length - 1), it is followed
+            # immediately by a backticked code span (starting with a non-whitespace backtick),
+            # so \z is also matched.
             pattern = idx < parts.length - 1 ? /(?
Date: Tue, 1 Sep 2026 18:56:37 +0000
Subject: [PATCH 4/7] docs(generator): clarify regex comments and document
 known YARD tags source

---
 gapic-generator/lib/gapic/formatting_utils.rb | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/gapic-generator/lib/gapic/formatting_utils.rb b/gapic-generator/lib/gapic/formatting_utils.rb
index 93145c780..f8155fcce 100644
--- a/gapic-generator/lib/gapic/formatting_utils.rb
+++ b/gapic-generator/lib/gapic/formatting_utils.rb
@@ -24,6 +24,8 @@ module FormattingUtils
     @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://github.com/lsegal/yard/blob/main/docs/Tags.md#tag-list
     @known_yard_tags = [
       "abstract", "api", "author", "deprecated", "example", "note", "option", "overload", "param",
       "private", "raise", "return", "see", "since", "todo", "version", "yield", "yieldparam", "yieldreturn"
@@ -119,7 +121,7 @@ def escape_line_braces line
             # Matches unescaped `{` outside backtick spans followed by non-whitespace.
             # If `{` is at the end of a non-code chunk (idx < parts.length - 1), it is followed
             # immediately by a backticked code span (starting with a non-whitespace backtick),
-            # so \z is also matched.
+            # so \z (end of string) is also matched.
             pattern = idx < parts.length - 1 ? /(?
Date: Tue, 1 Sep 2026 19:09:08 +0000
Subject: [PATCH 5/7] feat(generator): add attr, attr_reader, attr_writer to
 known YARD tags and update doc URL

---
 gapic-generator/lib/gapic/formatting_utils.rb       | 7 ++++---
 gapic-generator/test/gapic/formatting_utils_test.rb | 6 ++++++
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/gapic-generator/lib/gapic/formatting_utils.rb b/gapic-generator/lib/gapic/formatting_utils.rb
index f8155fcce..e22f780d9 100644
--- a/gapic-generator/lib/gapic/formatting_utils.rb
+++ b/gapic-generator/lib/gapic/formatting_utils.rb
@@ -25,10 +25,11 @@ module FormattingUtils
     @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://github.com/lsegal/yard/blob/main/docs/Tags.md#tag-list
+    # https://rubydoc.info/gems/yard/file/docs/Tags.md#Tag_List
     @known_yard_tags = [
-      "abstract", "api", "author", "deprecated", "example", "note", "option", "overload", "param",
-      "private", "raise", "return", "see", "since", "todo", "version", "yield", "yieldparam", "yieldreturn"
+      "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
diff --git a/gapic-generator/test/gapic/formatting_utils_test.rb b/gapic-generator/test/gapic/formatting_utils_test.rb
index b40d5dd26..7c37bf310 100644
--- a/gapic-generator/test/gapic/formatting_utils_test.rb
+++ b/gapic-generator/test/gapic/formatting_utils_test.rb
@@ -574,6 +574,9 @@ def test_dont_sanitize_known_yard_tags
       "@return [Integer]\n",
       "@deprecated Do not use\n",
       "@see http://example.com\n",
+      "@attr [String] name description\n",
+      "@attr_reader [String] name description\n",
+      "@attr_writer [String] name description\n",
       "@!attribute [rw] foo\n"
     ]
     assert_equal [
@@ -581,6 +584,9 @@ def test_dont_sanitize_known_yard_tags
       "@return [Integer]\n",
       "@deprecated Do not use\n",
       "@see http://example.com\n",
+      "@attr [String] name description\n",
+      "@attr_reader [String] name description\n",
+      "@attr_writer [String] name description\n",
       "@!attribute [rw] foo\n"
     ], result
   end

From 28efb738ca1b4149c9b9c2e6019909a479152977 Mon Sep 17 00:00:00 2001
From: Tomo Suzuki 
Date: Tue, 1 Sep 2026 20:39:45 +0000
Subject: [PATCH 6/7] fix(generator): preserve fenced code blocks without
 escaping braces or tags

---
 gapic-generator/lib/gapic/formatting_utils.rb | 35 ++++++++------
 .../test/gapic/formatting_utils_test.rb       | 46 +++++++++++++++++++
 2 files changed, 68 insertions(+), 13 deletions(-)

diff --git a/gapic-generator/lib/gapic/formatting_utils.rb b/gapic-generator/lib/gapic/formatting_utils.rb
index e22f780d9..5523db41b 100644
--- a/gapic-generator/lib/gapic/formatting_utils.rb
+++ b/gapic-generator/lib/gapic/formatting_utils.rb
@@ -52,24 +52,33 @@ 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.
+        # To detect preformatted blocks, this tracks:
+        # 1. Fenced code blocks (delimited by ``` or ~~~)
+        # 2. Indented code blocks according to Markdown. Specifically, this is the
+        #    effective indent of previous block, which is normally 0 except if we're
+        #    in a list item. 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.
+        in_fence = 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_block = nil
+          elsif in_fence
+            # Preformatted code inside fence; do not format
           else
-            in_block, base_indent = update_indent_state in_block, base_indent, line, indent
-            if in_block == false
-              line = escape_line_braces line
-              line = sanitize_line_tags line
-              line = format_line_xrefs api, line, disable_xrefs, transport
+            indent = line_indent line
+            if indent.nil?
+              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 = sanitize_line_tags line
+                line = format_line_xrefs api, line, disable_xrefs, transport
+              end
             end
           end
           line
diff --git a/gapic-generator/test/gapic/formatting_utils_test.rb b/gapic-generator/test/gapic/formatting_utils_test.rb
index 7c37bf310..66b0c6a8a 100644
--- a/gapic-generator/test/gapic/formatting_utils_test.rb
+++ b/gapic-generator/test/gapic/formatting_utils_test.rb
@@ -617,4 +617,50 @@ def test_escape_braces_followed_by_backtick
       "must be one of \\\\{`training`, `validation`, `test`}, and it defines\n"
     ], result
   end
+
+  def test_fenced_code_block_preserves_braces_and_tags
+    lines = [
+      "For example, the following JSON creates a divider:\n",
+      "\n",
+      "```\n",
+      "\"divider\": {}\n",
+      "Hello @FooBot how are you!\n",
+      "```\n",
+      "\n",
+      "Choose from {100, 200, 300}.\n"
+    ]
+    result = Gapic::FormattingUtils.format_doc_lines nil, lines
+    assert_equal [
+      "For example, the following JSON creates a divider:\n",
+      "\n",
+      "```\n",
+      "\"divider\": {}\n",
+      "Hello @FooBot how are you!\n",
+      "```\n",
+      "\n",
+      "Choose from \\\\{100, 200, 300}.\n"
+    ], result
+  end
+
+  def test_fenced_code_block_with_language_tag
+    lines = [
+      "Example with language:\n",
+      "```json\n",
+      "{\"name\": \"app\", \"ports\": [{8080}]}\n",
+      "```\n"
+    ]
+    result = Gapic::FormattingUtils.format_doc_lines nil, lines
+    assert_equal lines, result
+  end
+
+  def test_fenced_code_block_with_tilde
+    lines = [
+      "Example with tilde:\n",
+      "~~~\n",
+      "{\"name\": \"app\", \"ports\": [{8080}]}\n",
+      "~~~\n"
+    ]
+    result = Gapic::FormattingUtils.format_doc_lines nil, lines
+    assert_equal lines, result
+  end
 end

From 6243c237cc1ed4f45680862623f98e32138995b8 Mon Sep 17 00:00:00 2001
From: Tomo Suzuki 
Date: Wed, 2 Sep 2026 16:01:43 +0000
Subject: [PATCH 7/7] fix(generator): preserve multiline inline backtick code
 spans

---
 gapic-generator/lib/gapic/formatting_utils.rb | 78 +++++++++----------
 .../test/gapic/formatting_utils_test.rb       | 60 ++++++++++++++
 2 files changed, 96 insertions(+), 42 deletions(-)

diff --git a/gapic-generator/lib/gapic/formatting_utils.rb b/gapic-generator/lib/gapic/formatting_utils.rb
index 5523db41b..3925843ae 100644
--- a/gapic-generator/lib/gapic/formatting_utils.rb
+++ b/gapic-generator/lib/gapic/formatting_utils.rb
@@ -52,31 +52,25 @@ class << self
       #
       def format_doc_lines api, lines, disable_xrefs: false, transport: nil
         transport ||= api&.default_transport || :grpc
-        # To detect preformatted blocks, this tracks:
-        # 1. Fenced code blocks (delimited by ``` or ~~~)
-        # 2. Indented code blocks according to Markdown. Specifically, this is the
-        #    effective indent of previous block, which is normally 0 except if we're
-        #    in a list item. 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|
           if line =~ /^\s*(?:```|~~~)/
             in_fence = !in_fence
+            in_code_span = false
             in_block = nil
-          elsif in_fence
-            # Preformatted code inside fence; do not format
-          else
+          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 = escape_line_braces line
-                line = sanitize_line_tags line
+                line, in_code_span = format_line_content line, in_code_span
                 line = format_line_xrefs api, line, disable_xrefs, transport
               end
             end
@@ -123,40 +117,40 @@ def line_indent line
         m[1].length
       end
 
-      def escape_line_braces line
-        # Tokenize by backticks so inline code spans (e.g. `foo {bar}`) are preserved in odd indices.
-        parts = line.split(/(`[^`]*`)/)
-        parts.map.with_index do |part, idx|
-          if idx.even?
-            # Matches unescaped `{` outside backtick spans followed by non-whitespace.
-            # If `{` is at the end of a non-code chunk (idx < parts.length - 1), it is followed
-            # immediately by a backticked code span (starting with a non-whitespace backtick),
-            # so \z (end of string) is also matched.
-            pattern = idx < parts.length - 1 ? /(?