// src/index.ts
async function githubTree(name, ref) {
return {
id: 0,
path: "",
filename: name,
kind: {
type: "dir",
children: await githubListRecursive(name, ref)
}
};
}
async function githubListRecursive(name, ref, path = "", nextId = [1]) {
const fileNodes = await fetch(
`https://api.github.com/repos/${name}/contents${path}?ref=${ref}`
).then((res) => res.json());
return Promise.all(
fileNodes.map(async (node) => {
const id = nextId[0]++;
if (node.type === "dir") {
return await githubListRecursive(
name,
ref,
`${path}/${node.name}`,
nextId
).then((children) => ({
id,
filename: node.name,
path,
kind: { type: "dir", children }
}));
} else if (node.type === "file") {
return {
id,
filename: node.name,
path,
kind: { type: "file", url: node.download_url }
};
}
throw new Error();
})
);
}
function generateTreeHtml(node) {
return `
${generateNodeHtml(node)}
`;
}
function generateNodeHtml(node) {
if (node.kind.type === "dir") {
const children = node.kind.children.map((node2) => generateNodeHtml(node2)).join("");
return `
${node.filename}/
`;
} else if (node.kind.type === "file") {
return `
${node.filename}
`;
}
throw new Error();
}
function queryHtmlINodes(tree) {
const nodes = /* @__PURE__ */ new Map();
queryHtmlINodesRecursive(nodes, tree);
return nodes;
}
function queryHtmlINodesRecursive(nodes, node, parentId) {
const checkbox = document.querySelector(`#checkbox-${node.id}`);
if (node.kind.type === "dir") {
nodes.set(node.id, { node, parentId, checkbox });
for (const child of node.kind.children) {
queryHtmlINodesRecursive(nodes, child, node.id);
}
} else if (node.kind.type === "file") {
nodes.set(node.id, { node, parentId, checkbox });
}
}
function hydrateHtmlTree(tree) {
for (const node of tree.values()) {
node.checkbox.addEventListener("change", () => {
if (node.node.kind.type === "dir") {
setCheckChildrenRecursively(
tree,
node.node.id,
node.checkbox.checked
);
}
checkParentDirCheckRecursively(tree, node.parentId);
});
}
}
function setCheckChildrenRecursively(tree, id, state) {
const node = tree.get(id);
if (node.node.kind.type !== "dir") {
throw new Error();
}
for (const { id: childId } of node.node.kind.children) {
const child = tree.get(childId);
child.checkbox.checked = state;
if (child.node.kind.type === "dir") {
setCheckChildrenRecursively(tree, childId, state);
}
}
}
function checkParentDirCheckRecursively(tree, parentId) {
if (parentId === void 0) {
return;
}
const parent = tree.get(parentId);
if (parent === void 0) {
throw new Error();
}
if (parent.node.kind.type !== "dir") {
return;
}
const checked = parent.node.kind.children.some(({ id }) => tree.get(id).checkbox.checked);
parent.checkbox.checked = checked;
checkParentDirCheckRecursively(tree, parent.parentId);
}
async function* searchTree(tree, pattern) {
for (const node of tree.values()) {
if (node.node.kind.type !== "file") {
continue;
}
if (!node.checkbox.checked) {
continue;
}
const text = await fetch(node.node.kind.url).then((res) => res.text());
const lines = text.split("\n").map((v, i) => [v, i]);
for (const [linetext, i] of lines) {
if (!pattern.test(linetext)) {
continue;
}
const { filename, path } = node.node;
const linenr = i + 1;
yield { filename, path, linetext, linenr };
}
}
}
var repoNameInput = document.querySelector("#repo-name");
var repoRefInput = document.querySelector("#repo-ref");
var gitProviderSelect = document.querySelector("#git-provider");
var loadRepoButton = document.querySelector("#load-repo");
var fileTreeDiv = document.querySelector("#file-tree");
var searchPatternInput = document.querySelector("#search-pattern");
var searchButton = document.querySelector("#search");
var searchStatusSpan = document.querySelector("#search-status");
var searchResultsDiv = document.querySelector("#search-results");
loadRepoButton.onclick = async () => {
const name = repoNameInput.value;
const ref = repoRefInput.value;
const provider = gitProviderSelect.value;
if (provider === "github") {
const tree = await githubTree(name, ref);
fileTreeDiv.innerHTML = generateTreeHtml(tree);
const htmlTree = queryHtmlINodes(tree);
hydrateHtmlTree(htmlTree);
searchStatusSpan.innerText = ``;
searchButton.onclick = async () => {
const patternText = searchPatternInput.value;
const pattern = new RegExp(patternText);
searchStatusSpan.innerText = "Searching ...";
searchResultsDiv.innerHTML = "";
let matches = 0;
for await (const match of searchTree(htmlTree, pattern)) {
matches += 1;
searchResultsDiv.innerHTML += `
${match.path}/${match.filename} line ${match.linenr}
${match.linetext}
`;
}
if (matches === 0) {
searchStatusSpan.innerText = "** Nothing found **";
} else {
searchStatusSpan.innerText = `\u22C6\u02D9\u27E1 Done, ${matches} matches \u27E1\u02D9\u22C6`;
}
};
}
};
searchButton.onclick = () => {
searchStatusSpan.innerText = `No repository loaded :(`;
};