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
2 changes: 2 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { expressiveCodeConfig } from "./src/config.ts";
import { pluginLanguageBadge } from "./src/plugins/expressive-code/language-badge.ts";
import { AdmonitionComponent } from "./src/plugins/rehype-component-admonition.mjs";
import { GithubCardComponent } from "./src/plugins/rehype-component-github-card.mjs";
import { GithubFileCardComponent } from "./src/plugins/rehype-component-github-file-card.mjs";
import { parseDirectiveNode } from "./src/plugins/remark-directive-rehype.js";
import { remarkExcerpt } from "./src/plugins/remark-excerpt.js";
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs";
Expand Down Expand Up @@ -120,6 +121,7 @@ export default defineConfig({
{
components: {
github: GithubCardComponent,
githubfile: GithubFileCardComponent,
note: (x, y) => AdmonitionComponent(x, y, "note"),
tip: (x, y) => AdmonitionComponent(x, y, "tip"),
important: (x, y) => AdmonitionComponent(x, y, "important"),
Expand Down
16 changes: 14 additions & 2 deletions src/content/posts/markdown-extended.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ description: 'Read more about Markdown features in Fuwari'
image: ''
tags: [Demo, Example, Markdown, Fuwari]
category: 'Examples'
draft: false
draft: false
---

## GitHub Repository Cards
You can add dynamic cards that link to GitHub repositories, on page load, the repository information is pulled from the GitHub API.
You can add dynamic cards that link to GitHub repositories, on page load, the repository information is pulled from the GitHub API.

::github{repo="Fabrizz/MMM-OnSpotify"}

Expand All @@ -20,6 +20,18 @@ Create a GitHub repository card with the code `::github{repo="<owner>/<repo>"}`.
::github{repo="saicaca/fuwari"}
```

## GitHub File Cards
You can add dynamic cards that link to a specific file in a GitHub repository, on page load, the file metadata is pulled from the GitHub API.

::githubfile{repo="saicaca/fuwari" file="README.md" description="Theme overview"}

Create a GitHub file card with the code `::githubfile{repo="<owner>/<repo>" file="path/to/file.ext"}`.

Optional parameters: `description` (string) and `path` (alias for `file`).

```markdown
::githubfile{repo="saicaca/fuwari" file="README.md" description="Theme overview"}
```
## Admonitions

Following types of admonitions are supported: `note` `tip` `important` `warning` `caution`
Expand Down
203 changes: 203 additions & 0 deletions src/plugins/rehype-component-github-file-card.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/// <reference types="mdast" />
import { h } from "hastscript";

/**
* Creates a GitHub File Card component.
*
* @param {Object} properties - The properties of the component.
* @param {string} properties.repo - The GitHub repository in the format "owner/repo".
* @param {string} properties.file - The file path in the repository.
* @param {string} properties.description
* @param {import('mdast').RootContent[]} children - The children elements of the component.
* @returns {import('mdast').Parent} The created GitHub File Card component.
*/
export function GithubFileCardComponent(properties, children) {
if (Array.isArray(children) && children.length !== 0)
return h("div", { class: "hidden" }, [
'Invalid directive. ("githubfile" directive must be leaf type "::githubfile{repo="owner/repo" file="path/to/file"}")',
]);

if (!properties.repo || !properties.repo.includes("/"))
return h(
"div",
{ class: "hidden" },
'Invalid repository. ("repo" attribute must be in the format "owner/repo")',
);

const filePath = properties.file || properties.path;
if (!filePath)
return h(
"div",
{ class: "hidden" },
'Invalid file. ("file" attribute must be set to a repo path like "path/to/file.ext")',
);

const description = properties.description || "No description";

const repo = properties.repo;
const owner = repo.split("/")[0];
const repoName = repo.split("/")[1];
const ref = properties.ref || "HEAD";
const fileName = filePath.split("/").filter(Boolean).pop();
if (!fileName)
return h(
"div",
{ class: "hidden" },
'Invalid file. ("file" attribute must include a file name)',
);

const fileTypeLabel = fileName.includes(".")
? fileName.split(".").pop().toLowerCase()
: "file";

const encodedFilePath = filePath
.split("/")
.map((segment) => encodeURIComponent(segment))
.join("/");
const refQuery = ref === "HEAD" ? "" : `?ref=${encodeURIComponent(ref)}`;
const contentsUrl = `https://api.github.com/repos/${repo}/contents/${encodedFilePath}${refQuery}`;
const commitRefQuery =
ref === "HEAD" ? "" : `&sha=${encodeURIComponent(ref)}`;
const commitsUrl = `https://api.github.com/repos/${repo}/commits?path=${encodeURIComponent(filePath)}&per_page=1${commitRefQuery}`;

const cardUuid = `GFC${Math.random().toString(36).slice(-6)}`; // Collisions are not important
const fileUrl = encodeURI(
`https://github.com/${repo}/blob/${ref}/${filePath}`,
);

const nAvatar = h(`div#${cardUuid}-avatar`, { class: "gc-avatar" });

const nTitle = h("div", { class: "gc-titlebar" }, [
h("div", { class: "gc-titlebar-left" }, [
h("div", { class: "gc-owner" }, [
nAvatar,
h("div", { class: "gc-user" }, owner),
]),
h("div", { class: "gc-divider" }, "/"),
h("div", { class: "gc-user" }, repoName),
h("div", { class: "gc-divider" }, "/"),
h("div", { class: "gc-repo" }, fileName),
]),
h("div", { class: "github-logo" }),
]);

const nDescription = h(
`div#${cardUuid}-description`,
{ class: "gc-description" },
description,
);

const nFileType = h(
`div#${cardUuid}-filetype`,
{ class: "gc-filetype" },
fileTypeLabel,
);
const nFileSize = h(
`div#${cardUuid}-filesize`,
{ class: "gc-filesize" },
"...",
);
const nUpdated = h(`div#${cardUuid}-updated`, { class: "gc-updated" }, "...");
const nInfoBar = h("div", { class: "gc-infobar" }, [
nFileType,
nFileSize,
nUpdated,
]);

const nScript = h(
`script#${cardUuid}-script`,
{ type: "text/javascript", defer: true },
`
const initGithubFileCard = () => {
const cardEl = document.getElementById('${cardUuid}-card');
const avatarEl = document.getElementById('${cardUuid}-avatar');
const fileTypeEl = document.getElementById('${cardUuid}-filetype');
const fileSizeEl = document.getElementById('${cardUuid}-filesize');
const updatedEl = document.getElementById('${cardUuid}-updated');
if (!cardEl || !avatarEl || !fileTypeEl || !fileSizeEl || !updatedEl) {
console.warn("[GITHUB-FILE-CARD] Missing card elements for ${repo} | ${cardUuid}.");
return;
}

const formatBytes = (bytes) => {
if (typeof bytes !== "number" || Number.isNaN(bytes)) return "unknown";
const units = ["B", "KB", "MB", "GB", "TB"];
let size = bytes;
let unit = 0;
while (size >= 1024 && unit < units.length - 1) {
size /= 1024;
unit += 1;
}
const precision = size < 10 && unit > 0 ? 1 : 0;
return size.toFixed(precision) + units[unit];
};

const formatDate = (isoString) => {
if (!isoString) return "unknown";
const date = new Date(isoString);
if (Number.isNaN(date.getTime())) return "unknown";
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "2-digit",
});
};

const repoFetch = fetch('https://api.github.com/repos/${repo}', { referrerPolicy: "no-referrer" })
.then(response => response.json())
.then(data => {
const avatarUrl = data?.owner?.avatar_url || 'https://github.com/${owner}.png?size=96';
avatarEl.style.backgroundImage = 'url(' + avatarUrl + ')';
avatarEl.style.backgroundColor = 'transparent';
})
.catch(() => {
console.warn("[GITHUB-FILE-CARD] (Error) Loading avatar for ${repo} | ${cardUuid}.");
});

const contentsFetch = fetch('${contentsUrl}', { referrerPolicy: "no-referrer" })
.then(response => response.json())
.then(data => {
fileSizeEl.innerText = formatBytes(data?.size);
})
.catch(() => {
fileSizeEl.innerText = "unknown";
console.warn("[GITHUB-FILE-CARD] (Error) Loading file size for ${repo} | ${cardUuid}.");
});

const commitsFetch = fetch('${commitsUrl}', { referrerPolicy: "no-referrer" })
.then(response => response.json())
.then(data => {
const commitDate = data?.[0]?.commit?.committer?.date || data?.[0]?.commit?.author?.date;
updatedEl.innerText = formatDate(commitDate);
})
.catch(() => {
updatedEl.innerText = "unknown";
console.warn("[GITHUB-FILE-CARD] (Error) Loading commit date for ${repo} | ${cardUuid}.");
});

Promise.allSettled([repoFetch, contentsFetch, commitsFetch]).then(() => {
cardEl.classList.remove("fetch-waiting");
console.log("[GITHUB-FILE-CARD] Loaded card for ${repo} | ${cardUuid}.");
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initGithubFileCard, { once: true });
} else {
initGithubFileCard();
}
`,
);

return h(
`a#${cardUuid}-card`,
{
class: "card-github card-github-file fetch-waiting no-styling",
href: fileUrl,
target: "_blank",
repo,
file: filePath,
ref,
},
[nTitle, nDescription, nInfoBar, nScript],
);
}
14 changes: 13 additions & 1 deletion src/styles/markdown-extend.styl
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ a.card-github
.gc-language
display: none

.gc-stars, .gc-forks, .gc-license, .github-logo
.gc-stars, .gc-forks, .gc-license, .gc-filetype, .gc-filesize, .gc-updated, .github-logo
font-weight: 500
font-size: 0.875rem
opacity: 0.9;
Expand Down Expand Up @@ -198,6 +198,18 @@ a.card-github
&:before
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16'%3E%3Cpath d='M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z'%3E%3C/path%3E%3C/svg%3E")

.gc-filetype
&:before
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16'%3E%3Cpath d='M4 0h5.5L14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2Zm5 1.5V5h3.5L9 1.5Z'%3E%3C/path%3E%3C/svg%3E")

.gc-filesize
&:before
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M448 80v48c0 44.2-100.3 80-224 80S0 172.2 0 128V80C0 35.8 100.3 0 224 0s224 35.8 224 80m-54.8 134.7c20.8-7.4 39.9-16.9 54.8-28.6V288c0 44.2-100.3 80-224 80S0 332.2 0 288V186.1c14.9 11.8 34 21.2 54.8 28.6C99.7 230.7 159.5 240 224 240s124.3-9.3 169.2-25.3M0 346.1c14.9 11.8 34 21.2 54.8 28.6C99.7 390.7 159.5 400 224 400s124.3-9.3 169.2-25.3c20.8-7.4 39.9-16.9 54.8-28.6V432c0 44.2-100.3 80-224 80S0 476.2 0 432z'/%3E%3C/svg%3E")

.gc-updated
&:before
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16'%3E%3Cpath d='M8 1.5a6.5 6.5 0 1 0 0 13a6.5 6.5 0 0 0 0-13Zm0 1.5a5 5 0 1 1 0 10a5 5 0 0 1 0-10Zm.75 1.75a.75.75 0 0 0-1.5 0V8c0 .27.15.52.39.65l2.5 1.5a.75.75 0 1 0 .77-1.3L8.75 7.55V4.75Z'%3E%3C/path%3E%3C/svg%3E")

.github-logo
font-size: 1.25rem

Expand Down