Skip to content
Open
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
Binary file modified docs/assets/chrome-extension-parse.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 56 additions & 1 deletion extension/popup.css
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,63 @@ body {
color: var(--muted);
}

.actions {
.toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}

.segmented {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 999px;
padding: 6px;
}

.segmented-label {
color: var(--muted);
font-size: 12px;
font-weight: 600;
padding: 0 8px 0 10px;
}

.segmented-option {
border: 1px solid transparent;
background: transparent;
color: var(--ink);
border-radius: 999px;
cursor: pointer;
font: inherit;
font-size: 12px;
letter-spacing: 0.01em;
min-height: 28px;
padding: 0 10px;
transition: background-color 140ms ease, opacity 140ms ease;
}

.segmented-option:hover:enabled {
background: rgba(108, 99, 255, 0.08);
transform: none;
}

.segmented-option:disabled {
cursor: not-allowed;
opacity: 0.55;
}

.segmented-option.is-active {
background: var(--accent-soft);
color: var(--accent-strong);
border-color: rgba(108, 99, 255, 0.18);
font-weight: 700;
}

.actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}

Expand Down
9 changes: 9 additions & 0 deletions extension/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,18 @@ <h1>Scrape GitHub</h1>

<p id="status-text" class="status-text">Open a GitHub repository root or user profile root.</p>

<div class="toolbar">
<div class="segmented" role="group" aria-label="Output format">
<button id="format-json" class="segmented-option is-active" type="button" disabled>JSON</button>
<button id="format-csv" class="segmented-option" type="button" disabled>CSV</button>
</div>
</div>

<div class="actions">
<button id="parse-button" class="primary" type="button" disabled>Parse current page</button>
<button id="copy-button" class="secondary" type="button" disabled>Copy JSON</button>
<button id="download-json-button" class="secondary" type="button" disabled>Download JSON</button>
<button id="download-csv-button" class="secondary" type="button" disabled>Download CSV</button>
</div>

<pre id="output" class="output">No parsed result yet.</pre>
Expand Down
150 changes: 140 additions & 10 deletions extension/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ import { detectGitHubPageType, parseGitHubPage } from "./github-parsers.js";

const parseButton = document.getElementById("parse-button");
const copyButton = document.getElementById("copy-button");
const downloadJsonButton = document.getElementById("download-json-button");
const downloadCsvButton = document.getElementById("download-csv-button");
const formatJsonButton = document.getElementById("format-json");
const formatCsvButton = document.getElementById("format-csv");
const output = document.getElementById("output");
const statusBadge = document.getElementById("status-badge");
const statusText = document.getElementById("status-text");

let lastResult = null;
let currentTab = null;
let currentPageType = null;
let outputFormat = "json";

function getOutputFormat() {
return outputFormat === "csv" ? "csv" : "json";
}

function setStatus(label, message) {
statusBadge.textContent = label;
Expand All @@ -19,6 +28,97 @@ function setOutput(value) {
output.textContent = value;
}

function escapeCsvCell(value) {
const text = String(value ?? "");
if (/[",\n\r]/.test(text)) {
return `"${text.replace(/"/g, '""')}"`;
}
return text;
}

function toCsvRow(values) {
return values.map(escapeCsvCell).join(",");
}

function toCsv(result) {
if (!result || typeof result !== "object") {
return "";
}

const keys = Object.keys(result);
const header = toCsvRow(keys);
const row = toCsvRow(
keys.map((key) => {
const value = result[key];
if (value === null || value === undefined) return "";
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return value;
}
return JSON.stringify(value);
})
);

return `${header}\n${row}\n`;
}

function getOutputText(format) {
if (!lastResult) {
return "No parsed result yet.";
}
if (format === "csv") {
return toCsv(lastResult);
}
return JSON.stringify(lastResult, null, 2);
}

function getBaseFilename(result) {
if (!result || !result.success) {
return "github-parse";
}
if (result.type === "repository" && result.fullName) {
return `github-repo-${result.fullName.replace("/", "__")}`;
}
if (result.type === "user_profile" && result.username) {
return `github-user-${result.username}`;
}
return `github-${result.type || "parse"}`;
}

function downloadTextFile({ text, filename, mimeType }) {
const blob = new Blob([text], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
}

function setOutputFormat(nextFormat) {
outputFormat = nextFormat === "csv" ? "csv" : "json";
formatJsonButton?.classList.toggle("is-active", outputFormat === "json");
formatCsvButton?.classList.toggle("is-active", outputFormat === "csv");
updateUi();
}

function updateUi() {
const isSupported = Boolean(currentPageType);
const hasResult = Boolean(lastResult);
const format = getOutputFormat();

parseButton.disabled = !isSupported;
formatJsonButton.disabled = !isSupported;
formatCsvButton.disabled = !isSupported;
copyButton.disabled = !isSupported || !hasResult;
downloadJsonButton.disabled = !isSupported || !hasResult;
downloadCsvButton.disabled = !isSupported || !hasResult;

copyButton.textContent = format === "csv" ? "Copy CSV" : "Copy JSON";
setOutput(getOutputText(format));
}

async function getActiveTab() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
return tab || null;
Expand All @@ -42,19 +142,17 @@ async function refreshPageSupport() {
currentPageType = detectGitHubPageType(url);

if (!currentPageType) {
parseButton.disabled = true;
copyButton.disabled = true;
setStatus("Unsupported", "Supported pages are GitHub repository roots and personal profile roots only.");
setOutput("No parsed result yet.");
lastResult = null;
updateUi();
return;
}

parseButton.disabled = false;
copyButton.disabled = !lastResult;
setStatus(
currentPageType === "repository" ? "Repository" : "User profile",
"This page is supported. Run the local parser to generate structured JSON."
);
updateUi();
}

parseButton.addEventListener("click", async () => {
Expand All @@ -64,18 +162,19 @@ parseButton.addEventListener("click", async () => {

parseButton.disabled = true;
copyButton.disabled = true;
downloadJsonButton.disabled = true;
downloadCsvButton.disabled = true;
setStatus("Parsing", "Reading the current page and running the parser locally.");
setOutput("Parsing...");

try {
const { html, url } = await readActiveTabHtml(currentTab.id);
const result = parseGitHubPage(currentPageType, html, url);
lastResult = result;
setOutput(JSON.stringify(result, null, 2));
updateUi();

if (result.success) {
setStatus("Success", "Structured JSON generated for the current page.");
copyButton.disabled = false;
} else {
setStatus("Parser error", result.error || "Unable to parse this page.");
}
Expand All @@ -84,7 +183,7 @@ parseButton.addEventListener("click", async () => {
setStatus("Runtime error", error.message);
setOutput(error.stack || error.message);
} finally {
parseButton.disabled = false;
updateUi();
}
});

Expand All @@ -93,8 +192,39 @@ copyButton.addEventListener("click", async () => {
return;
}

await navigator.clipboard.writeText(JSON.stringify(lastResult, null, 2));
setStatus("Copied", "JSON copied to the clipboard.");
const format = getOutputFormat();
await navigator.clipboard.writeText(getOutputText(format));
setStatus("Copied", format === "csv" ? "CSV copied to the clipboard." : "JSON copied to the clipboard.");
});

downloadJsonButton.addEventListener("click", () => {
if (!lastResult) return;
const base = getBaseFilename(lastResult);
downloadTextFile({
text: JSON.stringify(lastResult, null, 2),
filename: `${base}.json`,
mimeType: "application/json"
});
setStatus("Downloaded", "JSON saved to your downloads.");
});

downloadCsvButton.addEventListener("click", () => {
if (!lastResult) return;
const base = getBaseFilename(lastResult);
downloadTextFile({
text: toCsv(lastResult),
filename: `${base}.csv`,
mimeType: "text/csv"
});
setStatus("Downloaded", "CSV saved to your downloads.");
});

formatJsonButton.addEventListener("click", () => {
setOutputFormat("json");
});

formatCsvButton.addEventListener("click", () => {
setOutputFormat("csv");
});

refreshPageSupport();