Skip to content

Commit b2c3c85

Browse files
Merge pull request #46 from ThisIs-Developer/copilot/add-import-markdown-from-github
Add GitHub URL-based Markdown import (repo/tree/blob/raw) with file discovery and selection
2 parents 6c16a5f + eca1c63 commit b2c3c85

5 files changed

Lines changed: 203 additions & 3 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Markdown Viewer is a professional, full-featured Markdown editor and preview app
2424
- **Mermaid diagrams** - Create diagrams and flowcharts within your Markdown; hover over any diagram to reveal a toolbar for zooming, downloading (PNG/SVG), and copying to clipboard
2525
- **Dark mode toggle** - Switch between light and dark themes for comfortable viewing
2626
- **Export options** - Download your content as Markdown, HTML, or PDF
27-
- **Import Markdown files** - Drag & drop or select files to open
27+
- **Import Markdown files** - Drag & drop, select local files, or import from public GitHub links
2828
- **Copy to clipboard** - Quickly copy your Markdown content with one click
2929
- **Sync scrolling** - Keep editor and preview panes aligned (toggleable)
3030
- **Content statistics** - Track word count, character count, and reading time
@@ -51,7 +51,7 @@ Markdown Viewer is a professional, full-featured Markdown editor and preview app
5151

5252
1. **Writing Markdown** - Type or paste Markdown content in the left editor panel
5353
2. **Viewing Output** - See the rendered HTML in the right preview panel
54-
3. **Importing Files** - Click "Import" or drag and drop .md files into the interface
54+
3. **Importing Files** - Click "Import" for local files, use "GitHub Import" for repository links, or drag and drop .md files
5555
4. **Exporting Content** - Use the "Export" dropdown to download as MD, HTML, or PDF
5656
5. **Toggle Dark Mode** - Click the moon icon to switch between light and dark themes
5757
6. **Toggle Sync Scrolling** - Enable/disable synchronized scrolling between panels

index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ <h1 class="h4 mb-0 me-2">Markdown Viewer</h1>
102102
<button id="import-button" class="tool-button" title="Import Markdown">
103103
<i class="bi bi-upload"></i> Import
104104
</button>
105+
<button id="import-github-button" class="tool-button" title="Import Markdown from GitHub URL">
106+
<i class="bi bi-github"></i> GitHub Import
107+
</button>
105108
<input type="file" id="file-input" class="file-input" accept=".md,.markdown,text/markdown">
106109

107110
<div class="dropdown">
@@ -195,6 +198,10 @@ <h5>Menu</h5>
195198
<i class="bi bi-upload me-2"></i> Import Markdown
196199
</button>
197200

201+
<button id="mobile-import-github-button" class="mobile-menu-item" title="Import Markdown from GitHub URL">
202+
<i class="bi bi-github me-2"></i> Import from GitHub
203+
</button>
204+
198205
<button id="mobile-export-md" class="mobile-menu-item" title="Export as Markdown">
199206
<i class="bi bi-file-earmark-text me-2"></i> Export as Markdown
200207
</button>

script.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ document.addEventListener("DOMContentLoaded", function () {
1414
const markdownPreview = document.getElementById("markdown-preview");
1515
const themeToggle = document.getElementById("theme-toggle");
1616
const importButton = document.getElementById("import-button");
17+
const importGithubButton = document.getElementById("import-github-button");
1718
const fileInput = document.getElementById("file-input");
1819
const exportMd = document.getElementById("export-md");
1920
const exportHtml = document.getElementById("export-html");
@@ -52,6 +53,7 @@ document.addEventListener("DOMContentLoaded", function () {
5253
const mobileCharCount = document.getElementById("mobile-char-count");
5354
const mobileToggleSync = document.getElementById("mobile-toggle-sync");
5455
const mobileImportBtn = document.getElementById("mobile-import-button");
56+
const mobileImportGithubBtn = document.getElementById("mobile-import-github-button");
5557
const mobileExportMd = document.getElementById("mobile-export-md");
5658
const mobileExportHtml = document.getElementById("mobile-export-html");
5759
const mobileExportPdf = document.getElementById("mobile-export-pdf");
@@ -812,6 +814,179 @@ This is a fully client-side application. Your content never leaves your browser
812814
reader.readAsText(file);
813815
}
814816

817+
function isMarkdownPath(path) {
818+
return /\.(md|markdown)$/i.test(path || "");
819+
}
820+
const MAX_GITHUB_FILES_SHOWN = 30;
821+
822+
function getFileName(path) {
823+
return (path || "").split("/").pop() || "document.md";
824+
}
825+
826+
function buildRawGitHubUrl(owner, repo, ref, filePath) {
827+
const encodedPath = filePath
828+
.split("/")
829+
.map((part) => encodeURIComponent(part))
830+
.join("/");
831+
return `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(ref)}/${encodedPath}`;
832+
}
833+
834+
async function fetchGitHubJson(url) {
835+
const response = await fetch(url, {
836+
headers: {
837+
Accept: "application/vnd.github+json"
838+
}
839+
});
840+
if (!response.ok) {
841+
throw new Error(`GitHub API request failed (${response.status})`);
842+
}
843+
return response.json();
844+
}
845+
846+
async function fetchTextContent(url) {
847+
const response = await fetch(url);
848+
if (!response.ok) {
849+
throw new Error(`Failed to fetch file (${response.status})`);
850+
}
851+
return response.text();
852+
}
853+
854+
function parseGitHubImportUrl(input) {
855+
let parsedUrl;
856+
try {
857+
parsedUrl = new URL((input || "").trim());
858+
} catch (_) {
859+
return null;
860+
}
861+
862+
const host = parsedUrl.hostname.replace(/^www\./, "");
863+
const segments = parsedUrl.pathname.split("/").filter(Boolean);
864+
865+
if (host === "raw.githubusercontent.com") {
866+
if (segments.length < 5) return null;
867+
const [owner, repo, ref, ...rest] = segments;
868+
const filePath = rest.join("/");
869+
return { owner, repo, ref, type: "file", filePath };
870+
}
871+
872+
if (host !== "github.com" || segments.length < 2) return null;
873+
874+
const owner = segments[0];
875+
const repo = segments[1].replace(/\.git$/i, "");
876+
if (segments.length === 2) {
877+
return { owner, repo, type: "repo" };
878+
}
879+
880+
const mode = segments[2];
881+
if (mode === "blob" && segments.length >= 5) {
882+
return {
883+
owner,
884+
repo,
885+
type: "file",
886+
ref: segments[3],
887+
filePath: segments.slice(4).join("/")
888+
};
889+
}
890+
891+
if (mode === "tree" && segments.length >= 4) {
892+
return {
893+
owner,
894+
repo,
895+
type: "tree",
896+
ref: segments[3],
897+
basePath: segments.slice(4).join("/")
898+
};
899+
}
900+
901+
return { owner, repo, type: "repo" };
902+
}
903+
904+
async function getDefaultBranch(owner, repo) {
905+
const repoInfo = await fetchGitHubJson(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`);
906+
return repoInfo.default_branch;
907+
}
908+
909+
async function listMarkdownFiles(owner, repo, ref, basePath) {
910+
const treeResponse = await fetchGitHubJson(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/trees/${encodeURIComponent(ref)}?recursive=1`);
911+
const normalizedBasePath = (basePath || "").replace(/^\/+|\/+$/g, "");
912+
913+
return (treeResponse.tree || [])
914+
.filter((entry) => entry.type === "blob" && isMarkdownPath(entry.path))
915+
.filter((entry) => !normalizedBasePath || entry.path === normalizedBasePath || entry.path.startsWith(normalizedBasePath + "/"))
916+
.map((entry) => entry.path)
917+
.sort((a, b) => a.localeCompare(b));
918+
}
919+
920+
function setGitHubImportLoading(isLoading) {
921+
[importGithubButton, mobileImportGithubBtn].forEach((btn) => {
922+
if (!btn) return;
923+
if (!btn.dataset.originalText) {
924+
btn.dataset.originalText = btn.innerHTML;
925+
}
926+
btn.disabled = isLoading;
927+
btn.innerHTML = isLoading ? '<i class="bi bi-hourglass-split"></i> Importing...' : btn.dataset.originalText;
928+
});
929+
}
930+
931+
async function importMarkdownFromGitHub() {
932+
const urlInput = prompt("Enter a GitHub repository, folder, or Markdown file URL:");
933+
if (!urlInput) return;
934+
935+
const parsed = parseGitHubImportUrl(urlInput);
936+
if (!parsed || !parsed.owner || !parsed.repo) {
937+
alert("Please enter a valid GitHub URL.");
938+
return;
939+
}
940+
941+
setGitHubImportLoading(true);
942+
try {
943+
if (parsed.type === "file") {
944+
if (!isMarkdownPath(parsed.filePath)) {
945+
throw new Error("The provided URL does not point to a Markdown file.");
946+
}
947+
const markdown = await fetchTextContent(buildRawGitHubUrl(parsed.owner, parsed.repo, parsed.ref, parsed.filePath));
948+
newTab(markdown, getFileName(parsed.filePath).replace(/\.(md|markdown)$/i, ""));
949+
return;
950+
}
951+
952+
const ref = parsed.ref || await getDefaultBranch(parsed.owner, parsed.repo);
953+
const files = await listMarkdownFiles(parsed.owner, parsed.repo, ref, parsed.basePath || "");
954+
955+
if (!files.length) {
956+
alert("No Markdown files were found at that GitHub location.");
957+
return;
958+
}
959+
960+
let targetPath = files[0];
961+
if (files.length > 1) {
962+
const maxShown = Math.min(files.length, MAX_GITHUB_FILES_SHOWN);
963+
const choices = files
964+
.slice(0, maxShown)
965+
.map((file, index) => `${index + 1}. ${file}`)
966+
.join("\n");
967+
const choice = prompt(
968+
`Found ${files.length} Markdown files.\nEnter a number to import (showing first ${maxShown}):\n\n${choices}${files.length > maxShown ? "\n..." : ""}`,
969+
"1"
970+
);
971+
972+
if (choice === null) return;
973+
const selectedIndex = Number(choice) - 1;
974+
if (!Number.isInteger(selectedIndex) || selectedIndex < 0 || selectedIndex >= maxShown) {
975+
throw new Error(`Please enter a number between 1 and ${maxShown} (from the displayed list).`);
976+
}
977+
targetPath = files[selectedIndex];
978+
}
979+
980+
const markdown = await fetchTextContent(buildRawGitHubUrl(parsed.owner, parsed.repo, ref, targetPath));
981+
newTab(markdown, getFileName(targetPath).replace(/\.(md|markdown)$/i, ""));
982+
} catch (error) {
983+
console.error("GitHub import failed:", error);
984+
alert("GitHub import failed: " + error.message);
985+
} finally {
986+
setGitHubImportLoading(false);
987+
}
988+
}
989+
815990
function processEmojis(element) {
816991
const walker = document.createTreeWalker(
817992
element,
@@ -1141,6 +1316,9 @@ This is a fully client-side application. Your content never leaves your browser
11411316
}
11421317
});
11431318
mobileImportBtn.addEventListener("click", () => fileInput.click());
1319+
mobileImportGithubBtn.addEventListener("click", () => {
1320+
importMarkdownFromGitHub().finally(closeMobileMenu);
1321+
});
11441322
mobileExportMd.addEventListener("click", () => exportMd.click());
11451323
mobileExportHtml.addEventListener("click", () => exportHtml.click());
11461324
mobileExportPdf.addEventListener("click", () => exportPdf.click());
@@ -1243,6 +1421,10 @@ This is a fully client-side application. Your content never leaves your browser
12431421
fileInput.click();
12441422
});
12451423

1424+
importGithubButton.addEventListener("click", function () {
1425+
importMarkdownFromGitHub();
1426+
});
1427+
12461428
fileInput.addEventListener("change", function (e) {
12471429
const file = e.target.files[0];
12481430
if (file) {

wiki/Features.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ Generates a PDF of the current preview using **jsPDF** + **html2canvas**. Comple
167167

168168
- **Drag & Drop**: Drag any `.md` file onto the editor pane.
169169
- **File Picker**: Click the Import button to open the OS file dialog.
170+
- **GitHub Import**: Paste a public GitHub repository/folder/file link to discover and import Markdown files.
170171

171172
Supported extensions: `.md`, `.markdown`.
172173

wiki/Usage-Guide.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ When you first open the application, the editor contains a sample document demon
5656

5757
## Importing Files
5858

59-
You can load an existing `.md` file into the editor in two ways:
59+
You can load Markdown into the editor in three ways:
6060

6161
### Drag & Drop
6262

@@ -68,6 +68,16 @@ Drag a `.md` file from your file explorer directly onto the **editor pane**. The
6868
2. Select a `.md` file in the file picker dialog.
6969
3. The file content is loaded into the editor.
7070

71+
### GitHub Import
72+
73+
1. Click **GitHub Import** in the toolbar (or in the mobile menu).
74+
2. Paste a public GitHub URL:
75+
- Repository URL (for example: `https://github.com/owner/repo`)
76+
- Folder URL (for example: `https://github.com/owner/repo/tree/main/docs`)
77+
- Markdown file URL (for example: `https://github.com/owner/repo/blob/main/README.md`)
78+
3. If multiple Markdown files are found, choose the file number from the prompt list.
79+
4. The selected file is opened in a new tab.
80+
7181
> **Tip**: Only `.md` and `.markdown` files are accepted.
7282
7383
---

0 commit comments

Comments
 (0)