Skip to content

Commit cac8518

Browse files
feat: add GitHub URL markdown import support
Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com>
1 parent 53d1c92 commit cac8518

5 files changed

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

1423+
importGithubButton.addEventListener("click", function () {
1424+
importMarkdownFromGitHub();
1425+
});
1426+
12461427
fileInput.addEventListener("change", function (e) {
12471428
const file = e.target.files[0];
12481429
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)