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 +76 -8
- package/README.md +77 -9
- package/dist/action.js +180 -174
- package/dist/cli.js +421 -0
- package/dist/index.d.mts +28 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +218 -18
- package/dist/index.mjs +206 -17
- package/package.json +7 -3
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. [
|
|
54
|
-
4. [
|
|
55
|
-
5. [
|
|
56
|
-
6. [
|
|
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. [
|
|
52
|
-
4. [
|
|
53
|
-
5. [
|
|
54
|
-
6. [
|
|
55
|
-
7. [
|
|
56
|
-
8. [
|
|
57
|
-
9. [
|
|
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
|
-
│
|
|
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
|
|
32
|
-
|
|
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
|
|
38
|
-
return lines.
|
|
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
|
|
48
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
return
|
|
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
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
return
|
|
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
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
77
|
-
|
|
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
|
|
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
|
|
89
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
|
|
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
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
|
194
|
-
const lines =
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
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 (
|
|
225
|
-
sections.push("
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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 (
|
|
231
|
-
sections.push("
|
|
232
|
-
sections.push("```
|
|
233
|
-
sections.push(
|
|
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 (
|
|
237
|
-
sections.push("
|
|
238
|
-
sections.push(
|
|
239
|
-
sections.push(
|
|
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/
|
|
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
|
|