154 lines · 4.5 KB · TypeScript
RawDownload
export interface ReviewCommentContext {  readonly id: string;  readonly sectionId: string;  readonly sectionTitle: string;  readonly filePath: string;  readonly startIndex: number;  readonly endIndex: number;  readonly rangeLabel: string;  readonly text: string;  readonly diff: string;} export type ReviewCommentMessageSegment =  | {      readonly kind: "text";      readonly id: string;      readonly text: string;    }  | {      readonly kind: "review-comment";      readonly comment: ReviewCommentContext;    }; const REVIEW_COMMENT_BLOCK_PATTERN = /<review_comment\b([^>]*)>\s*([\s\S]*?)<\/review_comment>/g;const REVIEW_COMMENT_ATTRIBUTE_PATTERN = /([a-zA-Z][a-zA-Z0-9_-]*)="([^"]*)"/g;const REVIEW_COMMENT_DIFF_FENCE_PATTERN = /```diff\s*\n([\s\S]*?)\n```/; function unescapeReviewCommentAttribute(value: string): string {  return value    .replace(/&lt;/g, "<")    .replace(/&quot;/g, '"')    .replace(/&amp;/g, "&");} function readReviewCommentAttributes(rawAttributes: string): Record<string, string> {  const attributes: Record<string, string> = {};  for (const match of rawAttributes.matchAll(REVIEW_COMMENT_ATTRIBUTE_PATTERN)) {    attributes[match[1]!] = unescapeReviewCommentAttribute(match[2] ?? "");  }  return attributes;} function readNonNegativeInteger(value: string | undefined): number | null {  if (value === undefined || !/^\d+$/.test(value)) {    return null;  }  return Number(value);} function extractReviewCommentText(rawBody: string): string {  const fenceIndex = rawBody.indexOf("```diff");  const commentBody = fenceIndex >= 0 ? rawBody.slice(0, fenceIndex) : rawBody;  return commentBody.trim();} function extractReviewCommentDiff(rawBody: string): string {  return rawBody.match(REVIEW_COMMENT_DIFF_FENCE_PATTERN)?.[1]?.trim() ?? "";} function parseReviewCommentContext(  rawAttributes: string,  rawBody: string,  index: number,): ReviewCommentContext | null {  const attributes = readReviewCommentAttributes(rawAttributes);  const startIndex = readNonNegativeInteger(attributes.startIndex);  const endIndex = readNonNegativeInteger(attributes.endIndex);  const filePath = attributes.filePath?.trim();  const sectionId = attributes.sectionId?.trim();  if (!filePath || !sectionId || startIndex === null || endIndex === null) {    return null;  }   return {    id: `review-comment:${index}:${sectionId}:${filePath}:${startIndex}:${endIndex}`,    sectionId,    sectionTitle: attributes.sectionTitle?.trim() || "Review",    filePath,    startIndex: Math.min(startIndex, endIndex),    endIndex: Math.max(startIndex, endIndex),    rangeLabel: attributes.rangeLabel?.trim() || "line",    text: extractReviewCommentText(rawBody),    diff: extractReviewCommentDiff(rawBody),  };} export function parseReviewCommentMessageSegments(  value: string,): ReadonlyArray<ReviewCommentMessageSegment> {  const segments: ReviewCommentMessageSegment[] = [];  let cursor = 0;  let parsedCommentIndex = 0;   for (const match of value.matchAll(REVIEW_COMMENT_BLOCK_PATTERN)) {    const matchIndex = match.index ?? 0;    const beforeText = value.slice(cursor, matchIndex);    if (beforeText.length > 0) {      segments.push({        kind: "text",        id: `review-comment-text:${cursor}`,        text: beforeText,      });    }     const comment = parseReviewCommentContext(match[1] ?? "", match[2] ?? "", parsedCommentIndex);    if (comment) {      segments.push({ kind: "review-comment", comment });      parsedCommentIndex += 1;    } else {      segments.push({        kind: "text",        id: `review-comment-invalid:${matchIndex}`,        text: match[0],      });    }     cursor = matchIndex + match[0].length;  }   const rest = value.slice(cursor);  if (rest.length > 0) {    segments.push({      kind: "text",      id: `review-comment-text:${cursor}`,      text: rest,    });  }   return segments;} export function hasReviewCommentMessageSegments(value: string): boolean {  return parseReviewCommentMessageSegments(value).some(    (segment) => segment.kind === "review-comment",  );} export function buildReviewCommentRenderablePatch(comment: ReviewCommentContext): string {  const diff = comment.diff.trim();  if (diff.length === 0) {    return "";  }  if (diff.startsWith("diff --git ")) {    return diff;  }   const normalizedPath = comment.filePath.replaceAll("\\", "/");  return [    `diff --git a/${normalizedPath} b/${normalizedPath}`,    `--- a/${normalizedPath}`,    `+++ b/${normalizedPath}`,    diff,  ].join("\n");}