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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "github-mobile-reader",
"version": "0.1.6",
"version": "0.1.7",
"description": "Transform git diffs into mobile-friendly Markdown — no more horizontal scrolling when reviewing code on your phone.",
"keywords": [
"github",
Expand Down
148 changes: 141 additions & 7 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export type SymbolKind =
export interface SymbolDiff {
name: string;
kind: SymbolKind;
status: "added" | "removed" | "modified";
status: "added" | "removed" | "modified" | "moved";
addedLines: string[];
removedLines: string[];
}
Expand All @@ -66,6 +66,97 @@ export interface PropsChange {
removed: string[];
}

// ── Test file helpers ──────────────────────────────────────────────────────────

/**
* Returns true if the filename looks like a test/spec file.
*/
export function isTestFile(filename: string): boolean {
return /\.(test|spec)\.(js|jsx|ts|tsx)$/.test(filename);
}

/**
* Returns true if the filename looks like a config file (vitest, jest, etc.)
*/
export function isConfigFile(filename: string): boolean {
return /(?:vitest|jest|vite|tsconfig|eslint|prettier|babel|webpack|rollup)\.config\.(js|ts|cjs|mjs)$/.test(filename)
|| /\.config\.(js|ts|cjs|mjs)$/.test(filename);
}

interface TestCase {
suite: string; // describe block name
name: string; // it/test block name
}

/**
* Extract describe/it/test block names from raw diff lines.
*/
export function extractTestCases(addedLines: string[]): TestCase[] {
const results: TestCase[] = [];
let currentSuite = "";

for (const line of addedLines) {
const t = line.trim();

// describe('suite name', ...) or describe("suite name", ...)
const suiteMatch = t.match(/^describe\s*\(\s*['"`](.+?)['"`]/);
if (suiteMatch) {
currentSuite = suiteMatch[1];
continue;
}

// it('test name', ...) or test('test name', ...)
const caseMatch = t.match(/^(?:it|test)\s*\(\s*['"`](.+?)['"`]/);
if (caseMatch) {
results.push({ suite: currentSuite, name: caseMatch[1] });
}
}

return results;
}

/**
* Generate a readable markdown summary for a test file diff.
* Groups test cases by suite and lists them clearly.
*/
export function generateTestFileSummary(
addedLines: string[],
removedLines: string[],
): string[] {
const sections: string[] = [];

const addedCases = extractTestCases(addedLines);
const removedCases = extractTestCases(removedLines);

// Group added cases by suite
const suiteMap = new Map<string, string[]>();
for (const { suite, name } of addedCases) {
const key = suite || "(root)";
if (!suiteMap.has(key)) suiteMap.set(key, []);
suiteMap.get(key)!.push(name);
}

if (suiteMap.size > 0) {
for (const [suite, cases] of suiteMap) {
sections.push(`**테스트: \`${suite}\`**`);
cases.forEach((c) => sections.push(` + ${c}`));
sections.push("");
}
}

// Removed test cases
if (removedCases.length > 0) {
sections.push("**제거된 테스트**");
removedCases.forEach(({ suite, name }) => {
const label = suite ? `${suite} > ${name}` : name;
sections.push(` - ${label}`);
});
sections.push("");
}

return sections;
}

// ── JSX / Tailwind helpers ─────────────────────────────────────────────────────

export function isJSXFile(filename: string): boolean {
Expand Down Expand Up @@ -1067,8 +1158,8 @@ export function generateSymbolSections(
isJSX: boolean,
): string[] {
const sections: string[] = [];
const STATUS_ICON = { added: "✅", removed: "❌", modified: "✏️" };
const STATUS_LABEL = { added: "새로 추가", removed: "제거됨", modified: "변경됨" };
const STATUS_ICON = { added: "✅", removed: "❌", modified: "✏️", moved: "📦" };
const STATUS_LABEL = { added: "새로 추가", removed: "제거됨", modified: "변경됨", moved: "다른 파일로 이동됨" };

// Walk the list in order: attach each setup variable to the nearest
// preceding significant symbol so it appears as an inline Context line.
Expand Down Expand Up @@ -1122,11 +1213,11 @@ export function generateSymbolSections(
props.added.forEach((p) => lines.push(`Props+ \`${abbreviateProp(p)}\``));
props.removed.forEach((p) => lines.push(`Props- \`${abbreviateProp(p)}\``));

// Behavioral summary
if (sym.status !== "removed") {
// Behavioral summary (skip for moved — content lives in the destination file)
if (sym.status !== "removed" && sym.status !== "moved") {
buildBehaviorSummary(sym.addedLines).forEach((l) => lines.push(`+ ${l}`));
}
if (sym.status !== "added" && sym.removedLines.length > 0) {
if (sym.status !== "added" && sym.status !== "moved" && sym.removedLines.length > 0) {
buildBehaviorSummary(sym.removedLines, "removed").slice(0, 4)
.forEach((l) => lines.push(`- ${l}`));
}
Expand Down Expand Up @@ -1224,6 +1315,41 @@ export function generateReaderMarkdown(
): string {
const { added, removed } = filterDiffLines(diffText);

// ── Test file shortcut ───────────────────────────────────
if (meta.file && isTestFile(meta.file)) {
const sections: string[] = [];
sections.push(...generateTestFileSummary(added, removed));
sections.push("---");
sections.push(
"🛠 Auto-generated by [github-mobile-reader](https://github.com/3rdflr/github-mobile-reader). Do not edit manually.",
);
return sections.join("\n");
}

// ── Config file shortcut ──────────────────────────────────
if (meta.file && isConfigFile(meta.file)) {
const sections: string[] = [];
const configImports = extractImportChanges(added, removed);
if (configImports.added.length > 0) {
sections.push("**플러그인/설정 추가**");
configImports.added.forEach((i) => sections.push(`+ \`${i}\``));
sections.push("");
}
if (configImports.removed.length > 0) {
sections.push("**플러그인/설정 제거**");
configImports.removed.forEach((i) => sections.push(`- \`${i}\``));
sections.push("");
}
if (sections.length === 0) {
sections.push("설정값 변경");
}
sections.push("---");
sections.push(
"🛠 Auto-generated by [github-mobile-reader](https://github.com/3rdflr/github-mobile-reader). Do not edit manually.",
);
return sections.join("\n");
}

// ── Detect JSX mode ──────────────────────────────────────
const isJSX = Boolean(
(meta.file && isJSXFile(meta.file)) || hasJSXContent(added),
Expand All @@ -1233,10 +1359,18 @@ export function generateReaderMarkdown(
const hunks = parseDiffHunks(diffText);
const symbolDiffs = attributeLinesToSymbols(hunks);

// ── Detect moved symbols (removed here, imported elsewhere) ──
const fileImportChanges = extractImportChanges(added, removed);
const newlyImported = new Set(fileImportChanges.added);
for (const sym of symbolDiffs) {
if (sym.status === "removed" && newlyImported.has(sym.name)) {
sym.status = "moved";
}
}

const sections: string[] = [];

// ── File-level import changes ─────────────────────────────
const fileImportChanges = extractImportChanges(added, removed);
if (fileImportChanges.added.length > 0 || fileImportChanges.removed.length > 0) {
sections.push("**Import 변화**");
fileImportChanges.added.forEach((i) => sections.push(`+ \`${i}\``));
Expand Down