@redpanda-data/docs-extensions-and-macros 5.30.0 → 5.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -446,7 +446,7 @@ const tools = [
446
446
  },
447
447
  surface: {
448
448
  type: 'string',
449
- description: 'Comma-separated surfaces to lint: properties, metrics, rpk, helm, crd, connect (optional, defaults to all)'
449
+ description: `Comma-separated surfaces to lint: ${Object.keys(require('../tools/lint-strings').SURFACES).join(', ')} (optional, defaults to all)`
450
450
  },
451
451
  diff: {
452
452
  type: 'string',
package/bin/doc-tools.js CHANGED
@@ -1082,6 +1082,7 @@ automation
1082
1082
  .option('--output-dir <dir>', 'Where to write all generated files', 'modules/reference')
1083
1083
  .option('--cloud-support', 'Add AsciiDoc tags to generated property docs to indicate which ones are supported in Redpanda Cloud. This data is fetched from the cloudv2 repository so requires a GitHub token with repo permissions. The token is resolved from GIT_CREDENTIALS, REDPANDA_GITHUB_TOKEN, ACTIONS_BOT_TOKEN, GITHUB_TOKEN, VBOT_GITHUB_API_TOKEN, or GH_TOKEN, in that priority order', true)
1084
1084
  .option('--no-cloud-support', 'Skip Cloud support tags, and the cloudv2 repository access they need. A GitHub token is still required either way: the extractor clones the private streaming-enterprise repo to read the source')
1085
+ .option('--skip-rp-util', 'Keep cluster/broker scope Tree-sitter-only, skipping the rp_util merge step (default: use rp_util when available, falling back to Tree-sitter-only data if its schema cannot be obtained)')
1085
1086
  .option('--template-property <path>', 'Custom Handlebars template for individual property sections')
1086
1087
  .option('--template-topic-property <path>', 'Custom Handlebars template for topic property sections')
1087
1088
  .option('--template-topic-property-mappings <path>', 'Custom Handlebars template for topic property mappings table')
@@ -1156,6 +1157,7 @@ automation
1156
1157
  if (githubToken) env.GH_TOKEN = githubToken
1157
1158
  if (overrides) env.OVERRIDES = path.resolve(overrides)
1158
1159
  if (options.cloudSupport) env.CLOUD_SUPPORT = '1'
1160
+ if (options.skipRpUtil) env.SKIP_RP_UTIL = '1'
1159
1161
  if (templates.property) env.TEMPLATE_PROPERTY = path.resolve(templates.property)
1160
1162
  if (templates.topicProperty) env.TEMPLATE_TOPIC_PROPERTY = path.resolve(templates.topicProperty)
1161
1163
  if (templates.topicPropertyMappings) env.TEMPLATE_TOPIC_PROPERTY_MAPPINGS = path.resolve(templates.topicPropertyMappings)
@@ -2620,7 +2622,9 @@ programCli
2620
2622
  .command('lint-strings')
2621
2623
  .description('Lint user-facing doc strings embedded in engineering source code (properties, metrics, ...)')
2622
2624
  .requiredOption('--repo <path>', 'Path to an existing engineering checkout (for example, a local redpanda clone). Nothing is cloned.')
2623
- .option('--surface <list>', 'Comma-separated surfaces to lint (default: all registered). Registered: properties, metrics, rpk, helm, crd, connect')
2625
+ // Read off the registry rather than restated here: a hardcoded list silently
2626
+ // went stale the moment a surface was added.
2627
+ .option('--surface <list>', `Comma-separated surfaces to lint (default: all registered). Registered: ${Object.keys(require('../tools/lint-strings').SURFACES).join(', ')}`)
2624
2628
  .option('--diff <base>', 'Declaration-anchored diff mode: lint only declarations whose full span intersects lines changed in <base>...HEAD')
2625
2629
  .option('--format <format>', 'Output format: human or json', 'human')
2626
2630
  .option('--skip-rules <list>', 'Comma-separated rule ids to skip')
@@ -16,7 +16,8 @@ const DOC_TOOLS_BIN = path.resolve(__dirname, '..', 'doc-tools.js');
16
16
  * Lint user-facing doc strings embedded in engineering source code
17
17
  * @param {Object} args - Arguments
18
18
  * @param {string} args.repo - Path to the engineering checkout (required)
19
- * @param {string} [args.surface] - Comma-separated surfaces (properties, metrics, rpk, helm, crd, connect)
19
+ * @param {string} [args.surface] - Comma-separated surfaces; see the SURFACES
20
+ * registry in tools/lint-strings/index.js for the current list
20
21
  * @param {string} [args.diff] - Base ref for declaration-anchored diff mode
21
22
  * @param {string} [args.skip_rules] - Comma-separated rule ids to skip
22
23
  * @param {string} [args.only_rules] - Comma-separated rule ids to run exclusively
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.30.0",
3
+ "version": "5.35.0",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -26,7 +26,21 @@ const SURFACE_ROUTES = [
26
26
  // console) and charts/<name>/values.yaml (connectors).
27
27
  { surface: 'helm', pattern: /^charts\/[^/]+\/(chart\/)?values\.yaml$/ },
28
28
  { surface: 'crd', pattern: /^operator\/api\// },
29
- { surface: 'connect', pattern: /^internal\/impl\// }
29
+ { surface: 'connect', pattern: /^internal\/impl\// },
30
+ // API protos, whose comments and openapiv2 option strings reach readers as
31
+ // OpenAPI descriptions. console holds the data plane and Console APIs under
32
+ // proto/redpanda/api/; cloudv2's control plane lives under proto/public/.
33
+ // Anchored on the api/ segment so cloudv2's proto/descriptors/ tree (private
34
+ // and internal) and every vendored proto/public/**/wellknown/ path stay out.
35
+ { surface: 'api', pattern: /^proto\/(?:redpanda\/api|public\/[^/]+\/redpanda\/api)\/.*\.proto$/ },
36
+ // Admin API v2, in the redpanda/streaming-enterprise repo. A different
37
+ // generator (protoc-gen-connect-openapi) and a third string form; see
38
+ // surfaces/api.js. These two directories are exactly what the api-docs
39
+ // bundler feeds into the published spec (tools/bundle-openapi.js):
40
+ // `admin/internal` and `core/testing` are excluded by that repo's buf.yaml
41
+ // and generate nothing, and `core/rest` generates a fragment that is never
42
+ // bundled, so none of them reach readers.
43
+ { surface: 'api', pattern: /^proto\/redpanda\/core\/(?:admin\/v2|common)\/.*\.proto$/ }
30
44
  ]
31
45
 
32
46
  /**
@@ -0,0 +1,114 @@
1
+ // Copyright 2026 Redpanda Data, Inc.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ //
6
+ // A license header is a leading comment on nothing. It must not attach itself
7
+ // to the first declaration in the file.
8
+
9
+ syntax = "proto3";
10
+
11
+ package redpanda.core.admin.v2;
12
+
13
+ import "proto/redpanda/core/pbgen/rpc.proto";
14
+
15
+ option (pbgen.cpp_namespace) = "proto::fixture";
16
+
17
+ service FixtureService {
18
+ // GetFixture
19
+ //
20
+ // Returns a single fixture.
21
+ rpc GetFixture(GetFixtureRequest) returns (GetFixtureResponse) {
22
+ option (pbgen.rpc) = {
23
+ authz: SUPERUSER
24
+ };
25
+ }
26
+
27
+ // CreateFixture
28
+ //
29
+ // Creates a fixture. The description can run over several comment
30
+ // lines, and the resolved prose joins them.
31
+ //
32
+ // A blank comment line inside the description is a paragraph break and
33
+ // stays part of the description.
34
+ rpc CreateFixture(CreateFixtureRequest)
35
+ returns (CreateFixtureResponse) {
36
+ option (pbgen.rpc) = {
37
+ authz: SUPERUSER
38
+ };
39
+ }
40
+
41
+ // Reports the collapsed case: with no blank line the generator uses this
42
+ // whole comment as BOTH the summary and the description, so the API
43
+ // reference's nav item becomes a paragraph.
44
+ rpc CollapsedFixture(CollapsedFixtureRequest)
45
+ returns (CollapsedFixtureResponse) {
46
+ option (pbgen.rpc) = {
47
+ authz: SUPERUSER
48
+ };
49
+ }
50
+
51
+ // DeleteFixture.
52
+ //
53
+ // Deletes a fixture.
54
+ rpc DeleteFixture(DeleteFixtureRequest) returns (DeleteFixtureResponse) {
55
+ option (pbgen.rpc) = {
56
+ authz: SUPERUSER
57
+ };
58
+ }
59
+
60
+ // ListFixtures
61
+ // over two lines
62
+ //
63
+ // Lists every fixture.
64
+ rpc ListFixtures(ListFixturesRequest) returns (ListFixturesResponse) {
65
+ option (pbgen.rpc) = {
66
+ authz: SUPERUSER
67
+ };
68
+ }
69
+
70
+ // BodylessFixture
71
+ //
72
+ // A body-less rpc opens no block, so it must not swallow the
73
+ // declarations that follow it.
74
+ rpc BodylessFixture(BodylessFixtureRequest) returns (BodylessFixtureResponse);
75
+
76
+ // WatchFixture
77
+ //
78
+ // Confirms the rpc after a body-less one is still found.
79
+ rpc WatchFixture(WatchFixtureRequest) returns (WatchFixtureResponse) {
80
+ option (pbgen.rpc) = {
81
+ authz: SUPERUSER
82
+ };
83
+ }
84
+ }
85
+
86
+ // The request for a single fixture.
87
+ message GetFixtureRequest {
88
+ // The node ID for the fixture. If unset, the broker handling the RPC
89
+ // returns information about itself.
90
+ optional int32 node_id = 1;
91
+
92
+ // Fixture name
93
+ string name = 2;
94
+
95
+ int32 undocumented = 3;
96
+
97
+ // Partition ID
98
+ int32 partition_id = 4;
99
+
100
+ // The protocol versions the fixture supports. See
101
+ // https://kafka.apache.org/protocol.html#protocol_compatibility
102
+ repeated int32 api_versions = 5;
103
+ }
104
+
105
+ // Fixture mode
106
+ //
107
+ // - Not specified
108
+ // - Runs fast
109
+ enum FixtureMode {
110
+ FIXTURE_MODE_UNSPECIFIED = 0;
111
+ // Per-value comments are discarded by protoc-gen-connect-openapi, so they
112
+ // are not extracted and must not be reported as declarations.
113
+ FIXTURE_MODE_FAST = 1;
114
+ }
@@ -0,0 +1,123 @@
1
+ syntax = "proto3";
2
+
3
+ package redpanda.api.fixture.v1;
4
+
5
+ import "google/protobuf/descriptor.proto";
6
+ import "protoc-gen-openapiv2/options/annotations.proto";
7
+
8
+ // Extension fields are wire metadata, never published schema. The field
9
+ // inside this block must not be extracted.
10
+ extend google.protobuf.MethodOptions {
11
+ string fixture_extension = 60002;
12
+ }
13
+
14
+ // FixtureMode selects how the fixture behaves.
15
+ enum FixtureMode {
16
+ FIXTURE_MODE_UNSPECIFIED = 0;
17
+ FIXTURE_MODE_FAST = 1;
18
+ }
19
+
20
+ // A fixture topic.
21
+ message FixtureTopic {
22
+ // A topic-level config key (e.g. `segment.bytes`).
23
+ string name = 1;
24
+
25
+ // Whether the config is read-only, or is dynamic and can be altered.
26
+ bool read_only = 2;
27
+
28
+ // Wrapped option lists must be covered by the declaration span, so a
29
+ // suggestion block can replace the whole statement.
30
+ string wrapped = 3 [
31
+ (buf.validate.field).string.min_len = 1,
32
+ (buf.validate.field).string.max_len = 255
33
+ ];
34
+
35
+ // Sets default_topic_partitions when the topic is created.
36
+ int32 partitions = 4;
37
+
38
+ // A comment can wrap over several lines, and the resolved prose joins them
39
+ // rather than keeping only the first.
40
+ string wrapped_comment = 5;
41
+
42
+ string undocumented = 6;
43
+
44
+ // An openapiv2_field description OVERRIDES this comment, so the comment is
45
+ // dead text and the option's string is what must be linted.
46
+ string option_documented = 9 [
47
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
48
+ description: "The string the generator actually publishes."
49
+ example: "\"ignored\""
50
+ }
51
+ ];
52
+
53
+ /*
54
+ * A field documented ONLY by an openapiv2_field option is not an
55
+ * undocumented field. 73 of console's fields are this shape and every one
56
+ * of them used to be reported as having no description.
57
+ *
58
+ * This note is a detached block comment, separated by a blank line, so that
59
+ * option_only genuinely has no leading comment of its own.
60
+ */
61
+
62
+ string option_only = 10 [
63
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
64
+ description: "Documented by the option alone."
65
+ }
66
+ ];
67
+
68
+ // A oneof moves the brace depth without opening a message. Matching a bare
69
+ // closing brace here popped FixtureTopic and reported the sibling message
70
+ // below as nested inside it.
71
+ oneof selector {
72
+ // Selects by name.
73
+ string by_name = 7;
74
+ // Selects by id.
75
+ string by_id = 8;
76
+ }
77
+
78
+ // Nested messages are genuinely nested and should report a dotted path.
79
+ message Nested {
80
+ // The nested value.
81
+ string value = 1;
82
+ }
83
+ }
84
+
85
+ // A sibling of FixtureTopic, not a child of it.
86
+ message FixtureSibling {
87
+ // The sibling name.
88
+ string name = 1;
89
+ }
90
+
91
+ service FixtureService {
92
+ rpc CreateFixture(FixtureTopic) returns (FixtureTopic) {
93
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
94
+ summary: "Create fixture."
95
+ description: "Create a [fixture](https://docs.redpanda.com/fixture/) with a `{prefix}` path."
96
+ responses: {
97
+ key: "200"
98
+ value: {description: "OK"}
99
+ }
100
+ responses: {
101
+ key: "201"
102
+ value: {description: "Fixture created"}
103
+ }
104
+ };
105
+ }
106
+
107
+ rpc ListFixtures(FixtureTopic) returns (FixtureTopic) {
108
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
109
+ summary: "List fixtures"
110
+ description: "Short one"
111
+ };
112
+ }
113
+
114
+ rpc JoinFixtures(FixtureTopic) returns (FixtureTopic) {
115
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
116
+ summary: "Join fixtures"
117
+ // Proto concatenates adjacent string literals like C++, so the resolved
118
+ // description is both halves, not just the first.
119
+ description: "Joins two fixtures together "
120
+ "and returns the result."
121
+ };
122
+ }
123
+ }
@@ -28,7 +28,7 @@ This fixture exercises the ` + "`raw` + interpreted + `raw`" + ` concatenation i
28
28
  This heading and the table below are legitimate AsciiDoc:
29
29
 
30
30
  |===
31
- | kafka_key | The record key
31
+ | `+"`kafka_key`"+` | The record key
32
32
  |===
33
33
  `).
34
34
  Fields(fixtureConfigFields()...)
@@ -4,6 +4,7 @@ package v1alpha2
4
4
 
5
5
  import (
6
6
  metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7
+ "k8s.io/apimachinery/pkg/runtime"
7
8
  )
8
9
 
9
10
  // WidgetSpec defines the configuration of a fixture widget.
@@ -35,6 +36,75 @@ type WidgetReference struct {
35
36
  Version int `json:"version"`
36
37
  }
37
38
 
39
+ // EmptyOnOneLine opens and closes on a single line. Assuming it opened a
40
+ // struct body left the parser inside a type that had already ended, which
41
+ // dropped every declaration below it in the file.
42
+ type EmptyOnOneLine struct{}
43
+
44
+ // ValueSource represents where a value can be pulled from.
45
+ type ValueSource struct {
46
+ // The literal value.
47
+ Value string `json:"value,omitempty"`
48
+ }
49
+
50
+ // UndocumentedType has no doc comment of its own, so a field referencing it
51
+ // has nothing to inherit and genuinely ships blank.
52
+ type undocumentedTypeMarker int
53
+
54
+ type UndocumentedTarget struct {
55
+ // The only field.
56
+ Only string `json:"only,omitempty"`
57
+ }
58
+
59
+ // InlinedValues is +hidefromdoc, but it is embedded with `json:",inline"`
60
+ // below, so crd-ref-docs flattens its fields into the embedding type and
61
+ // publishes every one of them. The marker suppresses this type's own section,
62
+ // not its fields.
63
+ // +hidefromdoc
64
+ type InlinedValues struct {
65
+ ReplicaCount *int32 `json:"replicaCount,omitempty"`
66
+ }
67
+
68
+ // HiddenNotInlined is +hidefromdoc and embedded nowhere, so nothing it
69
+ // declares is ever published.
70
+ // +hidefromdoc
71
+ type HiddenNotInlined struct {
72
+ NeverPublished string `json:"neverPublished,omitempty"`
73
+ }
74
+
75
+ // EmbeddingSpec inlines a hidden type.
76
+ type EmbeddingSpec struct {
77
+ InlinedValues `json:",inline"`
78
+ // The name of the thing.
79
+ Name string `json:"name,omitempty"`
80
+ }
81
+
82
+ // StructuralSpec covers external types that controller-gen publishes with no
83
+ // description of their own.
84
+ type StructuralSpec struct {
85
+ // +optional
86
+ Config *runtime.RawExtension `json:"config,omitempty"`
87
+ // +optional
88
+ Expiration *metav1.Time `json:"expiration,omitempty"`
89
+ }
90
+
91
+ // FallbackSpec covers what an uncommented field actually publishes.
92
+ type FallbackSpec struct {
93
+ // +optional
94
+ Inherited *ValueSource `json:"inherited,omitempty"`
95
+ // +optional
96
+ FromUndocumented *UndocumentedTarget `json:"fromUndocumented,omitempty"`
97
+ // +optional
98
+ Primitive string `json:"primitive,omitempty"`
99
+ // +optional
100
+ SliceOfDocumented []ValueSource `json:"sliceOfDocumented,omitempty"`
101
+ // +optional
102
+ External *metav1.Duration `json:"external,omitempty"`
103
+ // How long to wait before giving up on the widget.
104
+ // +optional
105
+ Both *ValueSource `json:"both,omitempty"`
106
+ }
107
+
38
108
  // WidgetList is ignored via the config's ignoreTypes ('List$').
39
109
  type WidgetList struct {
40
110
  metav1.TypeMeta `json:",inline"`
@@ -11,7 +11,8 @@ image:
11
11
  tag: latest
12
12
 
13
13
  # -- Service settings that were commented out wholesale. The nested markers
14
- # below are dead: no pipeline can attach them.
14
+ # below are dead: they use the indented `# -- ` form, which helm-docs does
15
+ # not recognize as a marker at all, so no pipeline can attach them.
15
16
  # service:
16
17
  # -- set service.name to override the default service name
17
18
  # name: redpanda
@@ -20,8 +21,9 @@ image:
20
21
  # annotations: {}
21
22
 
22
23
 
23
- # A marker separated from any key by a blank line is dead too:
24
- # -- This orphaned description attaches to nothing.
24
+ # A marker separated from the next key by blank lines is NOT dead: helm-docs
25
+ # ignores the gap and hands it to the next real key below, which is `resources`.
26
+ # -- Orphaned marker; helm-docs attaches this to the next real key.
25
27
 
26
28
  # @doc external.domain -- Optional domain advertised to external clients.
27
29
  # @default -- `nil`
@@ -33,6 +35,17 @@ resources:
33
35
  # @ignored
34
36
  internalHiddenKey: {}
35
37
 
38
+ misattached:
39
+ # -- Written for the commented-out key below, but with no blank line and no
40
+ # later marker, helm-docs hands this text to realKeyBelow instead.
41
+ # optionalKey:
42
+ realKeyBelow: value
43
+ # -- Written for the commented-out key below. The real key that follows has
44
+ # its own marker, so helm-docs uses that one and nothing is misattached.
45
+ # anotherOptional:
46
+ # -- The guarded key's own description.
47
+ guarded: value
48
+
36
49
  undocumentedTopLevel:
37
50
  child: value
38
51
 
@@ -25,7 +25,8 @@ const SURFACES = {
25
25
  rpk: require('./surfaces/rpk'),
26
26
  helm: require('./surfaces/helm'),
27
27
  crd: require('./surfaces/crd'),
28
- connect: require('./surfaces/connect')
28
+ connect: require('./surfaces/connect'),
29
+ api: require('./surfaces/api')
29
30
  }
30
31
 
31
32
  /**
@@ -54,6 +54,79 @@ function isNameEcho (name, description) {
54
54
  return true
55
55
  }
56
56
 
57
+ /**
58
+ * Blank out the spans of a description where a code-ish token is expected to
59
+ * appear bare, so the inline-code rule only sees prose:
60
+ *
61
+ * * backticked spans - already marked up, which is the whole point;
62
+ * * URLs and Markdown link targets - a path inside a link is part of the
63
+ * link, and backticking it would break the link;
64
+ * * anything the declaration is named after, which name-echo owns.
65
+ *
66
+ * Replaced with spaces rather than removed so match indices stay meaningful.
67
+ */
68
+ function maskNonProse (text) {
69
+ let out = text
70
+ const blank = (m) => ' '.repeat(m.length)
71
+ // Backticked spans first: a URL inside backticks is already marked up.
72
+ out = out.replace(/`[^`]*`/g, blank)
73
+ // Markdown links: mask the whole construct, target included.
74
+ out = out.replace(/\[[^\]]*\]\([^)]*\)/g, blank)
75
+ // Bare URLs, and AsciiDoc's url[text] form.
76
+ out = out.replace(/\b[a-z][a-z0-9+.-]*:\/\/\S+/gi, blank)
77
+ return out
78
+ }
79
+
80
+ /**
81
+ * Tokens that are code and read as code, so they belong in inline code.
82
+ * Backticks are the portable choice: a single-backtick span is inline code in
83
+ * both AsciiDoc and Markdown, so one rule covers every surface regardless of
84
+ * which format the generator emits.
85
+ *
86
+ * Deliberately narrow. This rule posts inline suggestions on engineering PRs,
87
+ * so a false positive costs more than a miss:
88
+ *
89
+ * * snake_case with at least two segments - in this domain that is always an
90
+ * identifier (`default_topic_partitions`), never prose;
91
+ * * long flags (`--tolerate-data-loss`);
92
+ * * absolute paths with at least two segments (`/etc/redpanda/redpanda.yaml`,
93
+ * `/v1/topics`).
94
+ *
95
+ * Not included, on purpose: dotted keys (`segment.bytes`) collide with
96
+ * sentence boundaries and abbreviations; bare `true`/`false` and numbers
97
+ * appear in ordinary prose; single-segment lowercase words are unrecoverable
98
+ * from prose without a symbol table.
99
+ */
100
+ const CODE_TOKEN_PATTERNS = Object.freeze([
101
+ { id: 'identifier', re: /\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b/g },
102
+ { id: 'flag', re: /(?<![\w-])--[a-z][a-z0-9]*(?:-[a-z0-9]+)*\b/g },
103
+ { id: 'path', re: /(?<![\w/])\/[a-z][a-zA-Z0-9_.-]*(?:\/[a-zA-Z0-9_.{}-]+)+\/?/g }
104
+ ])
105
+
106
+ /**
107
+ * Code-ish tokens in a description that are not inside inline code.
108
+ * Exported for tests and for the surfaces that scope the rule differently.
109
+ */
110
+ function findBareCodeTokens (text, name = null) {
111
+ const masked = maskNonProse(String(text || ''))
112
+ const found = []
113
+ const seen = new Set()
114
+ for (const { id, re } of CODE_TOKEN_PATTERNS) {
115
+ re.lastIndex = 0
116
+ let match
117
+ while ((match = re.exec(masked)) !== null) {
118
+ const token = match[0]
119
+ // The declaration's own name restating itself is name-echo's finding,
120
+ // not a markup one.
121
+ if (name && token === name) continue
122
+ if (seen.has(token)) continue
123
+ seen.add(token)
124
+ found.push({ token, kind: id })
125
+ }
126
+ }
127
+ return found
128
+ }
129
+
57
130
  const COMMON_RULES = [
58
131
  {
59
132
  name: 'empty-description',
@@ -142,7 +215,20 @@ const COMMON_RULES = [
142
215
  }
143
216
  return []
144
217
  }
218
+ },
219
+ {
220
+ name: 'missing-inline-code',
221
+ description: 'Code value or field name in prose without inline code',
222
+ severity: 'warning',
223
+ check: (decl) => {
224
+ const bare = findBareCodeTokens(decl.string, decl.name)
225
+ if (bare.length === 0) return []
226
+ const list = bare.map((b) => `\`${b.token}\``).join(', ')
227
+ return [{
228
+ message: `Code values and field names belong in inline code: ${list}. Wrap each in backticks, which render as inline code in both AsciiDoc and Markdown.`
229
+ }]
230
+ }
145
231
  }
146
232
  ]
147
233
 
148
- module.exports = { COMMON_RULES, JARGON_TERMS, isNameEcho, tokenize }
234
+ module.exports = { COMMON_RULES, JARGON_TERMS, isNameEcho, tokenize, findBareCodeTokens, maskNonProse }