github-mobile-reader 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -40,9 +40,13 @@ data
40
40
 
41
41
  - **의존성 제로 코어** — 파서는 Node.js ≥ 18이 있는 어디서나 동작합니다
42
42
  - **이중 출력 포맷** — CJS (`require`)와 ESM (`import`) 모두 지원, TypeScript 타입 포함
43
+ - **CLI** — `npx github-mobile-reader --repo owner/repo --pr 42` 로 어떤 PR이든 즉시 변환
43
44
  - **GitHub Action** — 레포에 YAML 파일 하나만 추가하면 PR마다 Reader 문서가 자동 생성됩니다
45
+ - **파일별 분리 출력** — 변경된 JS/TS 파일마다 독립적인 섹션으로 출력
46
+ - **JSX/Tailwind 인식** — `.jsx`/`.tsx` 파일은 컴포넌트 트리(`🎨 JSX Structure`)와 Tailwind 클래스 diff(`💅 Style Changes`)를 별도 섹션으로 분리 출력
44
47
  - **양방향 diff 추적** — 추가된 코드와 삭제된 코드를 각각 별도 섹션으로 표시
45
48
  - **보수적 설계** — 패턴이 애매할 때는 잘못된 정보를 보여주는 대신 덜 보여줍니다
49
+ - **보안 기본값** — 토큰은 `$GITHUB_TOKEN` 환경변수로만 읽음 — 셸 히스토리나 `ps` 목록에 노출되는 `--token` 플래그 없음
46
50
 
47
51
  ---
48
52
 
@@ -50,13 +54,75 @@ data
50
54
 
51
55
  1. [빠른 시작](#빠른-시작)
52
56
  2. [언어 지원](#언어-지원)
53
- 3. [GitHub Action (권장)](#github-action-권장)
54
- 4. [npm 라이브러리 사용법](#npm-라이브러리-사용법)
55
- 5. [출력 형식](#출력-형식)
56
- 6. [API 레퍼런스](#api-레퍼런스)
57
- 7. [파서 동작 원리](#파서-동작-원리)
58
- 8. [기여하기](#기여하기)
59
- 9. [라이선스](#라이선스)
57
+ 3. [CLI 사용법](#cli-사용법)
58
+ 4. [GitHub Action (권장)](#github-action-권장)
59
+ 5. [npm 라이브러리 사용법](#npm-라이브러리-사용법)
60
+ 6. [출력 형식](#출력-형식)
61
+ 7. [API 레퍼런스](#api-레퍼런스)
62
+ 8. [파서 동작 원리](#파서-동작-원리)
63
+ 9. [기여하기](#기여하기)
64
+ 10. [라이선스](#라이선스)
65
+
66
+ ---
67
+
68
+ ## CLI 사용법
69
+
70
+ 터미널에서 `github-mobile-reader`를 바로 실행할 수 있습니다 — 별도 설정 파일 불필요. GitHub에서 PR diff를 받아 모바일 친화적인 Markdown으로 변환하고 `./reader-output/`에 PR별로 파일을 저장합니다.
71
+
72
+ ### 인증 (토큰 설정)
73
+
74
+ CLI 실행 **전에** 환경변수로 GitHub 토큰을 설정하세요:
75
+
76
+ ```bash
77
+ export GITHUB_TOKEN=ghp_xxxx
78
+ npx github-mobile-reader --repo owner/repo --pr 42
79
+ ```
80
+
81
+ > **보안 안내:** CLI는 `--token` 플래그를 지원하지 않습니다. 커맨드라인 인자로 시크릿을 전달하면 셸 히스토리와 `ps` 출력에 토큰이 노출됩니다. 반드시 환경변수를 사용하세요.
82
+
83
+ ### 단일 PR
84
+
85
+ ```bash
86
+ npx github-mobile-reader --repo owner/repo --pr 42
87
+ ```
88
+
89
+ ### 최근 PR 전체
90
+
91
+ ```bash
92
+ npx github-mobile-reader --repo owner/repo --all
93
+ ```
94
+
95
+ ### 옵션
96
+
97
+ | 플래그 | 기본값 | 설명 |
98
+ | ----------- | ------------------ | ------------------------------------------------------- |
99
+ | `--repo` | *(필수)* | `owner/repo` 형식의 레포지토리 |
100
+ | `--pr` | — | 특정 PR 번호 하나 처리 |
101
+ | `--all` | — | 최근 PR 전체 처리 (`--limit`와 함께 사용) |
102
+ | `--out` | `./reader-output` | 생성된 `.md` 파일 저장 경로 — 상대 경로만 허용, `..` 불가 |
103
+ | `--limit` | `10` | `--all` 사용 시 가져올 PR 최대 개수 |
104
+
105
+ 토큰: `$GITHUB_TOKEN` 환경변수에서 읽음 (미인증 시 60 req/hr, 인증 시 5 000 req/hr).
106
+
107
+ ### 출력 결과
108
+
109
+ PR마다 `reader-output/pr-<번호>.md` 파일 하나가 생성됩니다.
110
+
111
+ JSX/TSX 파일은 추가 섹션이 생성됩니다:
112
+
113
+ ```
114
+ # 📖 PR #42 — My Feature
115
+
116
+ ## 📄 `src/App.tsx`
117
+
118
+ ### 🧠 Logical Flow ← JS 로직 트리
119
+ ### 🎨 JSX Structure ← 컴포넌트 계층 구조 (JSX/TSX 전용)
120
+ ### 💅 Style Changes ← 추가/제거된 Tailwind 클래스 (JSX/TSX 전용)
121
+ ### ✅ Added Code
122
+ ### ❌ Removed Code
123
+ ```
124
+
125
+ > **참고:** `reader-output/`는 기본적으로 `.gitignore`에 포함되어 있습니다 — 생성된 파일은 로컬에만 저장되며 레포지토리에 커밋되지 않습니다.
60
126
 
61
127
  ---
62
128
 
@@ -153,7 +219,7 @@ src/languages/
153
219
 
154
220
  이 라이브러리를 사용하는 가장 쉬운 방법입니다. 매 PR마다 자동으로:
155
221
 
156
- 1. 변경된 `.js` / `.ts` 파일의 diff를 파싱
222
+ 1. 변경된 `.js` / `.jsx` / `.ts` / `.tsx` / `.mjs` / `.cjs` 파일의 diff를 파싱
157
223
  2. `docs/reader/pr-<번호>.md` 파일을 레포에 저장
158
224
  3. PR에 요약 코멘트를 자동으로 달아줍니다
159
225
 
@@ -487,8 +553,10 @@ github-mobile-reader/
487
553
  │ ├── parser.ts ← 핵심 diff → logical flow 파서
488
554
  │ ├── index.ts ← npm 공개 API
489
555
  │ ├── action.ts ← GitHub Action 진입점
556
+ │ ├── cli.ts ← CLI 진입점 (npx github-mobile-reader)
490
557
  │ └── test.ts ← 스모크 테스트 (33개)
491
558
  ├── dist/ ← 컴파일 결과물 (자동 생성, 수정 금지)
559
+ ├── reader-output/ ← CLI 출력 디렉토리 (gitignore됨)
492
560
  ├── .github/
493
561
  │ └── workflows/
494
562
  │ └── mobile-reader.yml ← 사용자용 예시 워크플로우
package/README.md CHANGED
@@ -38,9 +38,13 @@ data
38
38
 
39
39
  - **Zero-dependency core** — the parser runs anywhere Node.js ≥ 18 is available
40
40
  - **Dual output format** — CJS (`require`) and ESM (`import`) with full TypeScript types
41
+ - **CLI** — `npx github-mobile-reader --repo owner/repo --pr 42` fetches and converts any PR instantly
41
42
  - **GitHub Action** — drop one YAML block into any repo and get auto-generated Reader docs on every PR
43
+ - **File-by-file output** — each changed JS/TS file gets its own independent section in the output
44
+ - **JSX/Tailwind aware** — `.jsx`/`.tsx` files get a component tree (`🎨 JSX Structure`) and a Tailwind class diff (`💅 Style Changes`) instead of one unreadable blob
42
45
  - **Tracks both sides of a diff** — shows added _and_ removed code in separate sections
43
46
  - **Conservative by design** — when a pattern is ambiguous, the library shows less rather than showing something wrong
47
+ - **Secure by default** — token is read from `$GITHUB_TOKEN` only; no flag that leaks to shell history or `ps`
44
48
 
45
49
  ---
46
50
 
@@ -48,13 +52,14 @@ data
48
52
 
49
53
  1. [Quick Start](#quick-start)
50
54
  2. [Language Support](#language-support)
51
- 3. [GitHub Action (recommended)](#github-action-recommended)
52
- 4. [npm Library Usage](#npm-library-usage)
53
- 5. [Output Format](#output-format)
54
- 6. [API Reference](#api-reference)
55
- 7. [How the Parser Works](#how-the-parser-works)
56
- 8. [Contributing](#contributing)
57
- 9. [License](#license)
55
+ 3. [CLI Usage](#cli-usage)
56
+ 4. [GitHub Action (recommended)](#github-action-recommended)
57
+ 5. [npm Library Usage](#npm-library-usage)
58
+ 6. [Output Format](#output-format)
59
+ 7. [API Reference](#api-reference)
60
+ 8. [How the Parser Works](#how-the-parser-works)
61
+ 9. [Contributing](#contributing)
62
+ 10. [License](#license)
58
63
 
59
64
  ---
60
65
 
@@ -129,6 +134,67 @@ If you'd like to contribute an adapter for your language, see [Contributing](#co
129
134
 
130
135
  ---
131
136
 
137
+ ## CLI Usage
138
+
139
+ Run `github-mobile-reader` directly from your terminal — no setup, no config file. It fetches a PR diff from GitHub, converts it to mobile-friendly Markdown, and saves one file per PR to `./reader-output/`.
140
+
141
+ ### Authentication
142
+
143
+ Set your GitHub token as an environment variable **before** running the CLI:
144
+
145
+ ```bash
146
+ export GITHUB_TOKEN=ghp_xxxx
147
+ npx github-mobile-reader --repo owner/repo --pr 42
148
+ ```
149
+
150
+ > **Security note:** The CLI does not accept a `--token` flag. Passing secrets as command-line arguments exposes them in shell history and `ps` output. Always use the environment variable.
151
+
152
+ ### Single PR
153
+
154
+ ```bash
155
+ npx github-mobile-reader --repo owner/repo --pr 42
156
+ ```
157
+
158
+ ### All recent PRs
159
+
160
+ ```bash
161
+ npx github-mobile-reader --repo owner/repo --all
162
+ ```
163
+
164
+ ### Options
165
+
166
+ | Flag | Default | Description |
167
+ | --------- | ----------------- | ------------------------------------------------- |
168
+ | `--repo` | *(required)* | Repository in `owner/repo` format |
169
+ | `--pr` | — | Process a single PR by number |
170
+ | `--all` | — | Process all recent PRs (use with `--limit`) |
171
+ | `--out` | `./reader-output` | Output directory — relative paths only, no `..` |
172
+ | `--limit` | `10` | Max number of PRs to fetch when using `--all` |
173
+
174
+ Token: read from `$GITHUB_TOKEN` environment variable (60 req/hr unauthenticated, 5 000 req/hr authenticated).
175
+
176
+ ### Output
177
+
178
+ Each PR produces one file: `reader-output/pr-<number>.md`.
179
+
180
+ Inside that file, every changed JS/TS file gets its own section. JSX/TSX files get two extra sections:
181
+
182
+ ```
183
+ # 📖 PR #42 — My Feature
184
+
185
+ ## 📄 `src/App.tsx`
186
+
187
+ ### 🧠 Logical Flow ← JS logic tree
188
+ ### 🎨 JSX Structure ← component hierarchy (JSX/TSX only)
189
+ ### 💅 Style Changes ← added/removed Tailwind classes (JSX/TSX only)
190
+ ### ✅ Added Code
191
+ ### ❌ Removed Code
192
+ ```
193
+
194
+ > **Note:** `reader-output/` is gitignored by default — the generated files are local only and not committed to your repository.
195
+
196
+ ---
197
+
132
198
  ## Quick Start
133
199
 
134
200
  ```bash
@@ -151,7 +217,7 @@ console.log(markdown);
151
217
 
152
218
  The easiest way to use this library is as a GitHub Action. On every pull request it will:
153
219
 
154
- 1. Parse the diff of all changed `.js` / `.ts` files
220
+ 1. Parse the diff of all changed `.js` / `.jsx` / `.ts` / `.tsx` / `.mjs` / `.cjs` files
155
221
  2. Write a Reader Markdown file to `docs/reader/pr-<number>.md` inside your repo
156
222
  3. Post a summary comment directly on the PR
157
223
 
@@ -482,8 +548,10 @@ github-mobile-reader/
482
548
  ├── src/
483
549
  │ ├── parser.ts ← core diff → logical flow parser
484
550
  │ ├── index.ts ← public npm API surface
485
- └── action.ts ← GitHub Action entry point
551
+ ├── action.ts ← GitHub Action entry point
552
+ │ └── cli.ts ← CLI entry point (npx github-mobile-reader)
486
553
  ├── dist/ ← compiled output (auto-generated, do not edit)
554
+ ├── reader-output/ ← CLI output directory (gitignored)
487
555
  ├── .github/
488
556
  │ └── workflows/
489
557
  │ └── mobile-reader.yml ← example workflow for consumers
package/dist/action.js CHANGED
@@ -28,191 +28,196 @@ var path = __toESM(require("path"));
28
28
  var import_child_process = require("child_process");
29
29
 
30
30
  // src/parser.ts
31
- function filterDiffLines(diffText) {
32
- const lines = diffText.split("\n");
33
- const added = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++") && l.trim() !== "+").map((l) => l.substring(1));
34
- const removed = lines.filter((l) => l.startsWith("-") && !l.startsWith("---") && l.trim() !== "-").map((l) => l.substring(1));
35
- return { added, removed };
31
+ function isJSXFile(filename) {
32
+ return /\.(jsx|tsx)$/.test(filename);
36
33
  }
37
- function normalizeCode(lines) {
38
- return lines.map((line) => {
39
- let normalized = line;
40
- normalized = normalized.replace(/\/\/.*$/, "");
41
- normalized = normalized.replace(/\/\*.*?\*\//, "");
42
- normalized = normalized.trim();
43
- normalized = normalized.replace(/;$/, "");
44
- return normalized;
45
- }).filter((line) => line.length > 0);
34
+ function hasJSXContent(lines) {
35
+ return lines.some((l) => /<[A-Z][A-Za-z]*[\s/>]/.test(l) || /return\s*\(/.test(l));
46
36
  }
47
- function getIndentDepth(line) {
48
- const match = line.match(/^(\s*)/);
49
- if (!match) return 0;
50
- return Math.floor(match[1].length / 2);
37
+ function isClassNameOnlyLine(line) {
38
+ return /^className=/.test(line.trim());
51
39
  }
52
- function isChaining(line, prevLine) {
53
- if (!prevLine) return false;
54
- if (!line.trim().startsWith(".")) return false;
55
- if (!prevLine.match(/[)\}]$/)) return false;
56
- return true;
40
+ function extractClassName(line) {
41
+ const staticMatch = line.match(/className="([^"]*)"/);
42
+ if (staticMatch) return staticMatch[1];
43
+ const ternaryMatch = line.match(/className=\{[^?]+\?\s*"([^"]*)"\s*:\s*"([^"]*)"\}/);
44
+ if (ternaryMatch) return `${ternaryMatch[1]} ${ternaryMatch[2]}`;
45
+ const templateMatch = line.match(/className=\{`([^`]*)`\}/);
46
+ if (templateMatch) {
47
+ const raw = templateMatch[1];
48
+ const literals = raw.replace(/\$\{[^}]*\}/g, " ").trim();
49
+ const exprStrings = [...raw.matchAll(/"([^"]*)"/g)].map((m) => m[1]);
50
+ return [literals, ...exprStrings].filter(Boolean).join(" ");
51
+ }
52
+ return null;
57
53
  }
58
- function extractChainMethod(line) {
59
- const match = line.match(/\.(\w+)\(/);
60
- if (match) return `${match[1]}()`;
61
- return line.trim();
54
+ function extractComponentFromLine(line) {
55
+ const tagMatch = line.match(/<([A-Za-z][A-Za-z0-9.]*)/);
56
+ if (tagMatch) return tagMatch[1];
57
+ return "unknown";
62
58
  }
63
- function simplifyCallback(methodCall) {
64
- const arrowMatch = methodCall.match(/(\w+)\((\w+)\s*=>\s*(\w+)\.(\w+)\)/);
65
- if (arrowMatch) {
66
- const [, method, param, , prop] = arrowMatch;
67
- return `${method}(${param} \u2192 ${prop})`;
59
+ function parseClassNameChanges(addedLines, removedLines) {
60
+ const componentMap = /* @__PURE__ */ new Map();
61
+ for (const line of addedLines.filter((l) => /className=/.test(l))) {
62
+ const cls = extractClassName(line);
63
+ const comp = extractComponentFromLine(line);
64
+ if (!cls) continue;
65
+ if (!componentMap.has(comp)) componentMap.set(comp, { added: /* @__PURE__ */ new Set(), removed: /* @__PURE__ */ new Set() });
66
+ cls.split(/\s+/).filter(Boolean).forEach((c) => componentMap.get(comp).added.add(c));
68
67
  }
69
- const callbackMatch = methodCall.match(/(\w+)\([^)]+\)/);
70
- if (callbackMatch) return `${callbackMatch[1]}(callback)`;
71
- return methodCall;
72
- }
73
- function isConditional(line) {
74
- return /^(if|else|switch)\s*[\(\{]/.test(line.trim());
68
+ for (const line of removedLines.filter((l) => /className=/.test(l))) {
69
+ const cls = extractClassName(line);
70
+ const comp = extractComponentFromLine(line);
71
+ if (!cls) continue;
72
+ if (!componentMap.has(comp)) componentMap.set(comp, { added: /* @__PURE__ */ new Set(), removed: /* @__PURE__ */ new Set() });
73
+ cls.split(/\s+/).filter(Boolean).forEach((c) => componentMap.get(comp).removed.add(c));
74
+ }
75
+ const changes = [];
76
+ for (const [comp, { added, removed }] of componentMap) {
77
+ if (comp === "unknown") continue;
78
+ const pureAdded = [...added].filter((c) => !removed.has(c));
79
+ const pureRemoved = [...removed].filter((c) => !added.has(c));
80
+ if (pureAdded.length === 0 && pureRemoved.length === 0) continue;
81
+ changes.push({ component: comp, added: pureAdded, removed: pureRemoved });
82
+ }
83
+ return changes;
75
84
  }
76
- function isLoop(line) {
77
- return /^(for|while)\s*\(/.test(line.trim());
85
+ function renderStyleChanges(changes) {
86
+ const lines = [];
87
+ for (const change of changes) {
88
+ lines.push(`**${change.component}**`);
89
+ if (change.added.length > 0) lines.push(` + ${change.added.join(" ")}`);
90
+ if (change.removed.length > 0) lines.push(` - ${change.removed.join(" ")}`);
91
+ }
92
+ return lines;
78
93
  }
79
- function isFunctionDeclaration(line) {
94
+ function isJSXElement(line) {
80
95
  const t = line.trim();
81
- return (
82
- // function foo() / async function foo()
83
- /^(async\s+)?function\s+\w+/.test(t) || // const foo = () => / const foo = async () => / const foo = async (x: T) =>
84
- /^(const|let|var)\s+\w+\s*=\s*(async\s*)?\(/.test(t) || // const foo = function / const foo = async function
85
- /^(const|let|var)\s+\w+\s*=\s*(async\s+)?function/.test(t)
86
- );
96
+ return /^<[A-Za-z]/.test(t) || /^<\/[A-Za-z]/.test(t);
87
97
  }
88
- function shouldIgnore(line) {
89
- const ignorePatterns = [
90
- /^import\s+/,
91
- /^export\s+/,
92
- /^type\s+/,
93
- /^interface\s+/,
94
- /^console\./,
95
- /^return$/,
96
- /^throw\s+/
97
- ];
98
- return ignorePatterns.some((p) => p.test(line.trim()));
98
+ function isJSXClosing(line) {
99
+ return /^<\/[A-Za-z]/.test(line.trim());
99
100
  }
100
- function extractRoot(line) {
101
- const assignMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=\s*(\w+)/);
102
- if (assignMatch) return assignMatch[2];
103
- const callMatch = line.match(/^(\w+)\(/);
104
- if (callMatch) return `${callMatch[1]}()`;
105
- const methodMatch = line.match(/^(\w+)\./);
106
- if (methodMatch) return methodMatch[1];
107
- return null;
101
+ function isJSXSelfClosing(line) {
102
+ return /\/>[\s]*$/.test(line.trim());
103
+ }
104
+ function extractJSXComponentName(line) {
105
+ const trimmed = line.trim();
106
+ const closingMatch = trimmed.match(/^<\/([A-Za-z][A-Za-z0-9.]*)/);
107
+ if (closingMatch) return `/${closingMatch[1]}`;
108
+ const nameMatch = trimmed.match(/^<([A-Za-z][A-Za-z0-9.]*)/);
109
+ if (!nameMatch) return trimmed;
110
+ const name = nameMatch[1];
111
+ const eventProps = [];
112
+ for (const m of trimmed.matchAll(/\b(on[A-Z]\w+)=/g)) {
113
+ eventProps.push(m[1]);
114
+ }
115
+ return eventProps.length > 0 ? `${name}(${eventProps.join(", ")})` : name;
108
116
  }
109
- function parseToFlowTree(lines) {
117
+ function shouldIgnoreJSX(line) {
118
+ const t = line.trim();
119
+ return isClassNameOnlyLine(t) || /^style=/.test(t) || /^aria-/.test(t) || /^data-/.test(t) || /^strokeLinecap=/.test(t) || /^strokeLinejoin=/.test(t) || /^strokeWidth=/.test(t) || /^viewBox=/.test(t) || /^fill=/.test(t) || /^stroke=/.test(t) || /^d="/.test(t) || t === "{" || t === "}" || t === "(" || t === ")" || t === "<>" || t === "</>" || /^\{\/\*/.test(t);
120
+ }
121
+ function parseJSXToFlowTree(lines) {
110
122
  const roots = [];
111
- let currentChain = null;
112
- let prevLine = null;
113
- let baseDepth = -1;
114
- for (let i = 0; i < lines.length; i++) {
115
- const line = lines[i];
116
- if (shouldIgnore(line)) {
117
- prevLine = line;
118
- continue;
119
- }
120
- const depth = getIndentDepth(lines[i]);
121
- if (baseDepth === -1) baseDepth = depth;
122
- const relativeDepth = depth - baseDepth;
123
- if (isChaining(line, prevLine)) {
124
- const method = extractChainMethod(line);
125
- const simplified = simplifyCallback(method);
126
- if (currentChain) {
127
- const chainNode = {
128
- type: "chain",
129
- name: simplified,
130
- children: [],
131
- depth: relativeDepth,
132
- priority: 1 /* CHAINING */
133
- };
134
- let parent = currentChain;
135
- while (parent.children.length > 0 && parent.children[parent.children.length - 1].depth >= relativeDepth) {
136
- const last = parent.children[parent.children.length - 1];
137
- if (last.children.length > 0) parent = last;
138
- else break;
139
- }
140
- parent.children.push(chainNode);
123
+ const stack = [];
124
+ for (const line of lines) {
125
+ if (!isJSXElement(line)) continue;
126
+ if (shouldIgnoreJSX(line)) continue;
127
+ const depth = getIndentDepth(line);
128
+ if (isJSXClosing(line)) {
129
+ while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
130
+ stack.pop();
141
131
  }
142
- prevLine = line;
143
132
  continue;
144
133
  }
145
- if (isFunctionDeclaration(line)) {
146
- const funcMatch = line.match(/(?:function|const|let|var)\s+(\w+)/);
147
- roots.push({
148
- type: "function",
149
- name: funcMatch ? `${funcMatch[1]}()` : "function()",
150
- children: [],
151
- depth: relativeDepth,
152
- priority: 4 /* FUNCTION */
153
- });
154
- currentChain = null;
155
- prevLine = line;
156
- continue;
134
+ const name = extractJSXComponentName(line);
135
+ const selfClosing = isJSXSelfClosing(line);
136
+ const node = {
137
+ type: "call",
138
+ name,
139
+ children: [],
140
+ depth,
141
+ priority: 5 /* OTHER */
142
+ };
143
+ while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
144
+ stack.pop();
145
+ }
146
+ if (stack.length === 0) {
147
+ roots.push(node);
148
+ } else {
149
+ stack[stack.length - 1].node.children.push(node);
157
150
  }
158
- const root = extractRoot(line);
159
- if (root) {
160
- currentChain = {
161
- type: "root",
162
- name: root,
163
- children: [],
164
- depth: relativeDepth,
165
- priority: 1 /* CHAINING */
166
- };
167
- roots.push(currentChain);
168
- } else if (isConditional(line)) {
169
- const condMatch = line.match(/(if|else|switch)\s*\(([^)]+)\)/);
170
- const condName = condMatch ? `${condMatch[1]} (${condMatch[2]})` : line.trim();
171
- roots.push({
172
- type: "condition",
173
- name: condName,
174
- children: [],
175
- depth: relativeDepth,
176
- priority: 2 /* CONDITIONAL */
177
- });
178
- currentChain = null;
179
- } else if (isLoop(line)) {
180
- roots.push({
181
- type: "loop",
182
- name: "loop",
183
- children: [],
184
- depth: relativeDepth,
185
- priority: 3 /* LOOP */
186
- });
187
- currentChain = null;
151
+ if (!selfClosing) {
152
+ stack.push({ node, depth });
188
153
  }
189
- prevLine = line;
190
154
  }
191
155
  return roots;
192
156
  }
193
- function renderFlowTree(nodes, indent = 0) {
194
- const lines = [];
195
- const prefix = indent === 0 ? "" : " ".repeat((indent - 1) * 4) + " \u2514\u2500 ";
196
- for (const node of nodes) {
197
- lines.push(prefix + node.name);
198
- if (node.children.length > 0) {
199
- lines.push(...renderFlowTree(node.children, indent + 1));
157
+ function filterDiffLines(diffText) {
158
+ const lines = diffText.split("\n");
159
+ const added = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++") && l.trim() !== "+").map((l) => l.substring(1));
160
+ const removed = lines.filter((l) => l.startsWith("-") && !l.startsWith("---") && l.trim() !== "-").map((l) => l.substring(1));
161
+ return { added, removed };
162
+ }
163
+ function getIndentDepth(line) {
164
+ const match = line.match(/^(\s*)/);
165
+ if (!match) return 0;
166
+ return Math.floor(match[1].length / 2);
167
+ }
168
+ function extractChangedSymbols(addedLines, removedLines) {
169
+ const FUNC_RE = /^(?:export\s+)?(?:async\s+)?function\s+([a-z]\w+)|^(?:export\s+)?(?:const|let|var)\s+([a-z]\w+)\s*=\s*(?:async\s*)?\(/;
170
+ const COMPONENT_RE = /^(?:export\s+)?(?:default\s+)?(?:function|const)\s+([A-Z][a-z][A-Za-z0-9]*)/;
171
+ const extract = (lines) => {
172
+ const names = /* @__PURE__ */ new Set();
173
+ for (const line of lines) {
174
+ const cm = line.match(COMPONENT_RE) || line.match(FUNC_RE);
175
+ if (cm) {
176
+ const name = cm[1] || cm[2];
177
+ if (name) names.add(name);
178
+ }
200
179
  }
180
+ return names;
181
+ };
182
+ const addedNames = extract(addedLines);
183
+ const removedNames = extract(removedLines);
184
+ const results = [];
185
+ const seen = /* @__PURE__ */ new Set();
186
+ for (const name of addedNames) {
187
+ seen.add(name);
188
+ results.push({ name, status: removedNames.has(name) ? "modified" : "added" });
201
189
  }
202
- return lines;
190
+ for (const name of removedNames) {
191
+ if (!seen.has(name)) {
192
+ results.push({ name, status: "removed" });
193
+ }
194
+ }
195
+ return results;
203
196
  }
204
- function parseDiffToLogicalFlow(diffText) {
205
- const { added, removed } = filterDiffLines(diffText);
206
- const normalizedAdded = normalizeCode(added);
207
- const flowTree = parseToFlowTree(normalizedAdded);
208
- return {
209
- root: flowTree,
210
- rawCode: added.join("\n"),
211
- removedCode: removed.join("\n")
212
- };
197
+ function renderJSXTreeCompact(nodes, maxDepth = 3) {
198
+ const lines = [];
199
+ function walk(node, depth) {
200
+ if (depth > maxDepth) return;
201
+ const indent = " ".repeat(depth);
202
+ const hasChildren = node.children.length > 0;
203
+ lines.push(`${indent}${node.name}${hasChildren ? "" : ""}`);
204
+ for (const child of node.children) {
205
+ walk(child, depth + 1);
206
+ }
207
+ }
208
+ for (const root of nodes) {
209
+ walk(root, 0);
210
+ }
211
+ return lines.join("\n");
213
212
  }
214
213
  function generateReaderMarkdown(diffText, meta = {}) {
215
- const result = parseDiffToLogicalFlow(diffText);
214
+ const { added, removed } = filterDiffLines(diffText);
215
+ const isJSX = Boolean(
216
+ meta.file && isJSXFile(meta.file) || hasJSXContent(added)
217
+ );
218
+ const changedSymbols = extractChangedSymbols(added, removed);
219
+ const classNameChanges = isJSX ? parseClassNameChanges(added, removed) : [];
220
+ const jsxTree = isJSX ? parseJSXToFlowTree(added) : [];
216
221
  const sections = [];
217
222
  sections.push("# \u{1F4D6} GitHub Reader View\n");
218
223
  sections.push("> Generated by **github-mobile-reader**");
@@ -221,26 +226,27 @@ function generateReaderMarkdown(diffText, meta = {}) {
221
226
  if (meta.commit) sections.push(`> Commit: \`${meta.commit}\``);
222
227
  if (meta.file) sections.push(`> File: \`${meta.file}\``);
223
228
  sections.push("\n");
224
- if (result.root.length > 0) {
225
- sections.push("## \u{1F9E0} Logical Flow\n");
226
- sections.push("```");
227
- sections.push(...renderFlowTree(result.root));
228
- sections.push("```\n");
229
+ if (changedSymbols.length > 0) {
230
+ sections.push("### \uBCC0\uACBD\uB41C \uD568\uC218 / \uCEF4\uD3EC\uB10C\uD2B8\n");
231
+ const STATUS_ICON = { added: "\u2705", removed: "\u274C", modified: "\u270F\uFE0F" };
232
+ for (const { name, status } of changedSymbols) {
233
+ sections.push(`- ${STATUS_ICON[status]} \`${name}()\` \u2014 ${status}`);
234
+ }
235
+ sections.push("");
229
236
  }
230
- if (result.rawCode.trim()) {
231
- sections.push("## \u2705 Added Code\n");
232
- sections.push("```typescript");
233
- sections.push(result.rawCode);
237
+ if (isJSX && jsxTree.length > 0) {
238
+ sections.push("### \u{1F3A8} JSX Structure\n");
239
+ sections.push("```");
240
+ sections.push(renderJSXTreeCompact(jsxTree));
234
241
  sections.push("```\n");
235
242
  }
236
- if (result.removedCode.trim()) {
237
- sections.push("## \u274C Removed Code\n");
238
- sections.push("```typescript");
239
- sections.push(result.removedCode);
240
- sections.push("```\n");
243
+ if (isJSX && classNameChanges.length > 0) {
244
+ sections.push("### \u{1F485} Style Changes\n");
245
+ sections.push(...renderStyleChanges(classNameChanges));
246
+ sections.push("");
241
247
  }
242
248
  sections.push("---");
243
- sections.push("\u{1F6E0} Auto-generated by [github-mobile-reader](https://github.com/your-org/github-mobile-reader). Do not edit manually.");
249
+ sections.push("\u{1F6E0} Auto-generated by [github-mobile-reader](https://github.com/3rdflr/github-mobile-reader). Do not edit manually.");
244
250
  return sections.join("\n");
245
251
  }
246
252