From 13754f87d06c698993adc951bb01d66240a775f0 Mon Sep 17 00:00:00 2001 From: 3rdflr <3rdflrhtl@gmail.com> Date: Mon, 23 Feb 2026 00:43:10 +0900 Subject: [PATCH 1/2] 0.1.7 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b5448f0..9715380 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "github-mobile-reader", - "version": "0.1.6", + "version": "0.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "github-mobile-reader", - "version": "0.1.6", + "version": "0.1.7", "license": "MIT", "devDependencies": { "@types/node": "^20.0.0", diff --git a/package.json b/package.json index b4d169e..1855007 100644 --- a/package.json +++ b/package.json @@ -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", From 24e41d0e88173f4531c7b7b1ff35dca0931a063a Mon Sep 17 00:00:00 2001 From: 3rdflr <3rdflrhtl@gmail.com> Date: Mon, 23 Feb 2026 14:48:45 +0900 Subject: [PATCH 2/2] feat: add test file parser, config file handler, and moved symbol detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isTestFile / isConfigFile: detect .test.ts and *.config.ts filenames - extractTestCases: parse describe/it blocks from diff lines - generateTestFileSummary: group test cases by suite with +/- markers - generateReaderMarkdown: route test/config files to dedicated handlers before the standard symbol pipeline - moved detection: symbols removed from a file but appearing in newly added imports are reclassified as πŸ“¦ moved rather than ❌ removed - STATUS_ICON/LABEL: add 'moved' entry (πŸ“¦ / λ‹€λ₯Έ 파일둜 이동됨) Co-Authored-By: Claude Sonnet 4.6 --- src/parser.ts | 148 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 141 insertions(+), 7 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 4493e1f..33eff20 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -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[]; } @@ -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(); + 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 { @@ -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. @@ -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}`)); } @@ -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), @@ -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}\``));