@redpanda-data/docs-extensions-and-macros 5.9.0 → 5.11.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.
@@ -2588,6 +2588,10 @@ async function generateRpkDocs(options = {}) {
2588
2588
  overrides,
2589
2589
  outputDir,
2590
2590
  cloudSecretDir, // Directory for rpk cloud and rpk security secret commands
2591
+ // Directory for the shared -X -> RPK_* env vars partial. The handler passes
2592
+ // the partials dir it already derived; when absent (direct callers, tests)
2593
+ // it is derived from outputDir the same way.
2594
+ envPartialDir,
2591
2595
  rpkVersion,
2592
2596
  pluginVersions = {},
2593
2597
  draftMissing = false,
@@ -3155,6 +3159,21 @@ async function generateRpkDocs(options = {}) {
3155
3159
  navResult = updateNavFile(navFile, commands, resolvedOverrides, topLevelWithSubcommands, effectiveProtectedPlugins)
3156
3160
  }
3157
3161
 
3162
+ // Regenerate the shared -X -> RPK_* env vars partial from the same tree,
3163
+ // so it always matches the rpk build the command pages came from. Trees
3164
+ // from rpk versions that predate x_options (redpanda#31520) skip with a
3165
+ // note; `doc-tools generate rpk-env-partial` covers those via its
3166
+ // -X list fallback.
3167
+ const envPartialResult = writeEnvVarsPartial(
3168
+ tree,
3169
+ envPartialDir || derivePartialsDir(outputDir)
3170
+ )
3171
+ if (envPartialResult.written) {
3172
+ console.log(` Regenerated env vars partial (${envPartialResult.keyCount} -X options): ${envPartialResult.output}`)
3173
+ } else {
3174
+ console.log(' Env vars partial not regenerated: tree has no x_options (rpk predates redpanda#31520)')
3175
+ }
3176
+
3158
3177
  return {
3159
3178
  commandCount: commands.length,
3160
3179
  filesGenerated,
@@ -3162,12 +3181,55 @@ async function generateRpkDocs(options = {}) {
3162
3181
  filesFailed,
3163
3182
  filesDeleted,
3164
3183
  subdirectoriesCreated: createdSubdirs.size,
3184
+ envPartial: envPartialResult,
3165
3185
  ...navResult
3166
3186
  }
3167
3187
  }
3168
3188
 
3189
+ /**
3190
+ * Resolve the partials directory of the Antora module that owns the rpk pages.
3191
+ *
3192
+ * For the docs layout (`.../modules/reference/pages/rpk`) this is the sibling
3193
+ * of `pages`, so `.../modules/reference/partials`. For an arbitrary
3194
+ * `--output-dir` there is no `pages` segment to anchor on, so fall back to a
3195
+ * `partials` directory beside the output. Walking a fixed number of levels up
3196
+ * from outputDir is wrong for arbitrary paths: it escapes the output tree and
3197
+ * can land on an unrelated directory.
3198
+ *
3199
+ * @param {string} outputDir - The rpk pages output directory
3200
+ * @returns {string} Absolute or relative path to the partials directory
3201
+ */
3202
+ function derivePartialsDir (outputDir) {
3203
+ const normalized = String(outputDir).replace(/\\/g, '/')
3204
+ const pagesIndex = normalized.lastIndexOf('/pages/')
3205
+ if (pagesIndex !== -1) {
3206
+ return path.join(normalized.substring(0, pagesIndex), 'partials')
3207
+ }
3208
+ return path.join(path.dirname(normalized), 'partials')
3209
+ }
3210
+
3211
+ /**
3212
+ * Write the -X -> RPK_* env vars partial from a command tree's x_options.
3213
+ * @param {Object} tree - Parsed rpk command tree
3214
+ * @param {string} partialsDir - Directory the partial is written into
3215
+ * @returns {{written: boolean, keyCount?: number, output?: string}}
3216
+ */
3217
+ function writeEnvVarsPartial (tree, partialsDir) {
3218
+ if (!Array.isArray(tree.x_options) || tree.x_options.length === 0) {
3219
+ return { written: false }
3220
+ }
3221
+ const { renderPartial, keyToEnvVar } = require('./generate-x-env-partial.js')
3222
+ const options = tree.x_options.map(o => ({ name: o.name, env: o.env || keyToEnvVar(o.name) }))
3223
+ const output = path.join(partialsDir, 'rpk-env-vars.adoc')
3224
+ fs.mkdirSync(path.dirname(output), { recursive: true })
3225
+ fs.writeFileSync(output, renderPartial(options))
3226
+ return { written: true, keyCount: options.length, output }
3227
+ }
3228
+
3169
3229
  module.exports = {
3170
3230
  generateRpkDocs,
3231
+ writeEnvVarsPartial,
3232
+ derivePartialsDir,
3171
3233
  mergeCommandOverrides,
3172
3234
  applyOverridesToTree,
3173
3235
  resolveReferences,
@@ -0,0 +1,284 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Generates the rpk env vars partial (the `-X` option -> RPK_* environment
5
+ * variable mapping table) from rpk itself, so the table cannot drift from
6
+ * the CLI.
7
+ *
8
+ * Data source, in order of preference:
9
+ * 1. The `x_options` array in `rpk --print-tree` JSON (added in
10
+ * redpanda#31520): structured, versioned with the release, and carries
11
+ * descriptions.
12
+ * 2. `rpk -X list` text output, for rpk versions that predate x_options.
13
+ *
14
+ * The partial is included by both reference:rpk/rpk-x-options.adoc and
15
+ * reference:environment-variables.adoc in the docs repo. Hidden -X options
16
+ * (for example cloud_environment, whose values are deliberately
17
+ * undocumented) appear in neither source, so they are excluded
18
+ * automatically.
19
+ */
20
+
21
+ const fs = require('fs')
22
+ const os = require('os')
23
+ const path = require('path')
24
+ const { spawnSync } = require('child_process')
25
+
26
+ // A healthy rpk exposes ~30 -X options. Refuse to overwrite the partial
27
+ // with a suspiciously small table (for example, if -X list output ever
28
+ // changes format and only partially parses).
29
+ const MIN_OPTIONS = 20
30
+
31
+ const GENERATED_BANNER = `// tag::generated[]
32
+ // Generated from rpk's own -X option data. Do not edit manually:
33
+ // regenerate with doc-tools generate rpk-docs (which rewrites this
34
+ // file alongside the rpk command pages) or, for a targeted refresh,
35
+ // doc-tools generate rpk-env-partial.
36
+ `
37
+
38
+ /**
39
+ * Extract -X options from `rpk --print-tree` JSON.
40
+ * @param {string} output - Raw stdout from `rpk --print-tree`
41
+ * @returns {Array<{name: string, env: string}>|null} options in display
42
+ * order, or null when the tree has no x_options (rpk predates
43
+ * redpanda#31520). The env name comes from rpk itself when present
44
+ * (the tree's env field is produced by the same derivation rpk reads).
45
+ */
46
+ function xOptionsFromTree (output) {
47
+ let tree
48
+ try {
49
+ tree = JSON.parse(output)
50
+ } catch {
51
+ return null
52
+ }
53
+ if (!Array.isArray(tree.x_options) || tree.x_options.length === 0) return null
54
+ return tree.x_options.map(o => ({ name: o.name, env: o.env || keyToEnvVar(o.name) }))
55
+ }
56
+
57
+ /**
58
+ * Parse `rpk -X list` output into option keys (fallback for rpk versions
59
+ * whose --print-tree has no x_options).
60
+ * Each line has the form `key=value-hint`; keys contain lowercase
61
+ * letters, digits, dots, and underscores.
62
+ * @param {string} output - Raw stdout from `rpk -X list`
63
+ * @returns {Array<string>} option keys in CLI order
64
+ */
65
+ function parseXList (output) {
66
+ const keys = []
67
+ for (const line of output.split('\n')) {
68
+ const eq = line.indexOf('=')
69
+ if (eq < 1) continue
70
+ const key = line.slice(0, eq).trim()
71
+ if (/^[a-z0-9._]+$/.test(key)) keys.push(key)
72
+ }
73
+ return keys
74
+ }
75
+
76
+ /**
77
+ * Convert an -X option key to its RPK_* environment variable name:
78
+ * prefix RPK_, uppercase, dots -> underscores.
79
+ * @param {string} key
80
+ * @returns {string}
81
+ */
82
+ function keyToEnvVar (key) {
83
+ return 'RPK_' + key.toUpperCase().replace(/\./g, '_')
84
+ }
85
+
86
+ /**
87
+ * Convert an -X option key to its section anchor on the rpk-x-options page.
88
+ * The docs repo's AsciiDoc ID convention turns dots into hyphens but keeps
89
+ * underscores (tls.insecure_skip_verify -> tls-insecure_skip_verify),
90
+ * verified against the rendered page IDs. The previous hand-written table
91
+ * hyphenated underscores too, which left 12 of its 31 in-page links dead.
92
+ * @param {string} key
93
+ * @returns {string}
94
+ */
95
+ function keyToAnchor (key) {
96
+ return key.replace(/\./g, '-')
97
+ }
98
+
99
+ /**
100
+ * Render the partial's AsciiDoc content.
101
+ * @param {Array<{name: string, env: string}>} options - -X options
102
+ * @returns {string}
103
+ */
104
+ function renderPartial (options) {
105
+ const rows = options.map(({ name, env }) => {
106
+ const xrefTarget = `xref:reference:rpk/rpk-x-options.adoc#${keyToAnchor(name)}[${name}]`
107
+ return `|${xrefTarget} |${env}`
108
+ })
109
+ return `${GENERATED_BANNER}
110
+ Every \`-X\` option has a corresponding \`RPK_*\` environment variable. Convert by prefixing with \`RPK_\` and replacing dots with underscores:
111
+
112
+ [cols="1m,1m"]
113
+ |===
114
+ |\`-X\` Option |Environment Variable
115
+
116
+ ${rows.join('\n')}
117
+ |===
118
+ // end::generated[]
119
+ `
120
+ }
121
+
122
+ /**
123
+ * Run an rpk invocation from a Go source checkout.
124
+ * @param {string} sourcePath - Path to src/go/rpk in a redpanda checkout
125
+ * @param {Array<string>} args - rpk arguments
126
+ * @returns {string} Raw stdout
127
+ */
128
+ function runRpkFromSource (sourcePath, args) {
129
+ const result = spawnSync('go', ['run', 'cmd/rpk/main.go', ...args], {
130
+ cwd: sourcePath,
131
+ encoding: 'utf8',
132
+ timeout: 300000, // includes build time
133
+ maxBuffer: 50 * 1024 * 1024
134
+ })
135
+ if (result.status !== 0) {
136
+ throw new Error(`Failed to run rpk ${args.join(' ')} from source: ${result.stderr}`)
137
+ }
138
+ return result.stdout
139
+ }
140
+
141
+ /**
142
+ * Run an rpk invocation with an existing binary.
143
+ * @param {string} rpkBin - Path to the rpk binary
144
+ * @param {Array<string>} args - rpk arguments
145
+ * @returns {string} Raw stdout
146
+ */
147
+ function runRpkBinary (rpkBin, args) {
148
+ const result = spawnSync(rpkBin, args, {
149
+ encoding: 'utf8',
150
+ timeout: 60000,
151
+ maxBuffer: 50 * 1024 * 1024
152
+ })
153
+ if (result.status !== 0) {
154
+ throw new Error(`Failed to run ${rpkBin} ${args.join(' ')}: ${result.stderr}`)
155
+ }
156
+ return result.stdout
157
+ }
158
+
159
+ /**
160
+ * Resolve a local rpk source path (repo root or src/go/rpk) without
161
+ * touching the checkout.
162
+ * @param {string} p - User-supplied path
163
+ * @returns {string} Path to src/go/rpk
164
+ */
165
+ function resolveRpkSourcePath (p) {
166
+ const abs = path.resolve(p)
167
+ if (abs.endsWith(path.join('src', 'go', 'rpk'))) return abs
168
+ const nested = path.join(abs, 'src', 'go', 'rpk')
169
+ if (fs.existsSync(nested)) return nested
170
+ throw new Error(`Cannot find rpk source under ${abs}: pass the repo root or the src/go/rpk directory`)
171
+ }
172
+
173
+ /**
174
+ * Find the temp sparse-clone root that prepareSourceFromRef created
175
+ * (os.tmpdir()/rpk-source-<random>/redpanda/src/go/rpk), or null when the path is
176
+ * not one of ours — never delete anything outside the temp dir.
177
+ * @param {string} sourcePath - The src/go/rpk path returned by prepareSourceFromRef
178
+ * @returns {string|null}
179
+ */
180
+ function tempCloneRoot (sourcePath) {
181
+ let dir = sourcePath
182
+ const tmp = fs.realpathSync(os.tmpdir())
183
+ const real = fs.realpathSync(dir)
184
+ dir = real
185
+ while (dir.startsWith(tmp) && dir !== tmp) {
186
+ if (path.basename(dir).startsWith('rpk-source-') && path.dirname(dir) === tmp) return dir
187
+ dir = path.dirname(dir)
188
+ }
189
+ return null
190
+ }
191
+
192
+ /**
193
+ * Generate the partial and write it to disk.
194
+ * @param {Object} options
195
+ * @param {string} [options.ref] - Git ref of redpanda to build rpk from
196
+ * @param {string} [options.fromSource] - Local src/go/rpk path (skips clone)
197
+ * @param {string} [options.rpkBin] - Existing rpk binary (skips build)
198
+ * @param {string} [options.fromJson] - Versioned tree snapshot from docs-data
199
+ * (skips clone and build; the snapshot must carry x_options)
200
+ * @param {string} options.output - Path to write the partial to
201
+ * @returns {{keyCount: number, output: string, source: string}}
202
+ */
203
+ function handleXEnvPartialGeneration (options) {
204
+ const { ref, fromSource, rpkBin, fromJson, output } = options
205
+ if (!output) throw new Error('Missing required --output path')
206
+
207
+ let xopts
208
+ let source
209
+ if (fromJson) {
210
+ // Snapshots saved by the rpk-docs handler wrap the tree in raw_tree/tree;
211
+ // accept a bare tree too. There is no rpk to run in this mode, so a
212
+ // snapshot from before x_options existed (redpanda#31520) cannot be used.
213
+ const j = JSON.parse(fs.readFileSync(fromJson, 'utf8'))
214
+ for (const candidate of [j.raw_tree, j.tree, j]) {
215
+ if (!candidate) continue
216
+ xopts = xOptionsFromTree(JSON.stringify(candidate))
217
+ if (xopts) break
218
+ }
219
+ if (!xopts) {
220
+ throw new Error(
221
+ `${fromJson} has no x_options; the snapshot predates redpanda#31520. ` +
222
+ 'Regenerate with --ref/--from-source/--rpk-bin instead.'
223
+ )
224
+ }
225
+ source = `snapshot ${path.basename(fromJson)}`
226
+ } else {
227
+ let run
228
+ let cleanupDir = null
229
+ if (rpkBin) {
230
+ run = args => runRpkBinary(rpkBin, args)
231
+ } else if (fromSource && !ref) {
232
+ // Local-source mode with no explicit ref: use the checkout exactly as
233
+ // it stands. prepareSourceFromRef would fetch and check out a ref,
234
+ // silently moving the user's working tree.
235
+ const sourcePath = resolveRpkSourcePath(fromSource)
236
+ run = args => runRpkFromSource(sourcePath, args)
237
+ } else {
238
+ const { prepareSourceFromRef } = require('./rpk-docs-handler.js')
239
+ const sourcePath = prepareSourceFromRef(ref || 'dev', fromSource || null)
240
+ if (!fromSource) {
241
+ // prepareSourceFromRef sparse-cloned into a temp dir this run owns;
242
+ // remove it when done so repeated runs don't accumulate clones.
243
+ cleanupDir = tempCloneRoot(sourcePath)
244
+ }
245
+ run = args => runRpkFromSource(sourcePath, args)
246
+ }
247
+
248
+ try {
249
+ // Preferred: structured x_options from the command tree JSON.
250
+ xopts = xOptionsFromTree(run(['--print-tree']))
251
+ source = 'print-tree x_options'
252
+ if (!xopts) {
253
+ // Fallback for rpk versions that predate x_options in --print-tree.
254
+ // The env name is derived locally with the documented mapping rule.
255
+ xopts = parseXList(run(['-X', 'list'])).map(name => ({ name, env: keyToEnvVar(name) }))
256
+ source = '-X list (fallback)'
257
+ }
258
+ } finally {
259
+ if (cleanupDir) {
260
+ fs.rmSync(cleanupDir, { recursive: true, force: true })
261
+ }
262
+ }
263
+ }
264
+
265
+ if (xopts.length < MIN_OPTIONS) {
266
+ throw new Error(
267
+ `Parsed only ${xopts.length} -X options from ${source} (expected at least ${MIN_OPTIONS}); ` +
268
+ 'refusing to write a suspiciously small partial. The output format may have changed.'
269
+ )
270
+ }
271
+
272
+ fs.mkdirSync(path.dirname(output), { recursive: true })
273
+ fs.writeFileSync(output, renderPartial(xopts))
274
+ return { keyCount: xopts.length, output, source }
275
+ }
276
+
277
+ module.exports = {
278
+ xOptionsFromTree,
279
+ parseXList,
280
+ keyToEnvVar,
281
+ keyToAnchor,
282
+ renderPartial,
283
+ handleXEnvPartialGeneration
284
+ }
@@ -7,7 +7,7 @@ const fs = require('fs')
7
7
  const os = require('os')
8
8
  const semver = require('semver')
9
9
  const { findRepoRoot } = require('../../cli-utils/doc-tools-utils')
10
- const { generateRpkDocs, applyOverridesToTree, resolveReferences, shouldExcludeCommand, shouldUsePartialDir } = require('./generate-rpk-docs')
10
+ const { generateRpkDocs, applyOverridesToTree, resolveReferences, shouldExcludeCommand, shouldUsePartialDir, derivePartialsDir } = require('./generate-rpk-docs')
11
11
  const { detectLinuxOnlyFromSource, warnIfDetectionLooksBroken } = require('./detect-platform-commands')
12
12
  const { generateRpkDiff, printDiffReport, generateWhatsNewSection, flattenToMap } = require('./report-delta')
13
13
  const { loadAndValidateOverrides, ValidationResult } = require('./validate-overrides')
@@ -1914,26 +1914,14 @@ async function handleRpkDocsGeneration(options = {}) {
1914
1914
  const defaultOutputDir = path.join(repoRoot, 'modules', 'reference', 'pages', 'rpk')
1915
1915
  const finalOutputDir = outputDir || defaultOutputDir
1916
1916
 
1917
- // cloudSecretDir should be relative to outputDir, not repoRoot
1918
- // If outputDir is .../modules/reference/pages/rpk, cloudSecretDir should be .../modules/reference/partials
1919
- let defaultCloudSecretDir
1920
- if (outputDir) {
1921
- // Check if outputDir follows docs repo structure (ends with pages/rpk or similar)
1922
- const normalizedOutput = finalOutputDir.replace(/\\/g, '/')
1923
- if (normalizedOutput.includes('/pages/')) {
1924
- // Derive cloudSecretDir from outputDir by going up from pages/rpk to partials
1925
- const pagesIndex = normalizedOutput.lastIndexOf('/pages/')
1926
- const referenceDir = normalizedOutput.substring(0, pagesIndex)
1927
- defaultCloudSecretDir = path.join(referenceDir, 'partials')
1928
- } else {
1929
- // Arbitrary output directory - use sibling partials directory
1930
- const outputParent = path.dirname(finalOutputDir)
1931
- defaultCloudSecretDir = path.join(outputParent, 'partials')
1932
- }
1933
- } else {
1934
- defaultCloudSecretDir = path.join(repoRoot, 'modules', 'reference', 'partials')
1935
- }
1936
- const finalCloudSecretDir = cloudSecretDir || defaultCloudSecretDir
1917
+ // Partials live relative to outputDir, not repoRoot: for the docs layout
1918
+ // (.../modules/reference/pages/rpk) that is .../modules/reference/partials,
1919
+ // and for an arbitrary --output-dir it is a partials dir beside the output.
1920
+ // derivePartialsDir owns that rule so each consumer does not re-derive it.
1921
+ const derivedPartialsDir = outputDir
1922
+ ? derivePartialsDir(finalOutputDir)
1923
+ : path.join(repoRoot, 'modules', 'reference', 'partials')
1924
+ const finalCloudSecretDir = cloudSecretDir || derivedPartialsDir
1937
1925
 
1938
1926
  // nav.adoc lives at modules/ROOT/nav.adoc relative to the repo root
1939
1927
  const navFile = path.join(repoRoot, 'modules', 'ROOT', 'nav.adoc')
@@ -2139,6 +2127,7 @@ async function handleRpkDocsGeneration(options = {}) {
2139
2127
  overrides: overridesData,
2140
2128
  outputDir: finalOutputDir,
2141
2129
  cloudSecretDir: finalCloudSecretDir,
2130
+ envPartialDir: derivedPartialsDir,
2142
2131
  rpkVersion,
2143
2132
  pluginVersions,
2144
2133
  draftMissing,
@@ -2526,6 +2515,7 @@ async function handleRpkDocsGeneration(options = {}) {
2526
2515
  overrides: overridesData,
2527
2516
  outputDir: finalOutputDir,
2528
2517
  cloudSecretDir: finalCloudSecretDir,
2518
+ envPartialDir: derivedPartialsDir,
2529
2519
  rpkVersion,
2530
2520
  pluginVersions,
2531
2521
  draftMissing,
@@ -23,12 +23,13 @@ const VALIDATION_RULES = [
23
23
  let match
24
24
  while ((match = urlPattern.exec(content)) !== null) {
25
25
  const url = match[1]
26
- // Check if it's a docs.redpanda.com URL (should always be xref)
26
+ // docs.redpanda.com URLs are converted to xrefs at site build time
27
+ // by the url-to-xref Antora extension, so a raw one is informational
27
28
  if (url.includes('docs.redpanda.com')) {
28
29
  issues.push({
29
30
  line: getLineNumber(content, match.index),
30
- message: `Raw docs.redpanda.com URL should be an xref: ${url}`,
31
- severity: 'error'
31
+ message: `Raw docs.redpanda.com URL (converted to an xref by the url-to-xref extension at build time): ${url}`,
32
+ severity: 'info'
32
33
  })
33
34
  }
34
35
  }
@@ -1,58 +0,0 @@
1
- const { URL } = require('url');
2
-
3
- /**
4
- * Converts a docs.redpanda.com URL, optionally suffixed with a label in brackets, into an Antora xref resource ID string.
5
- *
6
- * If the input includes a label in square brackets (for example, `[Label]`), the label is preserved and appended to the resulting xref.
7
- *
8
- * @param {string} input - A docs.redpanda.com URL, optionally followed by a label in square brackets.
9
- * @returns {string} The corresponding Antora xref resource ID, with the label preserved if present.
10
- *
11
- * @throws {Error} If the input is not a valid URL or does not belong to docs.redpanda.com.
12
- */
13
- function urlToXref(input) {
14
- // Peel off an optional “[label]”
15
- let urlPart = input;
16
- let label = '';
17
- const mLabel = input.match(/^(.*)\[([^\]]+)\]$/);
18
- if (mLabel) {
19
- urlPart = mLabel[1];
20
- label = mLabel[2];
21
- }
22
-
23
- //Parse & validate
24
- let url;
25
- try {
26
- url = new URL(urlPart);
27
- } catch {
28
- throw new Error(`Invalid URL: ${input}`);
29
- }
30
- if (!/docs\.redpanda\.com$/.test(url.hostname)) {
31
- throw new Error(`Not a docs.redpanda.com URL: ${input}`);
32
- }
33
-
34
- // Strip any leading “/docs”, “/docs/vX.Y”, “/docs/current”, “/vX.Y” or “/current”
35
- let p = url.pathname.replace(
36
- /^\/(?:docs(?:\/(?:v?\d+\.\d+|current))?|v?\d+\.\d+|current)\/?/,
37
- ''
38
- );
39
- // Drop trailing slash
40
- p = p.replace(/\/$/, '');
41
- const segments = p.split('/').filter(Boolean);
42
-
43
- // Build module + path + .adoc
44
- let xref;
45
- if (segments.length === 0) {
46
- xref = 'xref:index.adoc';
47
- } else {
48
- const moduleName = segments.shift();
49
- const pagePath = segments.join('/');
50
- const fileName = (pagePath || moduleName) + '.adoc';
51
- xref = `xref:${moduleName}:${fileName}`;
52
- }
53
-
54
- // Re-attach label if there was one
55
- return label ? `${xref}[${label}]` : xref;
56
- }
57
-
58
- module.exports = { urlToXref };