Skip to content

Commit ce61a20

Browse files
omarqureshiclaude
andcommitted
feat(ruby): tidy rendered examples/READMEs (inline refs, HTML comments, requires)
Three display-layer fixes for translated docs prose: - rubifyInlineRefs: translate inline `code` API references in prose to snake_case (`bucketArn` -> `bucket_arn`, `arnForObjects(pattern)` -> `arn_for_objects(pattern)`), matching the real member names. Literals/types/enums are left alone. - Strip HTML comments (CDK's `<!--BEGIN STABILITY BANNER-->` / CFNONLY markers), which YARD's Markdown renderer would otherwise show as visible text. - Strip `require` boilerplate from examples: it comes from the snippet fixture and, after import translation, collapses to duplicate `require 'aws-cdk-lib'` lines. Applied to @example (convertExample) and README/remarks fenced blocks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d440471 commit ce61a20

2 files changed

Lines changed: 76 additions & 6 deletions

File tree

packages/jsii-pacmak/lib/targets/ruby.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,21 @@ interface MemberLike {
166166
* packages/@jsii/ruby-runtime-test/spec/unit/utils_spec.rb), so kernel
167167
* callbacks dispatch to the renamed member.
168168
*/
169+
// A `require` / `require_relative` line in a rendered example. These come from the
170+
// snippet's fixture (import boilerplate) and are noise in the API docs — and after
171+
// import translation, several imports from one package collapse to duplicate
172+
// `require 'aws-cdk-lib'` lines. They're stripped before display.
173+
const REQUIRE_LINE = /^\s*require(?:_relative)?\s+['"][^'"]+['"];?\s*$/;
174+
175+
/** Drop `require` lines from a standalone code snippet and any blank lines they leave. */
176+
function stripRequireLines(code: string): string {
177+
return code
178+
.split('\n')
179+
.filter((line) => !REQUIRE_LINE.test(line))
180+
.join('\n')
181+
.replace(/^\s*\n+/, '');
182+
}
183+
169184
const RUBY_RESERVED_NAMES = new Set([
170185
// Keywords
171186
'alias',
@@ -241,7 +256,7 @@ export class RubyGenerator extends Generator {
241256
TargetLanguage.RUBY,
242257
enforcesStrictMode(this.assembly),
243258
);
244-
return translated.source;
259+
return stripRequireLines(translated.source);
245260
}
246261

247262
/**
@@ -252,12 +267,67 @@ export class RubyGenerator extends Generator {
252267
*/
253268
private convertMarkdown(markdown: string, apiLocation: ApiLocation): string {
254269
assertSpecIsRosettaCompatible(this.assembly);
255-
return this.rosetta.translateSnippetsInMarkdown(
270+
// Strip HTML comments (e.g. CDK's `<!--BEGIN STABILITY BANNER-->` / CFNONLY
271+
// markers): YARD's Markdown renderer emits them as visible text instead of hiding
272+
// them, so they'd otherwise show up verbatim in the rendered README.
273+
const cleaned = markdown.replace(/<!--[\s\S]*?-->/g, '');
274+
const translated = this.rosetta.translateSnippetsInMarkdown(
256275
apiLocation,
257-
markdown,
276+
cleaned,
258277
TargetLanguage.RUBY,
259278
enforcesStrictMode(this.assembly),
260279
);
280+
return this.rubifyInlineRefs(translated);
281+
}
282+
283+
/**
284+
* Rosetta translates fenced code blocks, but not inline `code` references in the
285+
* prose (e.g. the `bucketArn` / `arnForObjects(pattern)` list in the S3 README).
286+
* Rewrite those to Ruby: a bare camelCase identifier — optionally followed by a
287+
* call's parentheses — becomes snake_case via the same `toSnakeCase` the member
288+
* generator uses, so `bucketArn` -> `bucket_arn` and `arnForObjects(pattern)` ->
289+
* `arn_for_objects(pattern)`. Everything else is left untouched: fenced blocks
290+
* (already translated), and anything that isn't a bare identifier — ARNs, URLs and
291+
* names (they carry `:` `/` `-` `.`), PascalCase type names, ALL_CAPS enum members.
292+
*/
293+
private rubifyInlineRefs(markdown: string): string {
294+
let inFence = false;
295+
let fenceStarted = false; // seen real content since the fence opened
296+
const out: string[] = [];
297+
for (const line of markdown.split('\n')) {
298+
if (/^\s*(```|~~~)/.test(line)) {
299+
inFence = !inFence;
300+
fenceStarted = false;
301+
out.push(line);
302+
continue;
303+
}
304+
if (inFence) {
305+
// Drop `require` boilerplate from code samples, plus the leading blank lines
306+
// it leaves at the top of a block.
307+
if (REQUIRE_LINE.test(line)) {
308+
continue;
309+
}
310+
if (!fenceStarted && line.trim() === '') {
311+
continue;
312+
}
313+
fenceStarted = true;
314+
out.push(line);
315+
continue;
316+
}
317+
out.push(
318+
line.replace(/`([^`\n]+)`/g, (whole, inner: string) => {
319+
const m = /^([a-z][A-Za-z0-9]*)(\([A-Za-z0-9_,\s]*\))?$/.exec(inner);
320+
if (!m || !/[A-Z]/.test(m[1])) {
321+
return whole;
322+
}
323+
const call = m[2]
324+
? m[2].replace(/[A-Za-z][A-Za-z0-9]*/g, (arg) => toSnakeCase(arg))
325+
: '';
326+
return `\`${toSnakeCase(m[1])}${call}\``;
327+
}),
328+
);
329+
}
330+
return out.join('\n');
261331
}
262332

263333
/**

packages/jsii-pacmak/test/generated-code/__snapshots__/target-ruby.test.js.snap

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)