Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"update:integrations": "tsx ./scripts/update-integrations.ts",
"update:ts-api": "tsx ./scripts/update-ts-api.ts",
"update:github-stats": "tsx ./scripts/update-github-stats.ts",
"update:samples": "tsx ./scripts/update-samples.ts"
"update:samples": "tsx ./scripts/update-samples.ts",
"normalize:api-data": "tsx ./scripts/normalize-generated-api-data.ts"
},
"dependencies": {
"@astro-community/astro-embed-vimeo": "^0.3.12",
Expand Down
250 changes: 250 additions & 0 deletions src/frontend/scripts/aspire-terminology.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,256 @@ export function normalizeAspireTerminology(
return normalizeProse(text);
}

// Normalize terminology inside C#/TypeScript code comments. The scanner skips
// string, char, and template literals so their contents and executable code stay
// byte-for-byte identical.
export function normalizeAspireTerminologyInCode(code: string): string;
export function normalizeAspireTerminologyInCode(
code: string | null | undefined
): string | null | undefined;
export function normalizeAspireTerminologyInCode(
code: string | null | undefined
): string | null | undefined {
if (code == null) {
return code;
}

let result = '';
let lastIndex = 0;
let index = 0;

while (index < code.length) {
const commentEnd = findCommentEnd(code, index);
if (commentEnd !== undefined) {
result += code.slice(lastIndex, index);
result += normalizeProse(code.slice(index, commentEnd));
lastIndex = commentEnd;
index = commentEnd;
continue;
}

// Skip complete C#/TypeScript literals so comment-like text inside them is
// never rewritten. Interpolated literals scan nested expressions to find the
// real closing delimiter rather than stopping at an expression's string, and
// report any comments found inside those expressions so real code comments
// are still normalized while surrounding literal bytes stay identical.
const commentSpans: CommentSpan[] = [];
const literalEnd = findLiteralEnd(code, index, commentSpans);
if (literalEnd === undefined) {
index += 1;
continue;
}

for (const [spanStart, spanEnd] of commentSpans) {
result += code.slice(lastIndex, spanStart);
result += normalizeProse(code.slice(spanStart, spanEnd));
lastIndex = spanEnd;
}

index = literalEnd;
}

return result + code.slice(lastIndex);
}

// Half-open `[start, end)` range of a comment discovered inside an interpolation
// expression, collected so the caller can normalize just those spans.
type CommentSpan = readonly [start: number, end: number];

function findCommentEnd(code: string, index: number): number | undefined {
if (code.startsWith('//', index)) {
const lineEnd = code.indexOf('\n', index + 2);
return lineEnd === -1 ? code.length : lineEnd;
}

if (code.startsWith('/*', index)) {
const blockEnd = code.indexOf('*/', index + 2);
return blockEnd === -1 ? code.length : blockEnd + 2;
}

return undefined;
}

function findLiteralEnd(
code: string,
index: number,
commentSpans: CommentSpan[]
): number | undefined {
const rawStringEnd = findCSharpRawStringEnd(code, index);
if (rawStringEnd !== undefined) {
return rawStringEnd;
}

if (code.startsWith('$@"', index) || code.startsWith('@$"', index)) {
return findCSharpInterpolatedStringEnd(code, index + 3, true, commentSpans);
}

if (code.startsWith('$"', index)) {
return findCSharpInterpolatedStringEnd(code, index + 2, false, commentSpans);
}

if (code.startsWith('@"', index)) {
return findCSharpVerbatimStringEnd(code, index + 2);
}

const delimiter = code[index];
if (delimiter === '"' || delimiter === "'") {
return findEscapedStringEnd(code, index + 1, delimiter);
}

if (delimiter === '`') {
return findTypeScriptTemplateEnd(code, index + 1, commentSpans);
}

return undefined;
}

function findCSharpRawStringEnd(code: string, index: number): number | undefined {
let delimiterStart = index;
while (code[delimiterStart] === '$') {
delimiterStart++;
}

let quoteCount = 0;
while (code[delimiterStart + quoteCount] === '"') {
quoteCount++;
}

if (quoteCount < 3) {
return undefined;
}

const delimiter = '"'.repeat(quoteCount);
const closingStart = code.indexOf(delimiter, delimiterStart + quoteCount);
return closingStart === -1 ? code.length : closingStart + quoteCount;
}

function findCSharpInterpolatedStringEnd(
code: string,
index: number,
verbatim: boolean,
commentSpans: CommentSpan[]
): number {
while (index < code.length) {
if (!verbatim && code[index] === '\\') {
index += 2;
continue;
}

if (code[index] === '"') {
if (verbatim && code[index + 1] === '"') {
index += 2;
continue;
}
return index + 1;
}

if (code[index] === '{') {
if (code[index + 1] === '{') {
index += 2;
} else {
index = findInterpolationExpressionEnd(code, index + 1, commentSpans);
}
continue;
}

if (code[index] === '}' && code[index + 1] === '}') {
index += 2;
continue;
}

index++;
}

return code.length;
}

function findTypeScriptTemplateEnd(
code: string,
index: number,
commentSpans: CommentSpan[]
): number {
while (index < code.length) {
if (code[index] === '\\') {
index += 2;
} else if (code[index] === '`') {
return index + 1;
} else if (code[index] === '$' && code[index + 1] === '{') {
index = findInterpolationExpressionEnd(code, index + 2, commentSpans);
} else {
index++;
}
}

return code.length;
}

function findInterpolationExpressionEnd(
code: string,
index: number,
commentSpans: CommentSpan[]
): number {
let braceDepth = 1;

while (index < code.length) {
const commentEnd = findCommentEnd(code, index);
if (commentEnd !== undefined) {
// A comment inside an interpolation expression is executable-code prose,
// not string content, so record it for normalization by the caller.
commentSpans.push([index, commentEnd]);
index = commentEnd;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Normalize comments nested inside interpolation expressions

findInterpolationExpressionEnd advances past a nested comment without sending it through normalizeProse, and the outer scan then skips the complete interpolated literal. As a result, both of these valid inputs retain the forbidden term unchanged:

var value = $"{Get(/* app host */ 1)}";
const value = `${Get(/* app host */ 1)}`;

I reproduced both directly against this head. Such comments are executable-code comments, not string contents, so they need normalization for appHostCode to satisfy the helper's contract and avoid the forbidden-words failure. Please normalize comment spans encountered during interpolation traversal while continuing to preserve nested literals, and add both cases as regression tests.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c8ea7be. Comments nested inside C# interpolation and TypeScript template expressions now pass through normalizeProse, while the surrounding literal stays byte-for-byte identical. Both of your reproductions are covered as regression cases (var value = $"{Get(/* app host */ 1)}"; and the TS template equivalent), plus a case pairing an interpolation comment with a preserved nested string literal.

continue;
}

const literalEnd = findLiteralEnd(code, index, commentSpans);
if (literalEnd !== undefined) {
index = literalEnd;
continue;
}

if (code[index] === '{') {
braceDepth++;
} else if (code[index] === '}') {
braceDepth--;
if (braceDepth === 0) {
return index + 1;
}
}

index++;
}

return code.length;
}

function findCSharpVerbatimStringEnd(code: string, index: number): number {
while (index < code.length) {
if (code[index] !== '"') {
index++;
} else if (code[index + 1] === '"') {
index += 2;
} else {
return index + 1;
}
}

return code.length;
}

function findEscapedStringEnd(code: string, index: number, delimiter: string): number {
while (index < code.length) {
if (code[index] === '\\') {
index += 2;
} else if (code[index] === delimiter) {
return index + 1;
} else {
index++;
}
}

return code.length;
}

// Apply every terminology rule to prose only, copying fenced and inline code
// regions through untouched so sample commands stay runnable.
function normalizeProse(text: string): string {
Expand Down
Loading
Loading