Keep nested keep-sorted blocks together in the same group, regardless of indentation level. (#19)

diff --git a/goldens/group.in b/goldens/group.in
index d2c7ac7..544db49 100644
--- a/goldens/group.in
+++ b/goldens/group.in
@@ -137,3 +137,22 @@
   // keep-sorted-test end
   ];
 // keep-sorted-test end
+
+Nested keep-sorted without indentation
+// keep-sorted-test start group=yes newline_separated=yes
+
+// def
+// keep-sorted-test start
+3
+1
+2
+// keep-sorted-test end
+
+// abc
+// keep-sorted-test start
+b
+c
+a
+// keep-sorted-test end
+
+// keep-sorted-test end
diff --git a/goldens/group.out b/goldens/group.out
index abd5eda..94e065c 100644
--- a/goldens/group.out
+++ b/goldens/group.out
@@ -138,3 +138,21 @@
   // keep-sorted-test end
   ];
 // keep-sorted-test end
+
+Nested keep-sorted without indentation
+// keep-sorted-test start group=yes newline_separated=yes
+// def
+// keep-sorted-test start
+1
+2
+3
+// keep-sorted-test end
+
+// abc
+// keep-sorted-test start
+a
+b
+c
+// keep-sorted-test end
+
+// keep-sorted-test end
diff --git a/keepsorted/block.go b/keepsorted/block.go
index 5065f85..d75991e 100644
--- a/keepsorted/block.go
+++ b/keepsorted/block.go
@@ -24,7 +24,7 @@
 )
 
 type block struct {
-	opts blockOptions
+	metadata blockMetadata
 
 	start, end int
 	// lines are the content of this block from the original file.
@@ -40,6 +40,11 @@
 	nestedBlocks []block
 }
 
+type blockMetadata struct {
+	startDirective, endDirective string
+	opts                         blockOptions
+}
+
 type incompleteBlock struct {
 	line int
 	dir  directive
@@ -114,7 +119,11 @@
 			// directives will have depth >= 1 based on how deep it is.
 			depth := len(starts)
 			block := block{
-				opts:  opts,
+				metadata: blockMetadata{
+					startDirective: f.startDirective,
+					endDirective:   f.endDirective,
+					opts:           opts,
+				},
 				start: start.index + offset,
 				end:   endIndex + offset,
 				lines: lines[start.index+1 : endIndex],
@@ -223,8 +232,8 @@
 		}
 	}
 
-	groups := groupLines(lines, b.opts)
-	log.Printf("%d groups for block at index %d are (options %#v)", len(groups), b.start, b.opts)
+	groups := groupLines(lines, b.metadata)
+	log.Printf("%d groups for block at index %d are (options %#v)", len(groups), b.start, b.metadata.opts)
 	for _, lg := range groups {
 		log.Printf("%#v", lg)
 	}
@@ -232,7 +241,7 @@
 	trimTrailingComma := handleTrailingComma(groups)
 
 	wasNewlineSeparated := true
-	if b.opts.NewlineSeparated {
+	if b.metadata.opts.NewlineSeparated {
 		wasNewlineSeparated = isNewlineSeparated(groups)
 		var withoutNewlines []lineGroup
 		for _, lg := range groups {
@@ -245,7 +254,7 @@
 	}
 
 	removedDuplicate := false
-	if b.opts.RemoveDuplicates {
+	if b.metadata.opts.RemoveDuplicates {
 		seen := map[string]bool{}
 		var deduped []lineGroup
 		for _, lg := range groups {
@@ -270,7 +279,7 @@
 
 	trimTrailingComma(groups)
 
-	if b.opts.NewlineSeparated {
+	if b.metadata.opts.NewlineSeparated {
 		var separated []lineGroup
 		newline := lineGroup{lines: []string{""}}
 		for _, lg := range groups {
@@ -375,8 +384,8 @@
 	// An empty prefix can be used to move all remaining entries to a position
 	// between other prefixes.
 	var prefixWeights []prefixWeight
-	for i, p := range b.opts.PrefixOrder {
-		prefixWeights = append(prefixWeights, prefixWeight{p, i - len(b.opts.PrefixOrder)})
+	for i, p := range b.metadata.opts.PrefixOrder {
+		prefixWeights = append(prefixWeights, prefixWeight{p, i - len(b.metadata.opts.PrefixOrder)})
 	}
 	slices.SortStableFunc(prefixWeights, func(a, b prefixWeight) int {
 		return cmp.Compare(b.prefix, a.prefix)
@@ -411,13 +420,13 @@
 	//   foo_123
 	transformOrder := comparingPropertyWith(func(lg lineGroup) numericTokens {
 		l := lg.joinedLines()
-		if s, ok := b.opts.removeIgnorePrefix(l); ok {
+		if s, ok := b.metadata.opts.removeIgnorePrefix(l); ok {
 			l = s
 		}
-		if !b.opts.CaseSensitive {
+		if !b.metadata.opts.CaseSensitive {
 			l = strings.ToLower(l)
 		}
-		return b.opts.maybeParseNumeric(l)
+		return b.metadata.opts.maybeParseNumeric(l)
 	}, numericTokens.compare)
 
 	return func(a, b lineGroup) int {
diff --git a/keepsorted/keep_sorted.go b/keepsorted/keep_sorted.go
index 8f6ad11..4196d58 100644
--- a/keepsorted/keep_sorted.go
+++ b/keepsorted/keep_sorted.go
@@ -128,7 +128,7 @@
 
 	var fs []*Finding
 	for _, b := range blocks {
-		if considerLintOption && !b.opts.Lint {
+		if considerLintOption && !b.metadata.opts.Lint {
 			continue
 		}
 		if s, alreadySorted := b.sorted(); !alreadySorted {
diff --git a/keepsorted/keep_sorted_test.go b/keepsorted/keep_sorted_test.go
index 038299e..5521046 100644
--- a/keepsorted/keep_sorted_test.go
+++ b/keepsorted/keep_sorted_test.go
@@ -39,6 +39,12 @@
 		CaseSensitive:    true,
 		commentMarker:    "//",
 	}
+
+	defaultMetadata = blockMetadata{
+		startDirective: "keep-sorted-test start",
+		endDirective:   "keep-sorted-test end",
+		opts:           defaultOptions,
+	}
 )
 
 // initZerolog initializes zerolog to log as part of the test.
@@ -55,6 +61,12 @@
 	return opts
 }
 
+func defaultMetadataWith(opts blockOptions) blockMetadata {
+	meta := defaultMetadata
+	meta.opts = opts
+	return meta
+}
+
 func TestFix(t *testing.T) {
 	for _, tc := range []struct {
 		name string
@@ -348,9 +360,9 @@
 
 			wantBlocks: []block{
 				{
-					opts:  defaultOptions,
-					start: 3,
-					end:   7,
+					metadata: defaultMetadata,
+					start:    3,
+					end:      7,
 					lines: []string{
 						"c",
 						"b",
@@ -358,9 +370,9 @@
 					},
 				},
 				{
-					opts:  defaultOptions,
-					start: 9,
-					end:   13,
+					metadata: defaultMetadata,
+					start:    9,
+					end:      13,
 					lines: []string{
 						"1",
 						"2",
@@ -385,9 +397,9 @@
 
 			wantBlocks: []block{
 				{
-					opts:  defaultOptions,
-					start: 5,
-					end:   7,
+					metadata: defaultMetadata,
+					start:    5,
+					end:      7,
 					lines: []string{
 						"baz",
 					},
@@ -423,9 +435,9 @@
 
 			wantBlocks: []block{
 				{
-					opts:  defaultOptions,
-					start: 3,
-					end:   7,
+					metadata: defaultMetadata,
+					start:    3,
+					end:      7,
 					lines: []string{
 						"c",
 						"b",
@@ -451,9 +463,9 @@
 
 			wantBlocks: []block{
 				{
-					opts:  defaultOptions,
-					start: 1,
-					end:   6,
+					metadata: defaultMetadata,
+					start:    1,
+					end:      6,
 					lines: []string{
 						"",
 						"1",
@@ -484,9 +496,9 @@
 
 			wantBlocks: []block{
 				{
-					opts:  defaultOptions,
-					start: 1,
-					end:   13,
+					metadata: defaultMetadata,
+					start:    1,
+					end:      13,
 					lines: []string{
 						"a",
 						"b",
@@ -502,9 +514,9 @@
 					},
 					nestedBlocks: []block{
 						{
-							opts:  defaultOptions,
-							start: 5,
-							end:   9,
+							metadata: defaultMetadata,
+							start:    5,
+							end:      9,
 							lines: []string{
 								"d",
 								"e",
@@ -562,9 +574,9 @@
 
 			wantBlocks: []block{
 				{
-					opts:  defaultOptions,
-					start: 1,
-					end:   34,
+					metadata: defaultMetadata,
+					start:    1,
+					end:      34,
 					lines: []string{
 						"0.1",
 						"0.2",
@@ -601,9 +613,9 @@
 					},
 					nestedBlocks: []block{
 						{
-							opts:  defaultOptions,
-							start: 5,
-							end:   30,
+							metadata: defaultMetadata,
+							start:    5,
+							end:      30,
 							lines: []string{
 								"1.1",
 								"1.2",
@@ -632,9 +644,9 @@
 							},
 							nestedBlocks: []block{
 								{
-									opts:  defaultOptions,
-									start: 9,
-									end:   21,
+									metadata: defaultMetadata,
+									start:    9,
+									end:      21,
 									lines: []string{
 										"2.1",
 										"2.2",
@@ -650,9 +662,9 @@
 									},
 									nestedBlocks: []block{
 										{
-											opts:  defaultOptions,
-											start: 13,
-											end:   17,
+											metadata: defaultMetadata,
+											start:    13,
+											end:      17,
 											lines: []string{
 												"3.1",
 												"3.2",
@@ -662,9 +674,9 @@
 									},
 								},
 								{
-									opts:  defaultOptions,
-									start: 22,
-									end:   26,
+									metadata: defaultMetadata,
+									start:    22,
+									end:      26,
 									lines: []string{
 										"4.1",
 										"4.2",
@@ -676,9 +688,9 @@
 					},
 				},
 				{
-					opts:  defaultOptions,
-					start: 35,
-					end:   39,
+					metadata: defaultMetadata,
+					start:    35,
+					end:      39,
 					lines: []string{
 						"5.1",
 						"5.2",
@@ -702,10 +714,10 @@
 
 			wantBlocks: []block{
 				{
-					opts:  defaultOptions,
-					start: 5,
-					end:   7,
-					lines: []string{"2"},
+					metadata: defaultMetadata,
+					start:    5,
+					end:      7,
+					lines:    []string{"2"},
 				},
 			},
 			wantIncompleteBlocks: []incompleteBlock{
@@ -721,7 +733,7 @@
 			}
 
 			gotBlocks, gotIncompleteBlocks := New("keep-sorted-test").newBlocks(strings.Split(tc.in, "\n"), 0, tc.include)
-			if diff := cmp.Diff(tc.wantBlocks, gotBlocks, cmp.AllowUnexported(block{}), cmp.AllowUnexported(blockOptions{})); diff != "" {
+			if diff := cmp.Diff(tc.wantBlocks, gotBlocks, cmp.AllowUnexported(block{}, blockMetadata{}, blockOptions{})); diff != "" {
 				t.Errorf("blocks diff (-want +got):\n%s", diff)
 			}
 			if diff := cmp.Diff(tc.wantIncompleteBlocks, gotIncompleteBlocks, cmp.AllowUnexported(incompleteBlock{})); diff != "" {
@@ -1144,7 +1156,7 @@
 	} {
 		t.Run(tc.name, func(t *testing.T) {
 			initZerolog(t)
-			got, gotAlreadySorted := block{lines: tc.in, opts: tc.opts}.sorted()
+			got, gotAlreadySorted := block{lines: tc.in, metadata: defaultMetadataWith(tc.opts)}.sorted()
 			if gotAlreadySorted != tc.wantAlreadySorted {
 				t.Errorf("alreadySorted mismatch: got %t want %t", gotAlreadySorted, tc.wantAlreadySorted)
 			}
@@ -1226,6 +1238,7 @@
 			name: "Group",
 			opts: defaultOptionsWith(func(opts *blockOptions) {
 				opts.Group = true
+				opts.Block = false
 			}),
 
 			want: []lineGroup{
@@ -1242,6 +1255,7 @@
 			name: "Group_UnindentedNewlines",
 			opts: defaultOptionsWith(func(opts *blockOptions) {
 				opts.Group = true
+				opts.Block = false
 			}),
 
 			want: []lineGroup{
@@ -1262,6 +1276,34 @@
 			},
 		},
 		{
+			name: "Group_NestedKeepSortedBlocksWithoutAnyIndentation",
+			opts: defaultOptionsWith(func(opts *blockOptions) {
+				opts.Group = true
+				opts.Block = false
+			}),
+
+			want: []lineGroup{
+				{[]string{
+					"// def",
+				}, []string{
+					"// keep-sorted-test start",
+					"3",
+					"1",
+					"2",
+					"// keep-sorted-test end",
+				}},
+				{[]string{
+					"// abc",
+				}, []string{
+					"// keep-sorted-test start",
+					"b",
+					"c",
+					"a",
+					"// keep-sorted-test end",
+				}},
+			},
+		},
+		{
 			name: "Block_Brackets",
 			opts: defaultOptionsWith(func(opts *blockOptions) {
 				opts.Block = true
@@ -1440,7 +1482,7 @@
 				in = append(in, lg.lines...)
 			}
 
-			got := groupLines(in, tc.opts)
+			got := groupLines(in, defaultMetadataWith(tc.opts))
 			if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(lineGroup{})); diff != "" {
 				t.Errorf("groupLines mismatch (-want +got):\n%s", diff)
 			}
diff --git a/keepsorted/line_group.go b/keepsorted/line_group.go
index 43c7682..5d56c56 100644
--- a/keepsorted/line_group.go
+++ b/keepsorted/line_group.go
@@ -30,23 +30,44 @@
 }
 
 // groupLines splits lines into one or more lineGroups based on the provided options.
-func groupLines(lines []string, opts blockOptions) []lineGroup {
+func groupLines(lines []string, metadata blockMetadata) []lineGroup {
 	var groups []lineGroup
+	// Tracks which subsection of lines contains the comments for the current lineGroup.
 	var commentRange indexRange
+	// Tracks which subsection of lines contains the content for the current lineGroup.
 	var lineRange indexRange
+
+	// group=yes and block=no, these pieces of information are used to determine
+	// when we group lines together into a single group.
+
+	// Indent: All lines indented further than the first line are grouped together.
+	// Edge case: Whitespace-only lines are included in the group based on the
+	// indentation of the next non-empty line after the whitespace-only line.
 	var indents []int
 	var initialIndent *int
+	// Counts the number of unmatched start directives we've seen in the current group.
+	// We will include entire keep-sorted blocks as grouped lines to avoid
+	// breaking nested keep-sorted blocks that don't have indentation.
+	var numUnmatchedStartDirectives int
+
+	// block=yes: The code block that we're constructing until we have matched braces and quotations.
 	var block codeBlock
 
-	if opts.Group {
+	if metadata.opts.Group {
 		indents = calculateIndents(lines)
 	}
 
 	// append a line to both lineRange, and block, if necessary.
 	appendLine := func(i int, l string) {
 		lineRange.append(i)
-		if opts.Block {
-			block.append(l, opts)
+		if metadata.opts.Block {
+			block.append(l, metadata.opts)
+		} else if metadata.opts.Group {
+			if strings.Contains(l, metadata.startDirective) {
+				numUnmatchedStartDirectives++
+			} else if strings.Contains(l, metadata.endDirective) {
+				numUnmatchedStartDirectives--
+			}
 		}
 	}
 	// finish an outstanding lineGroup and reset our state to prepare for a new lineGroup.
@@ -57,21 +78,27 @@
 		block = codeBlock{}
 	}
 	for i, l := range lines {
-		if opts.Block && !lineRange.empty() && block.expectsContinuation() {
+		if metadata.opts.Block && !lineRange.empty() && block.expectsContinuation() {
 			appendLine(i, l)
-		} else if opts.Group && !lineRange.empty() && initialIndent != nil && indents[i] > *initialIndent {
+		} else if metadata.opts.Group && (!lineRange.empty() && initialIndent != nil && indents[i] > *initialIndent || numUnmatchedStartDirectives > 0) {
 			appendLine(i, l)
-		} else if opts.hasStickyPrefix(l) {
+		} else if metadata.opts.hasStickyPrefix(l) {
 			if !lineRange.empty() {
 				finishGroup()
 			}
 
-			commentRange.append(i)
+			if !metadata.opts.Block && metadata.opts.Group && strings.Contains(l, metadata.startDirective) {
+				// We don't need to check for end directives here because this makes
+				// numUnmatchedStartDirectives > 0, so we'll take the code path above through appendLine.
+				appendLine(i, l)
+			} else {
+				commentRange.append(i)
+			}
 		} else {
 			if !lineRange.empty() {
 				finishGroup()
 			}
-			if opts.Group && initialIndent == nil {
+			if metadata.opts.Group && initialIndent == nil {
 				initialIndent = &indents[i]
 			}
 			appendLine(i, l)