github-mobile-reader 0.1.0 → 0.1.2
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 +585 -0
- package/README.md +157 -84
- package/dist/action.js +177 -30
- package/dist/cli.js +552 -0
- package/dist/index.d.mts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +198 -23
- package/dist/index.mjs +188 -22
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -21,12 +21,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
Priority: () => Priority,
|
|
24
|
+
extractClassName: () => extractClassName,
|
|
25
|
+
extractJSXComponentName: () => extractJSXComponentName,
|
|
24
26
|
filterDiffLines: () => filterDiffLines,
|
|
25
27
|
generateReaderMarkdown: () => generateReaderMarkdown,
|
|
28
|
+
hasJSXContent: () => hasJSXContent,
|
|
29
|
+
isClassNameOnlyLine: () => isClassNameOnlyLine,
|
|
30
|
+
isJSXElement: () => isJSXElement,
|
|
31
|
+
isJSXFile: () => isJSXFile,
|
|
26
32
|
normalizeCode: () => normalizeCode,
|
|
33
|
+
parseClassNameChanges: () => parseClassNameChanges,
|
|
27
34
|
parseDiffToLogicalFlow: () => parseDiffToLogicalFlow,
|
|
35
|
+
parseJSXToFlowTree: () => parseJSXToFlowTree,
|
|
28
36
|
parseToFlowTree: () => parseToFlowTree,
|
|
29
|
-
renderFlowTree: () => renderFlowTree
|
|
37
|
+
renderFlowTree: () => renderFlowTree,
|
|
38
|
+
renderStyleChanges: () => renderStyleChanges
|
|
30
39
|
});
|
|
31
40
|
module.exports = __toCommonJS(index_exports);
|
|
32
41
|
|
|
@@ -39,6 +48,131 @@ var Priority = /* @__PURE__ */ ((Priority2) => {
|
|
|
39
48
|
Priority2[Priority2["OTHER"] = 5] = "OTHER";
|
|
40
49
|
return Priority2;
|
|
41
50
|
})(Priority || {});
|
|
51
|
+
function isJSXFile(filename) {
|
|
52
|
+
return /\.(jsx|tsx)$/.test(filename);
|
|
53
|
+
}
|
|
54
|
+
function hasJSXContent(lines) {
|
|
55
|
+
return lines.some((l) => /<[A-Z][A-Za-z]*[\s/>]/.test(l) || /return\s*\(/.test(l));
|
|
56
|
+
}
|
|
57
|
+
function isClassNameOnlyLine(line) {
|
|
58
|
+
return /^className=/.test(line.trim());
|
|
59
|
+
}
|
|
60
|
+
function extractClassName(line) {
|
|
61
|
+
const staticMatch = line.match(/className="([^"]*)"/);
|
|
62
|
+
if (staticMatch) return staticMatch[1];
|
|
63
|
+
const ternaryMatch = line.match(/className=\{[^?]+\?\s*"([^"]*)"\s*:\s*"([^"]*)"\}/);
|
|
64
|
+
if (ternaryMatch) return `${ternaryMatch[1]} ${ternaryMatch[2]}`;
|
|
65
|
+
const templateMatch = line.match(/className=\{`([^`]*)`\}/);
|
|
66
|
+
if (templateMatch) {
|
|
67
|
+
const raw = templateMatch[1];
|
|
68
|
+
const literals = raw.replace(/\$\{[^}]*\}/g, " ").trim();
|
|
69
|
+
const exprStrings = [...raw.matchAll(/"([^"]*)"/g)].map((m) => m[1]);
|
|
70
|
+
return [literals, ...exprStrings].filter(Boolean).join(" ");
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function extractComponentFromLine(line) {
|
|
75
|
+
const tagMatch = line.match(/<([A-Za-z][A-Za-z0-9.]*)/);
|
|
76
|
+
if (tagMatch) return tagMatch[1];
|
|
77
|
+
return "unknown";
|
|
78
|
+
}
|
|
79
|
+
function parseClassNameChanges(addedLines, removedLines) {
|
|
80
|
+
const componentMap = /* @__PURE__ */ new Map();
|
|
81
|
+
for (const line of addedLines.filter((l) => /className=/.test(l))) {
|
|
82
|
+
const cls = extractClassName(line);
|
|
83
|
+
const comp = extractComponentFromLine(line);
|
|
84
|
+
if (!cls) continue;
|
|
85
|
+
if (!componentMap.has(comp)) componentMap.set(comp, { added: /* @__PURE__ */ new Set(), removed: /* @__PURE__ */ new Set() });
|
|
86
|
+
cls.split(/\s+/).filter(Boolean).forEach((c) => componentMap.get(comp).added.add(c));
|
|
87
|
+
}
|
|
88
|
+
for (const line of removedLines.filter((l) => /className=/.test(l))) {
|
|
89
|
+
const cls = extractClassName(line);
|
|
90
|
+
const comp = extractComponentFromLine(line);
|
|
91
|
+
if (!cls) continue;
|
|
92
|
+
if (!componentMap.has(comp)) componentMap.set(comp, { added: /* @__PURE__ */ new Set(), removed: /* @__PURE__ */ new Set() });
|
|
93
|
+
cls.split(/\s+/).filter(Boolean).forEach((c) => componentMap.get(comp).removed.add(c));
|
|
94
|
+
}
|
|
95
|
+
const changes = [];
|
|
96
|
+
for (const [comp, { added, removed }] of componentMap) {
|
|
97
|
+
const pureAdded = [...added].filter((c) => !removed.has(c));
|
|
98
|
+
const pureRemoved = [...removed].filter((c) => !added.has(c));
|
|
99
|
+
if (pureAdded.length === 0 && pureRemoved.length === 0) continue;
|
|
100
|
+
changes.push({ component: comp, added: pureAdded, removed: pureRemoved });
|
|
101
|
+
}
|
|
102
|
+
return changes;
|
|
103
|
+
}
|
|
104
|
+
function renderStyleChanges(changes) {
|
|
105
|
+
const lines = [];
|
|
106
|
+
for (const change of changes) {
|
|
107
|
+
lines.push(`**${change.component}**`);
|
|
108
|
+
if (change.added.length > 0) lines.push(` + ${change.added.join(" ")}`);
|
|
109
|
+
if (change.removed.length > 0) lines.push(` - ${change.removed.join(" ")}`);
|
|
110
|
+
}
|
|
111
|
+
return lines;
|
|
112
|
+
}
|
|
113
|
+
function isJSXElement(line) {
|
|
114
|
+
const t = line.trim();
|
|
115
|
+
return /^<[A-Za-z]/.test(t) || /^<\/[A-Za-z]/.test(t);
|
|
116
|
+
}
|
|
117
|
+
function isJSXClosing(line) {
|
|
118
|
+
return /^<\/[A-Za-z]/.test(line.trim());
|
|
119
|
+
}
|
|
120
|
+
function isJSXSelfClosing(line) {
|
|
121
|
+
return /\/>[\s]*$/.test(line.trim());
|
|
122
|
+
}
|
|
123
|
+
function extractJSXComponentName(line) {
|
|
124
|
+
const trimmed = line.trim();
|
|
125
|
+
const closingMatch = trimmed.match(/^<\/([A-Za-z][A-Za-z0-9.]*)/);
|
|
126
|
+
if (closingMatch) return `/${closingMatch[1]}`;
|
|
127
|
+
const nameMatch = trimmed.match(/^<([A-Za-z][A-Za-z0-9.]*)/);
|
|
128
|
+
if (!nameMatch) return trimmed;
|
|
129
|
+
const name = nameMatch[1];
|
|
130
|
+
const eventProps = [];
|
|
131
|
+
for (const m of trimmed.matchAll(/\b(on[A-Z]\w+)=/g)) {
|
|
132
|
+
eventProps.push(m[1]);
|
|
133
|
+
}
|
|
134
|
+
return eventProps.length > 0 ? `${name}(${eventProps.join(", ")})` : name;
|
|
135
|
+
}
|
|
136
|
+
function shouldIgnoreJSX(line) {
|
|
137
|
+
const t = line.trim();
|
|
138
|
+
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);
|
|
139
|
+
}
|
|
140
|
+
function parseJSXToFlowTree(lines) {
|
|
141
|
+
const roots = [];
|
|
142
|
+
const stack = [];
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
if (!isJSXElement(line)) continue;
|
|
145
|
+
if (shouldIgnoreJSX(line)) continue;
|
|
146
|
+
const depth = getIndentDepth(line);
|
|
147
|
+
if (isJSXClosing(line)) {
|
|
148
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
|
|
149
|
+
stack.pop();
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const name = extractJSXComponentName(line);
|
|
154
|
+
const selfClosing = isJSXSelfClosing(line);
|
|
155
|
+
const node = {
|
|
156
|
+
type: "call",
|
|
157
|
+
name,
|
|
158
|
+
children: [],
|
|
159
|
+
depth,
|
|
160
|
+
priority: 5 /* OTHER */
|
|
161
|
+
};
|
|
162
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
|
|
163
|
+
stack.pop();
|
|
164
|
+
}
|
|
165
|
+
if (stack.length === 0) {
|
|
166
|
+
roots.push(node);
|
|
167
|
+
} else {
|
|
168
|
+
stack[stack.length - 1].node.children.push(node);
|
|
169
|
+
}
|
|
170
|
+
if (!selfClosing) {
|
|
171
|
+
stack.push({ node, depth });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return roots;
|
|
175
|
+
}
|
|
42
176
|
function filterDiffLines(diffText) {
|
|
43
177
|
const lines = diffText.split("\n");
|
|
44
178
|
const added = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++") && l.trim() !== "+").map((l) => l.substring(1));
|
|
@@ -48,10 +182,10 @@ function filterDiffLines(diffText) {
|
|
|
48
182
|
function normalizeCode(lines) {
|
|
49
183
|
return lines.map((line) => {
|
|
50
184
|
let normalized = line;
|
|
51
|
-
normalized = normalized.replace(/;$/, "");
|
|
52
185
|
normalized = normalized.replace(/\/\/.*$/, "");
|
|
53
186
|
normalized = normalized.replace(/\/\*.*?\*\//, "");
|
|
54
187
|
normalized = normalized.trim();
|
|
188
|
+
normalized = normalized.replace(/;$/, "");
|
|
55
189
|
return normalized;
|
|
56
190
|
}).filter((line) => line.length > 0);
|
|
57
191
|
}
|
|
@@ -88,7 +222,13 @@ function isLoop(line) {
|
|
|
88
222
|
return /^(for|while)\s*\(/.test(line.trim());
|
|
89
223
|
}
|
|
90
224
|
function isFunctionDeclaration(line) {
|
|
91
|
-
|
|
225
|
+
const t = line.trim();
|
|
226
|
+
return (
|
|
227
|
+
// function foo() / async function foo()
|
|
228
|
+
/^(async\s+)?function\s+\w+/.test(t) || // const foo = () => / const foo = async () => / const foo = async (x: T) =>
|
|
229
|
+
/^(const|let|var)\s+\w+\s*=\s*(async\s*)?\(/.test(t) || // const foo = function / const foo = async function
|
|
230
|
+
/^(const|let|var)\s+\w+\s*=\s*(async\s+)?function/.test(t)
|
|
231
|
+
);
|
|
92
232
|
}
|
|
93
233
|
function shouldIgnore(line) {
|
|
94
234
|
const ignorePatterns = [
|
|
@@ -147,6 +287,19 @@ function parseToFlowTree(lines) {
|
|
|
147
287
|
prevLine = line;
|
|
148
288
|
continue;
|
|
149
289
|
}
|
|
290
|
+
if (isFunctionDeclaration(line)) {
|
|
291
|
+
const funcMatch = line.match(/(?:function|const|let|var)\s+(\w+)/);
|
|
292
|
+
roots.push({
|
|
293
|
+
type: "function",
|
|
294
|
+
name: funcMatch ? `${funcMatch[1]}()` : "function()",
|
|
295
|
+
children: [],
|
|
296
|
+
depth: relativeDepth,
|
|
297
|
+
priority: 4 /* FUNCTION */
|
|
298
|
+
});
|
|
299
|
+
currentChain = null;
|
|
300
|
+
prevLine = line;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
150
303
|
const root = extractRoot(line);
|
|
151
304
|
if (root) {
|
|
152
305
|
currentChain = {
|
|
@@ -177,16 +330,6 @@ function parseToFlowTree(lines) {
|
|
|
177
330
|
priority: 3 /* LOOP */
|
|
178
331
|
});
|
|
179
332
|
currentChain = null;
|
|
180
|
-
} else if (isFunctionDeclaration(line)) {
|
|
181
|
-
const funcMatch = line.match(/(?:function|const|let|var)\s+(\w+)/);
|
|
182
|
-
roots.push({
|
|
183
|
-
type: "function",
|
|
184
|
-
name: funcMatch ? `${funcMatch[1]}()` : "function()",
|
|
185
|
-
children: [],
|
|
186
|
-
depth: relativeDepth,
|
|
187
|
-
priority: 4 /* FUNCTION */
|
|
188
|
-
});
|
|
189
|
-
currentChain = null;
|
|
190
333
|
}
|
|
191
334
|
prevLine = line;
|
|
192
335
|
}
|
|
@@ -214,8 +357,20 @@ function parseDiffToLogicalFlow(diffText) {
|
|
|
214
357
|
};
|
|
215
358
|
}
|
|
216
359
|
function generateReaderMarkdown(diffText, meta = {}) {
|
|
217
|
-
const
|
|
360
|
+
const { added, removed } = filterDiffLines(diffText);
|
|
361
|
+
const isJSX = Boolean(
|
|
362
|
+
meta.file && isJSXFile(meta.file) || hasJSXContent(added)
|
|
363
|
+
);
|
|
364
|
+
const addedForFlow = isJSX ? added.filter((l) => !isClassNameOnlyLine(l)) : added;
|
|
365
|
+
const normalizedAdded = normalizeCode(addedForFlow);
|
|
366
|
+
const flowTree = parseToFlowTree(normalizedAdded);
|
|
367
|
+
const rawCode = addedForFlow.join("\n");
|
|
368
|
+
const removedForCode = isJSX ? removed.filter((l) => !isClassNameOnlyLine(l)) : removed;
|
|
369
|
+
const removedCode = removedForCode.join("\n");
|
|
370
|
+
const classNameChanges = isJSX ? parseClassNameChanges(added, removed) : [];
|
|
371
|
+
const jsxTree = isJSX ? parseJSXToFlowTree(added) : [];
|
|
218
372
|
const sections = [];
|
|
373
|
+
const lang = isJSX ? "tsx" : "typescript";
|
|
219
374
|
sections.push("# \u{1F4D6} GitHub Reader View\n");
|
|
220
375
|
sections.push("> Generated by **github-mobile-reader**");
|
|
221
376
|
if (meta.repo) sections.push(`> Repository: ${meta.repo}`);
|
|
@@ -223,22 +378,33 @@ function generateReaderMarkdown(diffText, meta = {}) {
|
|
|
223
378
|
if (meta.commit) sections.push(`> Commit: \`${meta.commit}\``);
|
|
224
379
|
if (meta.file) sections.push(`> File: \`${meta.file}\``);
|
|
225
380
|
sections.push("\n");
|
|
226
|
-
if (
|
|
381
|
+
if (flowTree.length > 0) {
|
|
227
382
|
sections.push("## \u{1F9E0} Logical Flow\n");
|
|
228
383
|
sections.push("```");
|
|
229
|
-
sections.push(...renderFlowTree(
|
|
384
|
+
sections.push(...renderFlowTree(flowTree));
|
|
230
385
|
sections.push("```\n");
|
|
231
386
|
}
|
|
232
|
-
if (
|
|
387
|
+
if (isJSX && jsxTree.length > 0) {
|
|
388
|
+
sections.push("## \u{1F3A8} JSX Structure\n");
|
|
389
|
+
sections.push("```");
|
|
390
|
+
sections.push(...renderFlowTree(jsxTree));
|
|
391
|
+
sections.push("```\n");
|
|
392
|
+
}
|
|
393
|
+
if (isJSX && classNameChanges.length > 0) {
|
|
394
|
+
sections.push("## \u{1F485} Style Changes\n");
|
|
395
|
+
sections.push(...renderStyleChanges(classNameChanges));
|
|
396
|
+
sections.push("");
|
|
397
|
+
}
|
|
398
|
+
if (rawCode.trim()) {
|
|
233
399
|
sections.push("## \u2705 Added Code\n");
|
|
234
|
-
sections.push(
|
|
235
|
-
sections.push(
|
|
400
|
+
sections.push(`\`\`\`${lang}`);
|
|
401
|
+
sections.push(rawCode);
|
|
236
402
|
sections.push("```\n");
|
|
237
403
|
}
|
|
238
|
-
if (
|
|
404
|
+
if (removedCode.trim()) {
|
|
239
405
|
sections.push("## \u274C Removed Code\n");
|
|
240
|
-
sections.push(
|
|
241
|
-
sections.push(
|
|
406
|
+
sections.push(`\`\`\`${lang}`);
|
|
407
|
+
sections.push(removedCode);
|
|
242
408
|
sections.push("```\n");
|
|
243
409
|
}
|
|
244
410
|
sections.push("---");
|
|
@@ -248,10 +414,19 @@ function generateReaderMarkdown(diffText, meta = {}) {
|
|
|
248
414
|
// Annotate the CommonJS export names for ESM import in node:
|
|
249
415
|
0 && (module.exports = {
|
|
250
416
|
Priority,
|
|
417
|
+
extractClassName,
|
|
418
|
+
extractJSXComponentName,
|
|
251
419
|
filterDiffLines,
|
|
252
420
|
generateReaderMarkdown,
|
|
421
|
+
hasJSXContent,
|
|
422
|
+
isClassNameOnlyLine,
|
|
423
|
+
isJSXElement,
|
|
424
|
+
isJSXFile,
|
|
253
425
|
normalizeCode,
|
|
426
|
+
parseClassNameChanges,
|
|
254
427
|
parseDiffToLogicalFlow,
|
|
428
|
+
parseJSXToFlowTree,
|
|
255
429
|
parseToFlowTree,
|
|
256
|
-
renderFlowTree
|
|
430
|
+
renderFlowTree,
|
|
431
|
+
renderStyleChanges
|
|
257
432
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -7,6 +7,131 @@ var Priority = /* @__PURE__ */ ((Priority2) => {
|
|
|
7
7
|
Priority2[Priority2["OTHER"] = 5] = "OTHER";
|
|
8
8
|
return Priority2;
|
|
9
9
|
})(Priority || {});
|
|
10
|
+
function isJSXFile(filename) {
|
|
11
|
+
return /\.(jsx|tsx)$/.test(filename);
|
|
12
|
+
}
|
|
13
|
+
function hasJSXContent(lines) {
|
|
14
|
+
return lines.some((l) => /<[A-Z][A-Za-z]*[\s/>]/.test(l) || /return\s*\(/.test(l));
|
|
15
|
+
}
|
|
16
|
+
function isClassNameOnlyLine(line) {
|
|
17
|
+
return /^className=/.test(line.trim());
|
|
18
|
+
}
|
|
19
|
+
function extractClassName(line) {
|
|
20
|
+
const staticMatch = line.match(/className="([^"]*)"/);
|
|
21
|
+
if (staticMatch) return staticMatch[1];
|
|
22
|
+
const ternaryMatch = line.match(/className=\{[^?]+\?\s*"([^"]*)"\s*:\s*"([^"]*)"\}/);
|
|
23
|
+
if (ternaryMatch) return `${ternaryMatch[1]} ${ternaryMatch[2]}`;
|
|
24
|
+
const templateMatch = line.match(/className=\{`([^`]*)`\}/);
|
|
25
|
+
if (templateMatch) {
|
|
26
|
+
const raw = templateMatch[1];
|
|
27
|
+
const literals = raw.replace(/\$\{[^}]*\}/g, " ").trim();
|
|
28
|
+
const exprStrings = [...raw.matchAll(/"([^"]*)"/g)].map((m) => m[1]);
|
|
29
|
+
return [literals, ...exprStrings].filter(Boolean).join(" ");
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function extractComponentFromLine(line) {
|
|
34
|
+
const tagMatch = line.match(/<([A-Za-z][A-Za-z0-9.]*)/);
|
|
35
|
+
if (tagMatch) return tagMatch[1];
|
|
36
|
+
return "unknown";
|
|
37
|
+
}
|
|
38
|
+
function parseClassNameChanges(addedLines, removedLines) {
|
|
39
|
+
const componentMap = /* @__PURE__ */ new Map();
|
|
40
|
+
for (const line of addedLines.filter((l) => /className=/.test(l))) {
|
|
41
|
+
const cls = extractClassName(line);
|
|
42
|
+
const comp = extractComponentFromLine(line);
|
|
43
|
+
if (!cls) continue;
|
|
44
|
+
if (!componentMap.has(comp)) componentMap.set(comp, { added: /* @__PURE__ */ new Set(), removed: /* @__PURE__ */ new Set() });
|
|
45
|
+
cls.split(/\s+/).filter(Boolean).forEach((c) => componentMap.get(comp).added.add(c));
|
|
46
|
+
}
|
|
47
|
+
for (const line of removedLines.filter((l) => /className=/.test(l))) {
|
|
48
|
+
const cls = extractClassName(line);
|
|
49
|
+
const comp = extractComponentFromLine(line);
|
|
50
|
+
if (!cls) continue;
|
|
51
|
+
if (!componentMap.has(comp)) componentMap.set(comp, { added: /* @__PURE__ */ new Set(), removed: /* @__PURE__ */ new Set() });
|
|
52
|
+
cls.split(/\s+/).filter(Boolean).forEach((c) => componentMap.get(comp).removed.add(c));
|
|
53
|
+
}
|
|
54
|
+
const changes = [];
|
|
55
|
+
for (const [comp, { added, removed }] of componentMap) {
|
|
56
|
+
const pureAdded = [...added].filter((c) => !removed.has(c));
|
|
57
|
+
const pureRemoved = [...removed].filter((c) => !added.has(c));
|
|
58
|
+
if (pureAdded.length === 0 && pureRemoved.length === 0) continue;
|
|
59
|
+
changes.push({ component: comp, added: pureAdded, removed: pureRemoved });
|
|
60
|
+
}
|
|
61
|
+
return changes;
|
|
62
|
+
}
|
|
63
|
+
function renderStyleChanges(changes) {
|
|
64
|
+
const lines = [];
|
|
65
|
+
for (const change of changes) {
|
|
66
|
+
lines.push(`**${change.component}**`);
|
|
67
|
+
if (change.added.length > 0) lines.push(` + ${change.added.join(" ")}`);
|
|
68
|
+
if (change.removed.length > 0) lines.push(` - ${change.removed.join(" ")}`);
|
|
69
|
+
}
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
function isJSXElement(line) {
|
|
73
|
+
const t = line.trim();
|
|
74
|
+
return /^<[A-Za-z]/.test(t) || /^<\/[A-Za-z]/.test(t);
|
|
75
|
+
}
|
|
76
|
+
function isJSXClosing(line) {
|
|
77
|
+
return /^<\/[A-Za-z]/.test(line.trim());
|
|
78
|
+
}
|
|
79
|
+
function isJSXSelfClosing(line) {
|
|
80
|
+
return /\/>[\s]*$/.test(line.trim());
|
|
81
|
+
}
|
|
82
|
+
function extractJSXComponentName(line) {
|
|
83
|
+
const trimmed = line.trim();
|
|
84
|
+
const closingMatch = trimmed.match(/^<\/([A-Za-z][A-Za-z0-9.]*)/);
|
|
85
|
+
if (closingMatch) return `/${closingMatch[1]}`;
|
|
86
|
+
const nameMatch = trimmed.match(/^<([A-Za-z][A-Za-z0-9.]*)/);
|
|
87
|
+
if (!nameMatch) return trimmed;
|
|
88
|
+
const name = nameMatch[1];
|
|
89
|
+
const eventProps = [];
|
|
90
|
+
for (const m of trimmed.matchAll(/\b(on[A-Z]\w+)=/g)) {
|
|
91
|
+
eventProps.push(m[1]);
|
|
92
|
+
}
|
|
93
|
+
return eventProps.length > 0 ? `${name}(${eventProps.join(", ")})` : name;
|
|
94
|
+
}
|
|
95
|
+
function shouldIgnoreJSX(line) {
|
|
96
|
+
const t = line.trim();
|
|
97
|
+
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);
|
|
98
|
+
}
|
|
99
|
+
function parseJSXToFlowTree(lines) {
|
|
100
|
+
const roots = [];
|
|
101
|
+
const stack = [];
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
if (!isJSXElement(line)) continue;
|
|
104
|
+
if (shouldIgnoreJSX(line)) continue;
|
|
105
|
+
const depth = getIndentDepth(line);
|
|
106
|
+
if (isJSXClosing(line)) {
|
|
107
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
|
|
108
|
+
stack.pop();
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const name = extractJSXComponentName(line);
|
|
113
|
+
const selfClosing = isJSXSelfClosing(line);
|
|
114
|
+
const node = {
|
|
115
|
+
type: "call",
|
|
116
|
+
name,
|
|
117
|
+
children: [],
|
|
118
|
+
depth,
|
|
119
|
+
priority: 5 /* OTHER */
|
|
120
|
+
};
|
|
121
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
|
|
122
|
+
stack.pop();
|
|
123
|
+
}
|
|
124
|
+
if (stack.length === 0) {
|
|
125
|
+
roots.push(node);
|
|
126
|
+
} else {
|
|
127
|
+
stack[stack.length - 1].node.children.push(node);
|
|
128
|
+
}
|
|
129
|
+
if (!selfClosing) {
|
|
130
|
+
stack.push({ node, depth });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return roots;
|
|
134
|
+
}
|
|
10
135
|
function filterDiffLines(diffText) {
|
|
11
136
|
const lines = diffText.split("\n");
|
|
12
137
|
const added = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++") && l.trim() !== "+").map((l) => l.substring(1));
|
|
@@ -16,10 +141,10 @@ function filterDiffLines(diffText) {
|
|
|
16
141
|
function normalizeCode(lines) {
|
|
17
142
|
return lines.map((line) => {
|
|
18
143
|
let normalized = line;
|
|
19
|
-
normalized = normalized.replace(/;$/, "");
|
|
20
144
|
normalized = normalized.replace(/\/\/.*$/, "");
|
|
21
145
|
normalized = normalized.replace(/\/\*.*?\*\//, "");
|
|
22
146
|
normalized = normalized.trim();
|
|
147
|
+
normalized = normalized.replace(/;$/, "");
|
|
23
148
|
return normalized;
|
|
24
149
|
}).filter((line) => line.length > 0);
|
|
25
150
|
}
|
|
@@ -56,7 +181,13 @@ function isLoop(line) {
|
|
|
56
181
|
return /^(for|while)\s*\(/.test(line.trim());
|
|
57
182
|
}
|
|
58
183
|
function isFunctionDeclaration(line) {
|
|
59
|
-
|
|
184
|
+
const t = line.trim();
|
|
185
|
+
return (
|
|
186
|
+
// function foo() / async function foo()
|
|
187
|
+
/^(async\s+)?function\s+\w+/.test(t) || // const foo = () => / const foo = async () => / const foo = async (x: T) =>
|
|
188
|
+
/^(const|let|var)\s+\w+\s*=\s*(async\s*)?\(/.test(t) || // const foo = function / const foo = async function
|
|
189
|
+
/^(const|let|var)\s+\w+\s*=\s*(async\s+)?function/.test(t)
|
|
190
|
+
);
|
|
60
191
|
}
|
|
61
192
|
function shouldIgnore(line) {
|
|
62
193
|
const ignorePatterns = [
|
|
@@ -115,6 +246,19 @@ function parseToFlowTree(lines) {
|
|
|
115
246
|
prevLine = line;
|
|
116
247
|
continue;
|
|
117
248
|
}
|
|
249
|
+
if (isFunctionDeclaration(line)) {
|
|
250
|
+
const funcMatch = line.match(/(?:function|const|let|var)\s+(\w+)/);
|
|
251
|
+
roots.push({
|
|
252
|
+
type: "function",
|
|
253
|
+
name: funcMatch ? `${funcMatch[1]}()` : "function()",
|
|
254
|
+
children: [],
|
|
255
|
+
depth: relativeDepth,
|
|
256
|
+
priority: 4 /* FUNCTION */
|
|
257
|
+
});
|
|
258
|
+
currentChain = null;
|
|
259
|
+
prevLine = line;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
118
262
|
const root = extractRoot(line);
|
|
119
263
|
if (root) {
|
|
120
264
|
currentChain = {
|
|
@@ -145,16 +289,6 @@ function parseToFlowTree(lines) {
|
|
|
145
289
|
priority: 3 /* LOOP */
|
|
146
290
|
});
|
|
147
291
|
currentChain = null;
|
|
148
|
-
} else if (isFunctionDeclaration(line)) {
|
|
149
|
-
const funcMatch = line.match(/(?:function|const|let|var)\s+(\w+)/);
|
|
150
|
-
roots.push({
|
|
151
|
-
type: "function",
|
|
152
|
-
name: funcMatch ? `${funcMatch[1]}()` : "function()",
|
|
153
|
-
children: [],
|
|
154
|
-
depth: relativeDepth,
|
|
155
|
-
priority: 4 /* FUNCTION */
|
|
156
|
-
});
|
|
157
|
-
currentChain = null;
|
|
158
292
|
}
|
|
159
293
|
prevLine = line;
|
|
160
294
|
}
|
|
@@ -182,8 +316,20 @@ function parseDiffToLogicalFlow(diffText) {
|
|
|
182
316
|
};
|
|
183
317
|
}
|
|
184
318
|
function generateReaderMarkdown(diffText, meta = {}) {
|
|
185
|
-
const
|
|
319
|
+
const { added, removed } = filterDiffLines(diffText);
|
|
320
|
+
const isJSX = Boolean(
|
|
321
|
+
meta.file && isJSXFile(meta.file) || hasJSXContent(added)
|
|
322
|
+
);
|
|
323
|
+
const addedForFlow = isJSX ? added.filter((l) => !isClassNameOnlyLine(l)) : added;
|
|
324
|
+
const normalizedAdded = normalizeCode(addedForFlow);
|
|
325
|
+
const flowTree = parseToFlowTree(normalizedAdded);
|
|
326
|
+
const rawCode = addedForFlow.join("\n");
|
|
327
|
+
const removedForCode = isJSX ? removed.filter((l) => !isClassNameOnlyLine(l)) : removed;
|
|
328
|
+
const removedCode = removedForCode.join("\n");
|
|
329
|
+
const classNameChanges = isJSX ? parseClassNameChanges(added, removed) : [];
|
|
330
|
+
const jsxTree = isJSX ? parseJSXToFlowTree(added) : [];
|
|
186
331
|
const sections = [];
|
|
332
|
+
const lang = isJSX ? "tsx" : "typescript";
|
|
187
333
|
sections.push("# \u{1F4D6} GitHub Reader View\n");
|
|
188
334
|
sections.push("> Generated by **github-mobile-reader**");
|
|
189
335
|
if (meta.repo) sections.push(`> Repository: ${meta.repo}`);
|
|
@@ -191,22 +337,33 @@ function generateReaderMarkdown(diffText, meta = {}) {
|
|
|
191
337
|
if (meta.commit) sections.push(`> Commit: \`${meta.commit}\``);
|
|
192
338
|
if (meta.file) sections.push(`> File: \`${meta.file}\``);
|
|
193
339
|
sections.push("\n");
|
|
194
|
-
if (
|
|
340
|
+
if (flowTree.length > 0) {
|
|
195
341
|
sections.push("## \u{1F9E0} Logical Flow\n");
|
|
196
342
|
sections.push("```");
|
|
197
|
-
sections.push(...renderFlowTree(
|
|
343
|
+
sections.push(...renderFlowTree(flowTree));
|
|
198
344
|
sections.push("```\n");
|
|
199
345
|
}
|
|
200
|
-
if (
|
|
346
|
+
if (isJSX && jsxTree.length > 0) {
|
|
347
|
+
sections.push("## \u{1F3A8} JSX Structure\n");
|
|
348
|
+
sections.push("```");
|
|
349
|
+
sections.push(...renderFlowTree(jsxTree));
|
|
350
|
+
sections.push("```\n");
|
|
351
|
+
}
|
|
352
|
+
if (isJSX && classNameChanges.length > 0) {
|
|
353
|
+
sections.push("## \u{1F485} Style Changes\n");
|
|
354
|
+
sections.push(...renderStyleChanges(classNameChanges));
|
|
355
|
+
sections.push("");
|
|
356
|
+
}
|
|
357
|
+
if (rawCode.trim()) {
|
|
201
358
|
sections.push("## \u2705 Added Code\n");
|
|
202
|
-
sections.push(
|
|
203
|
-
sections.push(
|
|
359
|
+
sections.push(`\`\`\`${lang}`);
|
|
360
|
+
sections.push(rawCode);
|
|
204
361
|
sections.push("```\n");
|
|
205
362
|
}
|
|
206
|
-
if (
|
|
363
|
+
if (removedCode.trim()) {
|
|
207
364
|
sections.push("## \u274C Removed Code\n");
|
|
208
|
-
sections.push(
|
|
209
|
-
sections.push(
|
|
365
|
+
sections.push(`\`\`\`${lang}`);
|
|
366
|
+
sections.push(removedCode);
|
|
210
367
|
sections.push("```\n");
|
|
211
368
|
}
|
|
212
369
|
sections.push("---");
|
|
@@ -215,10 +372,19 @@ function generateReaderMarkdown(diffText, meta = {}) {
|
|
|
215
372
|
}
|
|
216
373
|
export {
|
|
217
374
|
Priority,
|
|
375
|
+
extractClassName,
|
|
376
|
+
extractJSXComponentName,
|
|
218
377
|
filterDiffLines,
|
|
219
378
|
generateReaderMarkdown,
|
|
379
|
+
hasJSXContent,
|
|
380
|
+
isClassNameOnlyLine,
|
|
381
|
+
isJSXElement,
|
|
382
|
+
isJSXFile,
|
|
220
383
|
normalizeCode,
|
|
384
|
+
parseClassNameChanges,
|
|
221
385
|
parseDiffToLogicalFlow,
|
|
386
|
+
parseJSXToFlowTree,
|
|
222
387
|
parseToFlowTree,
|
|
223
|
-
renderFlowTree
|
|
388
|
+
renderFlowTree,
|
|
389
|
+
renderStyleChanges
|
|
224
390
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-mobile-reader",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Transform git diffs into mobile-friendly Markdown — no more horizontal scrolling when reviewing code on your phone.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github",
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "https://github.com/3rdflr/github-mobile-reader.git"
|
|
20
|
+
"url": "git+https://github.com/3rdflr/github-mobile-reader.git"
|
|
21
21
|
},
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"author": "3rdflrhtl@gmail.com",
|
|
24
|
+
"bin": {
|
|
25
|
+
"github-mobile-reader": "dist/cli.js"
|
|
26
|
+
},
|
|
24
27
|
"main": "./dist/index.js",
|
|
25
28
|
"module": "./dist/index.mjs",
|
|
26
29
|
"types": "./dist/index.d.ts",
|
|
@@ -39,7 +42,8 @@
|
|
|
39
42
|
"scripts": {
|
|
40
43
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
41
44
|
"build:action": "tsup src/action.ts --format cjs --outDir dist --no-splitting",
|
|
42
|
-
"build:
|
|
45
|
+
"build:cli": "tsup src/cli.ts --format cjs --outDir dist --no-splitting",
|
|
46
|
+
"build:all": "npm run build && npm run build:action && npm run build:cli",
|
|
43
47
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
44
48
|
"prepublishOnly": "npm run build:all"
|
|
45
49
|
},
|