From 873fa88a3bfdea45dcbeb23104c4ec726d9c4a4e Mon Sep 17 00:00:00 2001 From: Thibault Richard Date: Wed, 24 Dec 2025 07:57:47 +0000 Subject: [PATCH 1/7] (fix): Add support for +required marker The +required marker is now recognized and produces the same "Required: {}" output as +kubebuilder:validation:Required. Assisted-by: CLAUDE --- processor/processor.go | 6 ++++++ test/api/v1/guestbook_types.go | 6 ++++++ test/expected.asciidoc | 4 ++++ test/expected.md | 2 ++ 4 files changed, 18 insertions(+) diff --git a/processor/processor.go b/processor/processor.go index 6939e4d..76c8905 100644 --- a/processor/processor.go +++ b/processor/processor.go @@ -554,6 +554,12 @@ func parseMarkers(markers markers.MarkerValues) (string, []string) { validation = append(validation, fmt.Sprintf("%s: %v", name, value)) } + // Handle standalone +required marker + // This is equivalent to +kubebuilder:validation:Required + if name == "required" { + validation = append(validation, "Required: {}") + } + if name == "kubebuilder:default" { if value, ok := value.(crdmarkers.Default); ok { defaultValue = fmt.Sprintf("%v", value.Value) diff --git a/test/api/v1/guestbook_types.go b/test/api/v1/guestbook_types.go index 5dbdcb0..78fd520 100644 --- a/test/api/v1/guestbook_types.go +++ b/test/api/v1/guestbook_types.go @@ -146,6 +146,12 @@ type GuestbookEntry struct { Comment string `json:"comment,omitempty"` // Rating provided by the guest Rating Rating `json:"rating,omitempty"` + // Email is the email address of the guest (required field using +required marker) + // +required + Email string `json:"email"` + // Location is the location of the guest (required field using +kubebuilder:validation:Required marker) + // +kubebuilder:validation:Required + Location string `json:"location"` } // GuestbookStatus defines the observed state of Guestbook. diff --git a/test/expected.asciidoc b/test/expected.asciidoc index a5a642e..5cfdceb 100644 --- a/test/expected.asciidoc +++ b/test/expected.asciidoc @@ -179,6 +179,10 @@ Looks good? + | | Pattern: `0\*[a-z0-9]*[a-z]\*[0-9]*|\s` + | *`rating`* __xref:{anchor_prefix}-github-com-elastic-crd-ref-docs-api-v1-rating[$$Rating$$]__ | Rating provided by the guest + | | Maximum: 5 + Minimum: 1 + +| *`email`* __string__ | Email is the email address of the guest (required field using +required marker) + | | Required: \{} + + +| *`location`* __string__ | Location is the location of the guest (required field using +kubebuilder:validation:Required marker) + | | Required: \{} + + |=== diff --git a/test/expected.md b/test/expected.md index a3678da..65d3596 100644 --- a/test/expected.md +++ b/test/expected.md @@ -132,6 +132,8 @@ _Appears in:_ | `time` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#time-v1-meta)_ | Time of entry | | | | `comment` _string_ | Comment by guest. This can be a multi-line comment.
Like this one.
Now let's test a list:
* a
* b
Another isolated comment.
Looks good? | | Pattern: `0*[a-z0-9]*[a-z]*[0-9]*\|\s`
| | `rating` _[Rating](#rating)_ | Rating provided by the guest | | Maximum: 5
Minimum: 1
| +| `email` _string_ | Email is the email address of the guest (required field using +required marker) | | Required: \{\}
| +| `location` _string_ | Location is the location of the guest (required field using +kubebuilder:validation:Required marker) | | Required: \{\}
| #### GuestbookHeader From 20a9e85d7cf83603e690356421d0625f56d32126 Mon Sep 17 00:00:00 2001 From: Thibault Richard Date: Tue, 6 Jan 2026 11:52:47 +0100 Subject: [PATCH 2/7] feat: add support for parsing k8s:required and k8s:optional markers --- processor/processor.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/processor/processor.go b/processor/processor.go index 76c8905..65cd0da 100644 --- a/processor/processor.go +++ b/processor/processor.go @@ -505,6 +505,14 @@ func mkRegistry(customMarkers []config.Marker) (*markers.Registry, error) { } } + // Register k8s:* markers - sig apimachinery plans to unify CRD and native types on these. + if err := registry.Define("k8s:required", markers.DescribesField, struct{}{}); err != nil { + return nil, err + } + if err := registry.Define("k8s:optional", markers.DescribesField, struct{}{}); err != nil { + return nil, err + } + for _, marker := range customMarkers { t := markers.DescribesField switch marker.Target { From 321c96395dea3b0db6bb8242ac5dd37a6a5e911e Mon Sep 17 00:00:00 2001 From: Thibault Richard Date: Tue, 6 Jan 2026 11:54:44 +0100 Subject: [PATCH 3/7] feat: add support for +required, +k8s:required, +optional and +k8s:optional markers --- processor/processor.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/processor/processor.go b/processor/processor.go index 65cd0da..bdcf5c7 100644 --- a/processor/processor.go +++ b/processor/processor.go @@ -562,11 +562,16 @@ func parseMarkers(markers markers.MarkerValues) (string, []string) { validation = append(validation, fmt.Sprintf("%s: %v", name, value)) } - // Handle standalone +required marker + // Handle standalone +required and +k8s:required marker // This is equivalent to +kubebuilder:validation:Required - if name == "required" { + if name == "required" || name == "k8s:required" { validation = append(validation, "Required: {}") } + // Handle standalone +optional and +k8s:optional marker + // This is equivalent to +kubebuilder:validation:Optional + if name == "optional" || name == "k8s:optional" { + validation = append(validation, "Optional: {}") + } if name == "kubebuilder:default" { if value, ok := value.(crdmarkers.Default); ok { From b2b68a1ea8680619d8770fb5553899b98d45e29e Mon Sep 17 00:00:00 2001 From: Thibault Richard Date: Tue, 6 Jan 2026 11:55:26 +0100 Subject: [PATCH 4/7] feat: enhance GuestbookEntry with required and optional markers --- test/api/v1/guestbook_types.go | 12 ++++++++++-- test/expected.asciidoc | 7 ++++++- test/expected.md | 6 ++++-- test/hide.md | 6 +++++- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/test/api/v1/guestbook_types.go b/test/api/v1/guestbook_types.go index 78fd520..1dac6e1 100644 --- a/test/api/v1/guestbook_types.go +++ b/test/api/v1/guestbook_types.go @@ -124,6 +124,7 @@ type PositiveInt int // GuestbookEntry defines an entry in a guest book. type GuestbookEntry struct { // Name of the guest (pipe | should be escaped) + // +kubebuilder:validation:Required // +kubebuilder:validation:MaxLength=80 // +kubebuilder:validation:Pattern=`0*[a-z0-9]*[a-z]*[0-9]` Name string `json:"name,omitempty"` @@ -146,12 +147,19 @@ type GuestbookEntry struct { Comment string `json:"comment,omitempty"` // Rating provided by the guest Rating Rating `json:"rating,omitempty"` + // Email is the email address of the guest (required field using +required marker) // +required Email string `json:"email"` - // Location is the location of the guest (required field using +kubebuilder:validation:Required marker) - // +kubebuilder:validation:Required + // Location is the location of the guest (required field using +k8s:required marker) + // +k8s:required Location string `json:"location"` + // Phone is the phone number of the guest (optional field using +optional marker) + // +optional + Phone string `json:"phone"` + // Company is the company of the guest (optional field using +k8s:optional marker) + // +k8s:optional + Company string `json:"company"` } // GuestbookStatus defines the observed state of Guestbook. diff --git a/test/expected.asciidoc b/test/expected.asciidoc index 5cfdceb..48b984f 100644 --- a/test/expected.asciidoc +++ b/test/expected.asciidoc @@ -162,6 +162,7 @@ GuestbookEntry defines an entry in a guest book. | Field | Description | Default | Validation | *`name`* __string__ | Name of the guest (pipe \| should be escaped) + | | MaxLength: 80 + Pattern: `0\*[a-z0-9]*[a-z]*[0-9]` + +Required: \{} + | *`tags`* __string array__ | Tags of the entry. + | | items:Pattern: `[a-z]*` + @@ -181,7 +182,11 @@ Minimum: 1 + | *`email`* __string__ | Email is the email address of the guest (required field using +required marker) + | | Required: \{} + -| *`location`* __string__ | Location is the location of the guest (required field using +kubebuilder:validation:Required marker) + | | Required: \{} + +| *`location`* __string__ | Location is the location of the guest (required field using +k8s:required marker) + | | Required: \{} + + +| *`phone`* __string__ | Phone is the phone number of the guest (optional field using +optional marker) + | | Optional: \{} + + +| *`company`* __string__ | Company is the company of the guest (optional field using +k8s:optional marker) + | | Optional: \{} + |=== diff --git a/test/expected.md b/test/expected.md index 65d3596..538adf6 100644 --- a/test/expected.md +++ b/test/expected.md @@ -127,13 +127,15 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `name` _string_ | Name of the guest (pipe \| should be escaped) | | MaxLength: 80
Pattern: `0*[a-z0-9]*[a-z]*[0-9]`
| +| `name` _string_ | Name of the guest (pipe \| should be escaped) | | MaxLength: 80
Pattern: `0*[a-z0-9]*[a-z]*[0-9]`
Required: \{\}
| | `tags` _string array_ | Tags of the entry. | | items:Pattern: `[a-z]*`
| | `time` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#time-v1-meta)_ | Time of entry | | | | `comment` _string_ | Comment by guest. This can be a multi-line comment.
Like this one.
Now let's test a list:
* a
* b
Another isolated comment.
Looks good? | | Pattern: `0*[a-z0-9]*[a-z]*[0-9]*\|\s`
| | `rating` _[Rating](#rating)_ | Rating provided by the guest | | Maximum: 5
Minimum: 1
| | `email` _string_ | Email is the email address of the guest (required field using +required marker) | | Required: \{\}
| -| `location` _string_ | Location is the location of the guest (required field using +kubebuilder:validation:Required marker) | | Required: \{\}
| +| `location` _string_ | Location is the location of the guest (required field using +k8s:required marker) | | Required: \{\}
| +| `phone` _string_ | Phone is the phone number of the guest (optional field using +optional marker) | | Optional: \{\}
| +| `company` _string_ | Company is the company of the guest (optional field using +k8s:optional marker) | | Optional: \{\}
| #### GuestbookHeader diff --git a/test/hide.md b/test/hide.md index 1757e3b..d274f96 100644 --- a/test/hide.md +++ b/test/hide.md @@ -128,11 +128,15 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `name` _string_ | Name of the guest (pipe \| should be escaped) | | MaxLength: 80
Pattern: `0*[a-z0-9]*[a-z]*[0-9]`
| +| `name` _string_ | Name of the guest (pipe \| should be escaped) | | MaxLength: 80
Pattern: `0*[a-z0-9]*[a-z]*[0-9]`
Required: \{\}
| | `tags` _string array_ | Tags of the entry. | | items:Pattern: `[a-z]*`
| | `time` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#time-v1-meta)_ | Time of entry | | | | `comment` _string_ | Comment by guest. This can be a multi-line comment.
Like this one.
Now let's test a list:
* a
* b
Another isolated comment.
Looks good? | | Pattern: `0*[a-z0-9]*[a-z]*[0-9]*\|\s`
| | `rating` _[Rating](#rating)_ | Rating provided by the guest | | Maximum: 5
Minimum: 1
| +| `email` _string_ | Email is the email address of the guest (required field using +required marker) | | Required: \{\}
| +| `location` _string_ | Location is the location of the guest (required field using +k8s:required marker) | | Required: \{\}
| +| `phone` _string_ | Phone is the phone number of the guest (optional field using +optional marker) | | Optional: \{\}
| +| `company` _string_ | Company is the company of the guest (optional field using +k8s:optional marker) | | Optional: \{\}
| #### GuestbookHeader From 88bbe30a4450efcadb28867d4067b60dc1b884a5 Mon Sep 17 00:00:00 2001 From: Thibault Richard Date: Tue, 6 Jan 2026 12:03:45 +0100 Subject: [PATCH 5/7] feat: add escape handling for pipe characters in Asciidoctor rendering --- renderer/asciidoctor.go | 7 +++++++ test/expected.asciidoc | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/renderer/asciidoctor.go b/renderer/asciidoctor.go index 6598266..dc4ee7f 100644 --- a/renderer/asciidoctor.go +++ b/renderer/asciidoctor.go @@ -169,6 +169,7 @@ func (adr *AsciidoctorRenderer) RenderFieldDoc(text string) string { func (adr *AsciidoctorRenderer) RenderValidation(text string) string { renderedText := escapeFirstAsterixInEachPair(text) + renderedText = escapePipe(renderedText) return escapeCurlyBraces(renderedText) } @@ -190,6 +191,12 @@ func escapeFirstAsterixInEachPair(text string) string { return text } +// escapePipe ensures sufficient escapes are added to pipe characters, so they are not mistaken +// for asciidoctor table formatting. +func escapePipe(text string) string { + return strings.Replace(text, "|", "\\|", -1) +} + // escapeCurlyBraces ensures sufficient escapes are added to curly braces, so they are not mistaken // for asciidoctor id attributes. func escapeCurlyBraces(text string) string { diff --git a/test/expected.asciidoc b/test/expected.asciidoc index 48b984f..dff065f 100644 --- a/test/expected.asciidoc +++ b/test/expected.asciidoc @@ -175,7 +175,7 @@ Now let's test a list: + Another isolated comment. + -Looks good? + | | Pattern: `0\*[a-z0-9]*[a-z]\*[0-9]*|\s` + +Looks good? + | | Pattern: `0\*[a-z0-9]*[a-z]\*[0-9]*\|\s` + | *`rating`* __xref:{anchor_prefix}-github-com-elastic-crd-ref-docs-api-v1-rating[$$Rating$$]__ | Rating provided by the guest + | | Maximum: 5 + Minimum: 1 + From e8750d8bf8f630955cf01e3613b563fd4bf956cf Mon Sep 17 00:00:00 2001 From: Thibault Richard Date: Wed, 7 Jan 2026 12:08:39 +0100 Subject: [PATCH 6/7] unit tests --- renderer/asciidoctor_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/renderer/asciidoctor_test.go b/renderer/asciidoctor_test.go index 124f659..2fb1e92 100644 --- a/renderer/asciidoctor_test.go +++ b/renderer/asciidoctor_test.go @@ -19,3 +19,8 @@ func Test_escapeCurlyBraces(t *testing.T) { assert.Equal(t, "[a-fA-F0-9]\\{64}", escapeCurlyBraces("[a-fA-F0-9]{64}")) assert.Equal(t, "[a-fA-F0-9]\\\\{64\\}", escapeCurlyBraces("[a-fA-F0-9]\\{64\\}")) } + +func Test_escapePipe(t *testing.T) { + assert.Equal(t, "[0-9]", escapePipe("[0-9]")) + assert.Equal(t, `[0-9]*\|\s`, escapePipe(`[0-9]*|\s`)) +} From 676f63700b1fdcea8986c245f2a903cc60929407 Mon Sep 17 00:00:00 2001 From: Thibault Richard Date: Wed, 7 Jan 2026 12:09:20 +0100 Subject: [PATCH 7/7] use ReplaceAll and reuse escapePipe --- renderer/asciidoctor.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/renderer/asciidoctor.go b/renderer/asciidoctor.go index dc4ee7f..695f2c2 100644 --- a/renderer/asciidoctor.go +++ b/renderer/asciidoctor.go @@ -151,7 +151,7 @@ func (adr *AsciidoctorRenderer) TemplateValue(key string) string { func (adr *AsciidoctorRenderer) RenderFieldDoc(text string) string { // Escape the pipe character, which has special meaning for asciidoc as a way to format tables, // so that including | in a comment does not result in wonky tables. - out := strings.ReplaceAll(text, "|", "\\|") + out := escapePipe(text) // Trim any leading and trailing whitespace from each line. lines := strings.Split(out, "\n") @@ -194,12 +194,12 @@ func escapeFirstAsterixInEachPair(text string) string { // escapePipe ensures sufficient escapes are added to pipe characters, so they are not mistaken // for asciidoctor table formatting. func escapePipe(text string) string { - return strings.Replace(text, "|", "\\|", -1) + return strings.ReplaceAll(text, "|", "\\|") } // escapeCurlyBraces ensures sufficient escapes are added to curly braces, so they are not mistaken // for asciidoctor id attributes. func escapeCurlyBraces(text string) string { // Per asciidoctor docs, only the leading curly brace needs to be escaped. - return strings.Replace(text, "{", "\\{", -1) + return strings.ReplaceAll(text, "{", "\\{") }