This commit is contained in:
Mrrp 2025-01-03 21:50:12 -08:00
parent 328a6bd178
commit 1dbf1e4074
21 changed files with 1046 additions and 545 deletions

View file

@ -34,10 +34,14 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Activate Venv - name: Activate Venv
run: source venv/bin/activate run: source venv/bin/activate
- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Install dependencies - name: Install dependencies
run: pip install python-frontmatter run: pip install python-frontmatter
- name: Update Page manifest - name: Update Page list
run: python utils/pageupdater/page_list_gen.py run: deno utils/pageupdater/update_pagelist.ts
- name: Update Page history - name: Update Page history
run: python utils/pageupdater/commit_post_history.py run: python utils/pageupdater/commit_post_history.py
- name: Generate RSS feed - name: Generate RSS feed

View file

@ -3,7 +3,8 @@ import { main } from "@popperjs/core";
import MainPage from "./pages/index.vue" import MainPage from "./pages/index.vue"
import backgroundCalm from "./components/BackgroundCalm.vue"; import backgroundCalm from "./components/BackgroundCalm.vue";
import Navbar from "./components/Navbar.vue" import Navbar from "./components/Navbar.vue"
import './assets/style.css' import './assets/style/style.css'
import siteConfig from '~/assets/config'
</script> </script>
<template> <template>

View file

@ -6,7 +6,7 @@ export default {
siteImage: '', siteImage: '',
// Site personalization // Site personalization
sitePrimaryColor: '#550077', siteColor: '#550077',
// Author information // Author information
siteAuthor: 'TheFelidae', siteAuthor: 'TheFelidae',

View file

@ -1,16 +1,212 @@
// TypeScript utilities for rendering Markdown/HTML content
import hljs from "highlight.js"; import hljs from "highlight.js";
import MarkdownIt from "markdown-it"; import MarkdownIt from "markdown-it";
import Token from "markdown-it/lib/token.mjs";
import { alert } from "@mdit/plugin-alert"; import { alert } from "@mdit/plugin-alert";
import { figure } from "@mdit/plugin-figure";
import TeXToSVG from "tex-to-svg";
import { tab } from "@mdit/plugin-tab"; import { tab } from "@mdit/plugin-tab";
import { tasklist } from "@mdit/plugin-tasklist"; import { tasklist } from "@mdit/plugin-tasklist";
import { mark } from "@mdit/plugin-mark"; import { mark } from "@mdit/plugin-mark";
import { footnote } from "@mdit/plugin-footnote"; import { footnote } from "@mdit/plugin-footnote";
import { align } from "@mdit/plugin-align"; import fm, { type FrontMatterResult } from "front-matter";
export default function configured_markdown(): MarkdownIt { export interface MarkdownMetadata {
title: string;
description: string;
date: string;
tags: string[];
background: string;
next?: string;
previous?: string;
}
export interface MarkdownOutput {
metadata: MarkdownMetadata;
contents: string;
}
function isolate_markdown(input: string): MarkdownOutput {
const front_matter: FrontMatterResult<any> = fm(input);
const content = front_matter.body;
const metadata: MarkdownMetadata = {
title: front_matter.attributes.title || "",
description: front_matter.attributes.description || "",
date: front_matter.attributes.date || "",
tags: front_matter.attributes.tags || [],
background: front_matter.attributes.background || "",
next: front_matter.attributes.next || "",
previous: front_matter.attributes.previous || ""
};
return {
metadata: metadata,
contents: content
};
}
function isolate_html(input: string): MarkdownOutput {
const title = input.match(/<title>([^<]+)<\/title>/);
const meta = input.match(/<meta name="([^"]+)" content="([^"]+)">/g);
const metadata: MarkdownMetadata = {
title: "",
description: "",
date: "",
tags: [],
background: ""
};
if (meta) {
for (const tag of meta) {
const match = tag.match(/<meta name="([^"]+)" content="([^"]+)">/);
if (match) {
switch (match[1]) {
case "title":
metadata.title = match[2];
break;
case "description":
metadata.description = match[2];
break;
case "date":
metadata.date = match[2];
break;
case "tags":
metadata.tags = match[2].split(",");
break;
case "background":
metadata.background = match[2];
break;
}
}
}
}
const front_matter: FrontMatterResult<any> = fm(input);
if (front_matter.attributes) {
metadata.title = front_matter.attributes.title || metadata.title;
metadata.description = front_matter.attributes.description || metadata.description;
metadata.date = front_matter.attributes.date || metadata.date;
metadata.tags = front_matter.attributes.tags || metadata.tags;
metadata.background = front_matter.attributes.background || metadata.background;
metadata.next = front_matter.attributes.next || "";
metadata.previous = front_matter.attributes.previous || "";
}
return {
metadata: metadata,
contents: input
};
}
export class MarkdownInput {
type: "markdown" | "html";
private contents: string;
private metadata: MarkdownMetadata;
constructor(type: "markdown" | "html", data: string) {
this.type = type;
this.metadata = this.get_metadata();
switch (this.type) {
case "markdown":
const result = isolate_markdown(data);
this.contents = result.contents;
this.metadata = result.metadata;
break;
case "html":
const result_html = isolate_html(data);
this.contents = data;
this.metadata = result_html.metadata;
break;
}
}
static from_markdown(data: string): MarkdownInput {
return new MarkdownInput("markdown", data);
}
static from_html(data: string): MarkdownInput {
return new MarkdownInput("html", data);
}
static async from_url(url: string): Promise<MarkdownInput> {
const response = await fetch(url);
const data = await response.text();
// Check the content type of the response
const content_type = response.headers.get("content-type");
if (content_type) {
if (content_type.includes("text/markdown")) {
return MarkdownInput.from_markdown(data);
} else if (content_type.includes("text/html")) {
return MarkdownInput.from_html(data);
}
}
if (url.endsWith(".md")) {
return MarkdownInput.from_markdown(data);
} else if (url.endsWith(".html")) {
return MarkdownInput.from_html(data);
}
// Fallback to markdown
return MarkdownInput.from_markdown(data);
}
public get_contents(): string {
return this.contents;
}
public get_metadata(): MarkdownMetadata {
return this.metadata;
}
}
export class MarkdownContext {
private md: MarkdownIt;
constructor(md: MarkdownIt | undefined) {
if (md) {
this.md = md;
} else {
this.md = configured_markdown();
}
}
private render_markdown(input: MarkdownInput): MarkdownOutput {
console.log("Rendering markdown")
const content = configured_markdown().render(input.get_contents());
const result: MarkdownOutput = {
metadata: input.get_metadata(),
contents: content
};
return result;
}
private render_html(inputs: MarkdownInput): MarkdownOutput {
const result: MarkdownOutput = {
metadata: inputs.get_metadata(),
contents: inputs.get_contents()
};
return result;
}
render(input: MarkdownInput): MarkdownOutput {
switch (input.type) {
case "markdown":
return this.render_markdown(input);
case "html":
return this.render_html(input);
}
}
}
function configured_markdown(): MarkdownIt {
var md: MarkdownIt = MarkdownIt({ var md: MarkdownIt = MarkdownIt({
breaks: true, breaks: true,
typographer: true, typographer: true,
@ -69,3 +265,11 @@ export default function configured_markdown(): MarkdownIt {
return md; return md;
} }
export var globalMarkdown = new MarkdownContext(undefined);
export default {
MarkdownInput,
MarkdownContext,
globalMarkdown
}

View file

@ -1,11 +1,11 @@
{ {
"last_generated": "2025-01-04 02:30:21", "last_generated": "2025-01-03 20:00:53",
"posts": [ "posts": [
{ {
"metadata": { "metadata": {
"title": "3DS Programming - Using RomFS", "title": "3DS Programming - Using RomFS",
"description": "A guide to using RomFS on the 3DS. (Old)", "description": "A guide to using RomFS on the 3DS. (Old)",
"date": "2025-01-01", "date": "2025-01-01T00:00:00.000Z",
"tags": [ "tags": [
"3ds", "3ds",
"programming", "programming",
@ -20,25 +20,11 @@
"url": "/blog/old3ds_romfs.md", "url": "/blog/old3ds_romfs.md",
"hash": "0b28a366868e9fa564b6a33d9b1aa1d8269f7971497f25488f05f54929e88410" "hash": "0b28a366868e9fa564b6a33d9b1aa1d8269f7971497f25488f05f54929e88410"
}, },
{
"metadata": {
"title": "Styling Test",
"description": "A test post to see how the site styling looks",
"date": "2025-01-01",
"tags": [
"meta",
"web"
]
},
"id": "/styling_test.md",
"url": "/blog/styling_test.md",
"hash": "8e6c14fdef5fd67ea17dcc8b58d59f2040d8250c15c2d18d3e1fdc1b1b60dc54"
},
{ {
"metadata": { "metadata": {
"title": "Awesome", "title": "Awesome",
"description": "A curated list of awesome stuff I like", "description": "A curated list of awesome stuff I like",
"date": "2024-11-26", "date": "2024-11-26T00:00:00.000Z",
"tags": [ "tags": [
"awesome", "awesome",
"curated" "curated"
@ -50,41 +36,24 @@
}, },
{ {
"metadata": { "metadata": {
"title": "LGBTQ+ Resources", "title": "Badges!",
"description": "A list of resources for LGBTQ+ individuals", "description": "A collection of 88x31 badges for various things",
"date": "2025-01-02", "date": "2024-12-21T00:00:00.000Z",
"tags": [ "tags": [
"lgbtq+", "badges",
"resources" "retro",
"web"
] ]
}, },
"id": "/lgbtq_resources.md", "id": "/badges.md",
"url": "/blog/lgbtq_resources.md", "url": "/blog/badges.md",
"hash": "1dd63f74b3f74077510f1043cb471ab9c691711545bb6ef1f3d6eff4f6a23c89" "hash": "338ccfecc6523dff93708330a8b43af715f1e80d55e1cc3bea2d1a7306fc4f00"
},
{
"metadata": {
"title": "3DS Programming - Hello World",
"description": "A guide to creating a simple Hello, World program for the 3DS. (Old)",
"date": "2025-01-01",
"tags": [
"3ds",
"programming",
"c",
"devkitpro",
"old"
],
"next": "old3ds_romfs.md"
},
"id": "/old3ds_helloworld.md",
"url": "/blog/old3ds_helloworld.md",
"hash": "86e0bd1deae0d00b17ab0960634ea7292d6387063f70600cec4001564fde9514"
}, },
{ {
"metadata": { "metadata": {
"title": "3DS Programming - Touchscreen Input", "title": "3DS Programming - Touchscreen Input",
"description": "A guide to using the touchscreen on the 3DS. (Old)", "description": "A guide to using the touchscreen on the 3DS. (Old)",
"date": "2025-01-01", "date": "2025-01-01T00:00:00.000Z",
"tags": [ "tags": [
"3ds", "3ds",
"programming", "programming",
@ -100,18 +69,49 @@
}, },
{ {
"metadata": { "metadata": {
"title": "Badges!", "title": "Styling Test",
"description": "A collection of 88x31 badges for various things", "description": "A test post to see how the site styling looks",
"date": "2024-12-21", "date": "2025-01-01T00:00:00.000Z",
"tags": [ "tags": [
"badges", "meta",
"retro",
"web" "web"
] ]
}, },
"id": "/badges.md", "id": "/styling_test.md",
"url": "/blog/badges.md", "url": "/blog/styling_test.md",
"hash": "338ccfecc6523dff93708330a8b43af715f1e80d55e1cc3bea2d1a7306fc4f00" "hash": "8e6c14fdef5fd67ea17dcc8b58d59f2040d8250c15c2d18d3e1fdc1b1b60dc54"
},
{
"metadata": {
"title": "LGBTQ+ Resources",
"description": "A list of resources for LGBTQ+ individuals",
"date": "2025-01-02T00:00:00.000Z",
"tags": [
"lgbtq+",
"resources"
]
},
"id": "/lgbtq_resources.md",
"url": "/blog/lgbtq_resources.md",
"hash": "1dd63f74b3f74077510f1043cb471ab9c691711545bb6ef1f3d6eff4f6a23c89"
},
{
"metadata": {
"title": "3DS Programming - Hello World",
"description": "A guide to creating a simple Hello, World program for the 3DS. (Old)",
"date": "2025-01-01T00:00:00.000Z",
"tags": [
"3ds",
"programming",
"c",
"devkitpro",
"old"
],
"next": "old3ds_romfs.md"
},
"id": "/old3ds_helloworld.md",
"url": "/blog/old3ds_helloworld.md",
"hash": "86e0bd1deae0d00b17ab0960634ea7292d6387063f70600cec4001564fde9514"
} }
] ]
} }

250
assets/style/markdown.scss Normal file
View file

@ -0,0 +1,250 @@
/* Note: Use `md-override` to override the default markdown styling */
/* Headers (1-3) get a small visual upgrade */
.md-contents h1:not(.md-override), .md-contents h2:not(.md-override), .md-contents h3:not(.md-override) {
@apply border-highlight border-b-2;
padding-bottom: 0.25rem;
margin-bottom: 0.5rem;
margin-top: 1rem;
}
/* Markdown alerts get a box, a shadow, and a background+border color */
.md-contents .markdown-alert:not(.md-override) {
@apply rounded-lg border-highlight border-b-2 shadow-primary shadow-md;
background-color: #333;
padding: 0.5rem;
margin: 1rem 0;
}
.md-contents .markdown-alert-title {
font-weight: bold;
margin-bottom: 0.5rem;
}
.md-contents .markdown-alert-warning:not(.md-override) {
@apply border-2 border-warning shadow-warning shadow-md;
}
.md-contents .markdown-alert-warning .markdown-alert-title:not(.md-override):before {
content: "⚠️ ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-danger:not(.md-override) {
@apply border-2 border-danger shadow-danger shadow-md;
}
.md-contents .markdown-alert-danger .markdown-alert-title:not(.md-override):before {
content: "";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-success:not(.md-override) {
@apply border-2 border-success shadow-success shadow-md;
}
.md-contents .markdown-alert-success .markdown-alert-title:not(.md-override):before {
content: "";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-info:not(.md-override) {
@apply border-2 border-info shadow-info shadow-md;
}
.md-contents .markdown-alert-info .markdown-alert-title:not(.md-override):before {
content: " ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-important:not(.md-override) {
@apply border-2 border-important shadow-important shadow-md;
}
.md-contents .markdown-alert-important .markdown-alert-title:not(.md-override):before {
content: "";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-caution:not(.md-override) {
@apply border-2 border-warning shadow-warning shadow-md;
}
.md-contents .markdown-alert-caution .markdown-alert-title:not(.md-override):before {
content: "⚠️ ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-note:not(.md-override) {
@apply border-2 border-note shadow-note shadow-md;
}
.md-contents .markdown-alert-note .markdown-alert-title:not(.md-override):before {
content: "📝 ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-tip:not(.md-override) {
@apply border-2 border-tip shadow-tip shadow-md;
}
.md-contents .markdown-alert-tip .markdown-alert-title:not(.md-override):before {
content: "💡 ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-question:not(.md-override) {
@apply border-2 border-note shadow-note shadow-md;
}
.md-contents .markdown-alert-question .markdown-alert-title:not(.md-override):before {
content: "";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-quote:not(.md-override) {
@apply border-2 border-note shadow-note shadow-md border-dashed;
}
.md-contents .markdown-alert-quote .markdown-alert-title:not(.md-override):before {
content: "";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-deprecated:not(.md-override) {
@apply border-2 border-danger shadow-danger shadow-md;
}
.md-contents .markdown-alert-deprecated .markdown-alert-title:not(.md-override):before {
content: "🚫 ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-example:not(.md-override) {
@apply border-2 border-info shadow-info shadow-md;
}
.md-contents .markdown-alert-example .markdown-alert-title:not(.md-override):before {
content: "💡 ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-todo:not(.md-override) {
@apply border-2 border-warning shadow-warning shadow-md;
}
.md-contents .markdown-alert-todo .markdown-alert-title:not(.md-override):before {
content: "📝 ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-done:not(.md-override) {
@apply border-2 border-success shadow-success shadow-md;
}
.md-contents .markdown-alert-done .markdown-alert-title:not(.md-override):before {
content: "";
text-shadow: 1px 1px 1px #000;
}
/* Footnotes, ensure they are by default white with no transition if they have no href */
.md-contents .footnote-anchor:not([href]) {
color: white;
transition: none;
}
/* Apply margin to code blocks */
.md-contents pre:not(.md-override) {
margin: 1rem 0;
}
/* Apply styling to lists and list items with nesting */
.md-contents ul:not(.md-override), .md-contents ol:not(.md-override) {
margin: 1rem 0;
padding-left: 1rem;
}
/* Apply different bullet types to nested lists as they are nested */
.md-contents ul:not(.md-override) ul:not(.md-override), .md-contents ol:not(.md-override) ul:not(.md-override) {
list-style-type: circle;
}
/* Apply table styling, with a pleasant glowing border effect */
.md-contents table:not(.md-override) {
border-collapse: collapse;
width: 100%;
border: 1px solid #333;
box-shadow: 0 0 7px #A020F0
}
.md-contents th:not(.md-override), .md-contents td:not(.md-override) {
border: 1px solid #A020F0;
padding: 0.5rem;
}
.md-contents th:not(.md-override) {
background-color: #333;
color: white;
}
Hosted
.md-contents button:not(.md-override) {
background-color: #333;
color: white;
border: 1px solid #A020F0;
padding: 0.5rem;
border-radius: 0.25rem;
box-shadow: 0 0 7px #A020F0;
}
.md-contents button:hover:not(.md-override) {
background-color: #555;
box-shadow: 0 0 10px #A020F0;
color: white;
}
/* transition for button hover effect */
.md-contents button:not(.md-override) {
transition: background-color 0.5s;
}
/* iframes get a border and a shadow */
.md-contents iframe:not(.md-override) {
@apply border-2 border-purple-600 rounded-lg;
box-shadow: 0 0 7px #A020F0;
}
/* Identify if the iframe links to a YouTube video - If so, red border */
.md-contents iframe[src*="youtube.com"]:not(.md-override) {
@apply border-2 border-red-600;
box-shadow: 0 0 7px red;
}
/* Identify if the iframe links to a Twitch video - If so, purple border */
.md-contents iframe[src*="twitch.tv"]:not(.md-override) {
@apply border-2 border-purple-600;
box-shadow: 0 0 7px #A020F0;
}
/* Identify if the iframe links to a Spotify playlist - If so, green border */
.md-contents iframe[src*="spotify.com"]:not(.md-override) {
@apply border-2 border-green-600 rounded-xl;
box-shadow: 0 0 7px green;
}
/* Identify if the iframe links to a SoundCloud track - If so, orange border */
.md-contents iframe[src*="soundcloud.com"]:not(.md-override) {
@apply border-2 border-orange-600;
box-shadow: 0 0 7px orange;
}
/* Identify if the iframe links to a Bandcamp track - If so, blue border */
.md-contents iframe[src*="bandcamp.com"]:not(.md-override) {
@apply border-2 border-blue-600;
box-shadow: 0 0 7px blue;
}
/* Identify if the iframe links to a GitHub Gist - If so, gray border */
.md-contents iframe[src*="gist.github.com"]:not(.md-override) {
@apply border-2 border-gray-600;
box-shadow: 0 0 7px gray;
}
/* And if it links to Wikipedia, the free online encyclopedia, give it a white border */
.md-contents iframe[src*="wikipedia.org"]:not(.md-override) {
@apply border-2 border-white;
box-shadow: 0 0 7px white;
}
/* If the iframe links to a Google Maps location */
.md-contents iframe[src*="google.com/maps"]:not(.md-override) {
border-left: 2px solid #4285F4;
border-right: 2px solid #0F9D58;
border-top: 2px solid #F4B400;
border-bottom: 2px solid #DB4437;
box-shadow: 0 0 7px #4285F4;
}
/* Redo the audio element styling */
.md-contents audio:not(.md-override) {
@apply border-2 border-purple-600 rounded-lg;
box-shadow: 0 0 7px #A020F0;
}
/* Images get a border and a shadow */
.md-contents img:not(.md-override) {
@apply border-2 border-image shadow-image shadow-sm m-1;
}

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

View file

@ -1,11 +1,3 @@
<script setup lang="ts">
import { Engine } from '@tsparticles/engine';
const onLoad = (container: Container) => {
}
</script>
<template> <template>
<div> <div>
<NuxtParticles <NuxtParticles

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="m-1 p-5 rounded-lg container bg-opacity-90 bg-slate-900 border-purple-600 border-2"> <div class="m-1 p-5 rounded-lg container bg-opacity-90 bg-slate-900 border-primary border-2">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
@ -8,9 +8,8 @@
/* Glow effect on border, plus a slight response to hover by changing the border color near the cursor */ /* Glow effect on border, plus a slight response to hover by changing the border color near the cursor */
.container { .container {
@apply border-primary border-2 shadow-primary shadow-md;
transition: border-color 0.5s; transition: border-color 0.5s;
border-color: #A020F0;
box-shadow: 0 0 10px #A020F0;
} }
</style> </style>

View file

@ -1,306 +1,63 @@
<script setup lang="ts"> <script setup lang="ts">
import configured_markdown from '~/assets/markdown_conf'; import { globalMarkdown, MarkdownInput } from '~/assets/markdown_conf';
import { useSlots } from 'vue'; import { useSlots } from 'vue';
import fm, { type FrontMatterResult } from 'front-matter';
import '~/assets/style/markdown.scss';
const text = ref(""); // External inputs (reactive)
const loading = ref(true); const input = defineProps({
input: {
const props = defineProps({ type: String,
text: String, required: true
},
type: {
type: String,
default: "markdown",
validator: (value: string) => ["markdown", "html"].includes(value)
}
}); });
function render_markdown(data: string | undefined) { // Internal state
// Validate that the data is a string const contents = ref("");
if (typeof data !== 'string') { const loading = ref(false);
loading.value = false; const error = ref("");
const emit = defineEmits(["metadata"]);
function updateContents(input: string | undefined, type: "markdown" | "html" | undefined) {
if (input === "" || input === undefined) {
contents.value = "";
error.value = "";
return; return;
} }
text.value = configured_markdown().render(data); const markdown_input: MarkdownInput = type ?
loading.value = false type === "markdown" ?
MarkdownInput.from_markdown(input) : type == "html" ?
MarkdownInput.from_html(input) :
MarkdownInput.from_markdown(input) : MarkdownInput.from_markdown(input);
console.log("Rendering...")
const render = globalMarkdown.render(markdown_input);
console.log("Metadata: ", render.metadata);
contents.value = render.contents;
emit("metadata", render.metadata);
} }
watch(() => props.text, (newVal) => { watch(() => input.input, (newVal) => {
render_markdown(newVal); updateContents(newVal, input.type as "markdown" | "html");
}, { }, { immediate: true });
immediate: true
});
</script> </script>
<template> <template>
<div v-if="loading" class="text-center animate-pulse"> <div v-if="loading" class="text-center animate-pulse">
<h2>Loading...</h2> <h2>Loading...</h2>
</div> </div>
<div class="md-contents" v-html="text"></div> <div v-else-if="error" class="text-center">
<h2>Error: {{ error }}</h2>
</div>
<div v-else class="md-contents">
<div class="md-contents" v-html="contents"></div>
</div>
</template> </template>
<style>
/* Note: Use `md-override` to override the default markdown styling */
/* Headers (1-3) get a small visual upgrade */
.md-contents h1:not(.md-override), .md-contents h2:not(.md-override), .md-contents h3:not(.md-override) {
border-bottom: 1px solid #A020F0;
padding-bottom: 0.25rem;
margin-bottom: 0.5rem;
margin-top: 1rem;
}
/* Markdown alerts get a box, a shadow, and a background+border color */
.md-contents .markdown-alert:not(.md-override) {
@apply rounded-lg;
border: 1px solid #A020F0;
background-color: #333;
box-shadow: 0 0 7px #A020F0;
padding: 0.5rem;
margin: 1rem 0;
}
.md-contents .markdown-alert-title {
font-weight: bold;
margin-bottom: 0.5rem;
}
.md-contents .markdown-alert-warning:not(.md-override) {
border: 1px solid #FFA500;
background-color: #aa6600;
box-shadow: 0 0 7px #FFA500;
}
.md-contents .markdown-alert-warning .markdown-alert-title:not(.md-override):before {
content: "⚠️ ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-danger:not(.md-override) {
border: 1px solid #FF0000;
background-color: #aa0000;
box-shadow: 0 0 7px #FF0000;
}
.md-contents .markdown-alert-danger .markdown-alert-title:not(.md-override):before {
content: "❌ ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-success:not(.md-override) {
border: 1px solid #00FF00;
box-shadow: 0 0 7px #00FF00;
}
.md-contents .markdown-alert-success .markdown-alert-title:not(.md-override):before {
content: "✅ ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-info:not(.md-override) {
border: 1px solid #00FFFF;
box-shadow: 0 0 7px #00FFFF;
}
.md-contents .markdown-alert-info .markdown-alert-title:not(.md-override):before {
content: " ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-important:not(.md-override) {
border: 1px solid #FF00FF;
box-shadow: 0 0 7px #FF00FF;
}
.md-contents .markdown-alert-important .markdown-alert-title:not(.md-override):before {
content: "❗ ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-caution:not(.md-override) {
border: 1px solid #FFFF00;
box-shadow: 0 0 7px #FFFF00;
}
.md-contents .markdown-alert-caution .markdown-alert-title:not(.md-override):before {
content: "⚠️ ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-note:not(.md-override) {
border: 1px solid #0000FF;
box-shadow: 0 0 7px #0000FF;
}
.md-contents .markdown-alert-note .markdown-alert-title:not(.md-override):before {
content: "📝 ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-tip:not(.md-override) {
border: 1px solid #00FF00;
box-shadow: 0 0 7px #00FF00;
}
.md-contents .markdown-alert-tip .markdown-alert-title:not(.md-override):before {
content: "💡 ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-question:not(.md-override) {
border: 1px solid #ccc;
box-shadow: 0 0 7px #ccc;
}
.md-contents .markdown-alert-question .markdown-alert-title:not(.md-override):before {
content: "❓ ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-quote:not(.md-override) {
border: 1px dashed #ccc;
box-shadow: 0 0 7px #ccc;
}
.md-contents .markdown-alert-quote .markdown-alert-title:not(.md-override):before {
content: "❝ ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-deprecated:not(.md-override) {
border: 1px solid indigo;
box-shadow: 0 0 7px indigo;
}
.md-contents .markdown-alert-deprecated .markdown-alert-title:not(.md-override):before {
content: "🚫 ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-example:not(.md-override) {
border: 1px solid #ccc;
box-shadow: 0 0 7px #ccc;
}
.md-contents .markdown-alert-example .markdown-alert-title:not(.md-override):before {
content: "💡 ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-todo:not(.md-override) {
border: 1px solid skyblue;
box-shadow: 0 0 7px skyblue;
}
.md-contents .markdown-alert-todo .markdown-alert-title:not(.md-override):before {
content: "📝 ";
text-shadow: 1px 1px 1px #000;
}
.md-contents .markdown-alert-done:not(.md-override) {
border: 1px solid #00FF00;
box-shadow: 0 0 7px #00FF00;
}
.md-contents .markdown-alert-done .markdown-alert-title:not(.md-override):before {
content: "✅ ";
text-shadow: 1px 1px 1px #000;
}
/* Footnotes, ensure they are by default white with no transition if they have no href */
.md-contents .footnote-anchor:not([href]) {
color: white;
transition: none;
}
/* Apply margin to code blocks */
.md-contents pre:not(.md-override) {
margin: 1rem 0;
}
/* Apply styling to lists and list items with nesting */
.md-contents ul:not(.md-override), .md-contents ol:not(.md-override) {
margin: 1rem 0;
padding-left: 1rem;
}
/* Apply different bullet types to nested lists as they are nested */
.md-contents ul:not(.md-override) ul:not(.md-override), .md-contents ol:not(.md-override) ul:not(.md-override) {
list-style-type: circle;
}
/* Apply table styling, with a pleasant glowing border effect */
.md-contents table:not(.md-override) {
border-collapse: collapse;
width: 100%;
border: 1px solid #333;
box-shadow: 0 0 7px #A020F0
}
.md-contents th:not(.md-override), .md-contents td:not(.md-override) {
border: 1px solid #A020F0;
padding: 0.5rem;
}
.md-contents th:not(.md-override) {
background-color: #333;
color: white;
}
Hosted
.md-contents button:not(.md-override) {
background-color: #333;
color: white;
border: 1px solid #A020F0;
padding: 0.5rem;
border-radius: 0.25rem;
box-shadow: 0 0 7px #A020F0;
}
.md-contents button:hover:not(.md-override) {
background-color: #555;
box-shadow: 0 0 10px #A020F0;
color: white;
}
/* transition for button hover effect */
.md-contents button:not(.md-override) {
transition: background-color 0.5s;
}
/* iframes get a border and a shadow */
.md-contents iframe:not(.md-override) {
@apply border-2 border-purple-600 rounded-lg;
box-shadow: 0 0 7px #A020F0;
}
/* Identify if the iframe links to a YouTube video - If so, red border */
.md-contents iframe[src*="youtube.com"]:not(.md-override) {
@apply border-2 border-red-600;
box-shadow: 0 0 7px red;
}
/* Identify if the iframe links to a Twitch video - If so, purple border */
.md-contents iframe[src*="twitch.tv"]:not(.md-override) {
@apply border-2 border-purple-600;
box-shadow: 0 0 7px #A020F0;
}
/* Identify if the iframe links to a Spotify playlist - If so, green border */
.md-contents iframe[src*="spotify.com"]:not(.md-override) {
@apply border-2 border-green-600 rounded-xl;
box-shadow: 0 0 7px green;
}
/* Identify if the iframe links to a SoundCloud track - If so, orange border */
.md-contents iframe[src*="soundcloud.com"]:not(.md-override) {
@apply border-2 border-orange-600;
box-shadow: 0 0 7px orange;
}
/* Identify if the iframe links to a Bandcamp track - If so, blue border */
.md-contents iframe[src*="bandcamp.com"]:not(.md-override) {
@apply border-2 border-blue-600;
box-shadow: 0 0 7px blue;
}
/* Identify if the iframe links to a GitHub Gist - If so, gray border */
.md-contents iframe[src*="gist.github.com"]:not(.md-override) {
@apply border-2 border-gray-600;
box-shadow: 0 0 7px gray;
}
/* And if it links to Wikipedia, the free online encyclopedia, give it a white border */
.md-contents iframe[src*="wikipedia.org"]:not(.md-override) {
@apply border-2 border-white;
box-shadow: 0 0 7px white;
}
/* If the iframe links to a Google Maps location */
.md-contents iframe[src*="google.com/maps"]:not(.md-override) {
border-left: 2px solid #4285F4;
border-right: 2px solid #0F9D58;
border-top: 2px solid #F4B400;
border-bottom: 2px solid #DB4437;
box-shadow: 0 0 7px #4285F4;
}
/* Redo the audio element styling */
.md-contents audio:not(.md-override) {
@apply border-2 border-purple-600 rounded-lg;
box-shadow: 0 0 7px #A020F0;
}
/* Images get a border and a shadow */
.md-contents img:not(.md-override) {
@apply border-2 border-purple-600;
box-shadow: 0 0 7px #A020F0;
}
</style>

View file

@ -4,11 +4,26 @@ import { ref } from 'vue';
import siteConfig from '../assets/config'; import siteConfig from '../assets/config';
const props = defineProps({ const props = defineProps({
title: String, title: {
description: String, type: String,
date: String, default: 'Untitled'
tags: Array<String>, },
background: String description: {
type: String,
default: 'No description provided'
},
date: {
type: String,
default: 'No date provided'
},
tags: {
type: Array<String>,
default: []
},
background: {
type: String,
default: ''
}
}); });
const tags: Ref<String[]> = ref(props.tags || []); const tags: Ref<String[]> = ref(props.tags || []);
@ -39,7 +54,7 @@ useSeoMeta({
ogSiteName: siteConfig.siteTitle, ogSiteName: siteConfig.siteTitle,
ogLocale: 'en_US', ogLocale: 'en_US',
ogLocaleAlternate: 'en_GB', ogLocaleAlternate: 'en_GB',
themeColor: siteConfig.sitePrimaryColor, themeColor: siteConfig.siteColor,
twitterCard: 'summary', twitterCard: 'summary',
twitterTitle: fullTitle, twitterTitle: fullTitle,
twitterDescription: description, twitterDescription: description,
@ -54,7 +69,7 @@ useHead({
{ name: 'keywords', content: tagsToString(tags.value) }, { name: 'keywords', content: tagsToString(tags.value) },
{ name: 'author', content: siteConfig.siteAuthor }, { name: 'author', content: siteConfig.siteAuthor },
{ name: 'date', content: date }, { name: 'date', content: date },
{ name: 'theme-color', content: siteConfig.sitePrimaryColor }, { name: 'theme-color', content: siteConfig.siteColor },
{ name: 'twitter:card', content: 'summary' }, { name: 'twitter:card', content: 'summary' },
{ name: 'twitter:title', content: fullTitle }, { name: 'twitter:title', content: fullTitle },
{ name: 'twitter:description', content: description }, { name: 'twitter:description', content: description },
@ -72,4 +87,31 @@ useHead({
] ]
}) })
watch(() => props.title, (newVal) => {
title.value = newVal;
fullTitle.value = title.value + ' | ' + siteConfig.siteTitle;
});
watch(() => props.description, (newVal) => {
description.value = newVal;
});
watch(() => props.date, (newVal) => {
date.value = newVal;
});
watch(() => props.tags, (newVal) => {
tags.value = newVal;
});
watch(() => props.background, (newVal) => {
background.value = newVal;
});
</script> </script>
<template>
<div>
<slot></slot>
</div>
</template>

130
deno.lock generated
View file

@ -16,6 +16,7 @@
"npm:@types/markdown-it@^14.1.2": "14.1.2", "npm:@types/markdown-it@^14.1.2": "14.1.2",
"npm:autoprefixer@^10.4.20": "10.4.20_postcss@8.4.49", "npm:autoprefixer@^10.4.20": "10.4.20_postcss@8.4.49",
"npm:css-loader@^7.1.2": "7.1.2_postcss@8.4.49", "npm:css-loader@^7.1.2": "7.1.2_postcss@8.4.49",
"npm:date-fns@4.1.0": "4.1.0",
"npm:defu@^6.1.4": "6.1.4", "npm:defu@^6.1.4": "6.1.4",
"npm:front-matter@^4.0.2": "4.0.2", "npm:front-matter@^4.0.2": "4.0.2",
"npm:highlight.js@^11.10.0": "11.11.0", "npm:highlight.js@^11.10.0": "11.11.0",
@ -27,6 +28,7 @@
"npm:ofetch@^1.4.1": "1.4.1", "npm:ofetch@^1.4.1": "1.4.1",
"npm:postcss-loader@^8.1.1": "8.1.1_postcss@8.4.49", "npm:postcss-loader@^8.1.1": "8.1.1_postcss@8.4.49",
"npm:postcss@^8.4.49": "8.4.49", "npm:postcss@^8.4.49": "8.4.49",
"npm:sass-embedded@^1.83.1": "1.83.1",
"npm:tailwindcss@^3.4.17": "3.4.17_postcss@8.4.49", "npm:tailwindcss@^3.4.17": "3.4.17_postcss@8.4.49",
"npm:tex-to-svg@0.2": "0.2.0", "npm:tex-to-svg@0.2": "0.2.0",
"npm:texsvg@^2.2.2": "2.2.2", "npm:texsvg@^2.2.2": "2.2.2",
@ -281,6 +283,9 @@
"@babel/helper-validator-identifier" "@babel/helper-validator-identifier"
] ]
}, },
"@bufbuild/protobuf@2.2.3": {
"integrity": "sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg=="
},
"@cloudflare/kv-asset-handler@0.3.4": { "@cloudflare/kv-asset-handler@0.3.4": {
"integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==",
"dependencies": [ "dependencies": [
@ -2101,6 +2106,9 @@
"update-browserslist-db" "update-browserslist-db"
] ]
}, },
"buffer-builder@0.2.0": {
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg=="
},
"buffer-crc32@1.0.0": { "buffer-crc32@1.0.0": {
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==" "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="
}, },
@ -2297,6 +2305,9 @@
"colorette@1.4.0": { "colorette@1.4.0": {
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="
}, },
"colorjs.io@0.5.2": {
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="
},
"commander@2.20.3": { "commander@2.20.3": {
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
}, },
@ -2562,6 +2573,9 @@
"csstype@3.1.3": { "csstype@3.1.3": {
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
}, },
"date-fns@4.1.0": {
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
},
"db0@0.2.1": { "db0@0.2.1": {
"integrity": "sha512-BWSFmLaCkfyqbSEZBQINMVNjCVfrogi7GQ2RSy1tmtfK9OXlsup6lUMwLsqSD7FbAjD04eWFdXowSHHUp6SE/Q==" "integrity": "sha512-BWSFmLaCkfyqbSEZBQINMVNjCVfrogi7GQ2RSy1tmtfK9OXlsup6lUMwLsqSD7FbAjD04eWFdXowSHHUp6SE/Q=="
}, },
@ -3367,6 +3381,9 @@
"image-meta@0.2.1": { "image-meta@0.2.1": {
"integrity": "sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw==" "integrity": "sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw=="
}, },
"immutable@5.0.3": {
"integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw=="
},
"import-fresh@3.3.0": { "import-fresh@3.3.0": {
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dependencies": [ "dependencies": [
@ -5071,12 +5088,111 @@
"queue-microtask" "queue-microtask"
] ]
}, },
"rxjs@7.8.1": {
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dependencies": [
"tslib"
]
},
"safe-buffer@5.1.2": { "safe-buffer@5.1.2": {
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}, },
"safe-buffer@5.2.1": { "safe-buffer@5.2.1": {
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}, },
"sass-embedded-android-arm64@1.83.1": {
"integrity": "sha512-S63rlLPGCA9FCqYYOobDJrwcuBX0zbSOl7y0jT9DlfqeqNOkC6NIT1id6RpMFCs3uhd4gbBS2E/5WPv5J5qwbw=="
},
"sass-embedded-android-arm@1.83.1": {
"integrity": "sha512-FKfrmwDG84L5cfn8fmIew47qnCFFUdcoOTCzOw8ROItkRhLLH0hnIm6gEpG5T6OFf6kxzUxvE9D0FvYQUznZrw=="
},
"sass-embedded-android-ia32@1.83.1": {
"integrity": "sha512-AGlY2vFLJhF2hN0qOz12f4eDs6x0b5BUapOpgfRrqQLHIfJhxkvi39bInsiBgQ57U0jb4I7AaS2e2e+sj7+Rqw=="
},
"sass-embedded-android-riscv64@1.83.1": {
"integrity": "sha512-OyU4AnfAUVd/wBaT60XvHidmQdaEsVUnxvI71oyPM/id1v97aWTZX3SmGkwGb7uA/q6Soo2uNalgvOSNJn7PwA=="
},
"sass-embedded-android-x64@1.83.1": {
"integrity": "sha512-NY5rwffhF4TnhXVErZnfFIjHqU3MNoWxCuSHumRN3dDI8hp8+IF59W5+Qw9AARlTXvyb+D0u5653aLSea5F40w=="
},
"sass-embedded-darwin-arm64@1.83.1": {
"integrity": "sha512-w1SBcSkIgIWgUfB7IKcPoTbSwnS3Kag5PVv3e3xfW6ZCsDweYZLQntUd2WGgaoekdm1uIbVuvPxnDH2t880iGQ=="
},
"sass-embedded-darwin-x64@1.83.1": {
"integrity": "sha512-RWrmLtUhEP5kvcGOAFdr99/ebZ/eW9z3FAktLldvgl2k96WSTC1Zr2ctL0E+Y+H3uLahEZsshIFk6RkVIRKIsA=="
},
"sass-embedded-linux-arm64@1.83.1": {
"integrity": "sha512-HVIytzj8OO18fmBY6SVRIYErcJ+Nd9a5RNF6uArav/CqvwPLATlUV8dwqSyWQIzSsQUhDF/vFIlJIoNLKKzD3A=="
},
"sass-embedded-linux-arm@1.83.1": {
"integrity": "sha512-y7rHuRgjg2YM284rin068PsEdthPljSGb653Slut5Wba4A2IP11UNVraSl6Je2AYTuoPRjQX0g7XdsrjXlzC3g=="
},
"sass-embedded-linux-ia32@1.83.1": {
"integrity": "sha512-/pc+jHllyvfaYYLTRCoXseRc4+V3Z7IDPqsviTcfVdICAoR9mgK2RtIuIZanhm1NP/lDylDOgvj1NtjcA2dNvg=="
},
"sass-embedded-linux-musl-arm64@1.83.1": {
"integrity": "sha512-wjSIYYqdIQp3DjliSTYNFg04TVqQf/3Up/Stahol0Qf/TTjLkjHHtT2jnDaZI5GclHi2PVJqQF3wEGB8bGJMzQ=="
},
"sass-embedded-linux-musl-arm@1.83.1": {
"integrity": "sha512-sFM8GXOVoeR91j9MiwNRcFXRpTA7u4185SaGuvUjcRMb84mHvtWOJPGDvgZqbWdVClBRJp6J7+CShliWngy/og=="
},
"sass-embedded-linux-musl-ia32@1.83.1": {
"integrity": "sha512-iwhTH5gwmoGt3VH6dn4WV8N6eWvthKAvUX5XPURq7e9KEsc7QP8YNHagwaAJh7TAPopb32buyEg6oaUmzxUI+Q=="
},
"sass-embedded-linux-musl-riscv64@1.83.1": {
"integrity": "sha512-FjFNWHU1n0Q6GpK1lAHQL5WmzlPjL8DTVLkYW2A/dq8EsutAdi3GfpeyWZk9bte8kyWdmPUWG3BHlnQl22xdoA=="
},
"sass-embedded-linux-musl-x64@1.83.1": {
"integrity": "sha512-BUfYR5TIDvgGHWhxSIKwTJocXU88ECZ0BW89RJqtvr7m83fKdf5ylTFCOieU7BwcA7SORUeZzcQzVFIdPUM3BQ=="
},
"sass-embedded-linux-riscv64@1.83.1": {
"integrity": "sha512-KOBGSpMrJi8y+H+za3vAAVQImPUvQa5eUrvTbbOl+wkU7WAGhOu8xrxgmYYiz3pZVBBcfRjz4I2jBcDFKJmWSw=="
},
"sass-embedded-linux-x64@1.83.1": {
"integrity": "sha512-swUsMHKqlEU9dZQ/I5WADDaXz+QkmJS27x/Oeh+oz41YgZ0ppKd0l4Vwjn0LgOQn+rxH1zLFv6xXDycvj68F/w=="
},
"sass-embedded-win32-arm64@1.83.1": {
"integrity": "sha512-6lONEBN5TaFD5L/y68zUugryXqm4RAFuLdaOPeZQRu+7ay/AmfhtFYfE5gRssnIcIx1nlcoq7zA3UX+SN2jo1Q=="
},
"sass-embedded-win32-ia32@1.83.1": {
"integrity": "sha512-HxZDkAE9n6Gb8Rz6xd67VHuo5FkUSQ4xPb7cHKa4pE0ndwH5Oc0uEhbqjJobpgmnuTm1rQYNU2nof1sFhy2MFA=="
},
"sass-embedded-win32-x64@1.83.1": {
"integrity": "sha512-5Q0aPfUaqRek8Ee1AqTUIC0o6yQSA8QwyhCgh7upsnHG3Ltm8pkJOYjzm+UgYPJeoMNppDjdDlRGQISE7qzd4g=="
},
"sass-embedded@1.83.1": {
"integrity": "sha512-LdKG6nxLEzpXbMUt0if12PhUNonGvy91n7IWHOZRZjvA6AWm9oVdhpO+KEXN/Sc+jjGvQeQcav9+Z8DwmII/pA==",
"dependencies": [
"@bufbuild/protobuf",
"buffer-builder",
"colorjs.io",
"immutable",
"rxjs",
"sass-embedded-android-arm",
"sass-embedded-android-arm64",
"sass-embedded-android-ia32",
"sass-embedded-android-riscv64",
"sass-embedded-android-x64",
"sass-embedded-darwin-arm64",
"sass-embedded-darwin-x64",
"sass-embedded-linux-arm",
"sass-embedded-linux-arm64",
"sass-embedded-linux-ia32",
"sass-embedded-linux-musl-arm",
"sass-embedded-linux-musl-arm64",
"sass-embedded-linux-musl-ia32",
"sass-embedded-linux-musl-riscv64",
"sass-embedded-linux-musl-x64",
"sass-embedded-linux-riscv64",
"sass-embedded-linux-x64",
"sass-embedded-win32-arm64",
"sass-embedded-win32-ia32",
"sass-embedded-win32-x64",
"supports-color@8.1.1",
"sync-child-process",
"varint"
]
},
"scheduler@0.23.2": { "scheduler@0.23.2": {
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dependencies": [ "dependencies": [
@ -5437,6 +5553,15 @@
"picocolors@1.1.1" "picocolors@1.1.1"
] ]
}, },
"sync-child-process@1.0.2": {
"integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
"dependencies": [
"sync-message-port"
]
},
"sync-message-port@1.1.3": {
"integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg=="
},
"system-architecture@0.1.0": { "system-architecture@0.1.0": {
"integrity": "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==" "integrity": "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="
}, },
@ -5824,6 +5949,9 @@
"uuid@9.0.1": { "uuid@9.0.1": {
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="
}, },
"varint@6.0.0": {
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="
},
"vite-hot-client@0.2.3_vite@5.4.11": { "vite-hot-client@0.2.3_vite@5.4.11": {
"integrity": "sha512-rOGAV7rUlUHX89fP2p2v0A2WWvV3QMX2UYq0fRqsWSvFvev4atHWqjwGoKaZT1VTKyLGk533ecu3eyd0o59CAg==", "integrity": "sha512-rOGAV7rUlUHX89fP2p2v0A2WWvV3QMX2UYq0fRqsWSvFvev4atHWqjwGoKaZT1VTKyLGk533ecu3eyd0o59CAg==",
"dependencies": [ "dependencies": [
@ -6141,6 +6269,7 @@
"npm:@types/markdown-it@^14.1.2", "npm:@types/markdown-it@^14.1.2",
"npm:autoprefixer@^10.4.20", "npm:autoprefixer@^10.4.20",
"npm:css-loader@^7.1.2", "npm:css-loader@^7.1.2",
"npm:date-fns@4.1.0",
"npm:front-matter@^4.0.2", "npm:front-matter@^4.0.2",
"npm:highlight.js@^11.10.0", "npm:highlight.js@^11.10.0",
"npm:markdown-it-checkbox@^1.1.0", "npm:markdown-it-checkbox@^1.1.0",
@ -6151,6 +6280,7 @@
"npm:ofetch@^1.4.1", "npm:ofetch@^1.4.1",
"npm:postcss-loader@^8.1.1", "npm:postcss-loader@^8.1.1",
"npm:postcss@^8.4.49", "npm:postcss@^8.4.49",
"npm:sass-embedded@^1.83.1",
"npm:tailwindcss@^3.4.17", "npm:tailwindcss@^3.4.17",
"npm:tex-to-svg@0.2", "npm:tex-to-svg@0.2",
"npm:texsvg@^2.2.2", "npm:texsvg@^2.2.2",

View file

@ -27,6 +27,7 @@
"nuxt": "^3.14.1592", "nuxt": "^3.14.1592",
"ofetch": "^1.4.1", "ofetch": "^1.4.1",
"postcss-loader": "^8.1.1", "postcss-loader": "^8.1.1",
"sass-embedded": "^1.83.1",
"tex-to-svg": "^0.2.0", "tex-to-svg": "^0.2.0",
"texsvg": "^2.2.2", "texsvg": "^2.2.2",
"tsparticles": "^3.7.1", "tsparticles": "^3.7.1",
@ -37,7 +38,8 @@
"@tsparticles/vue3": "^3.0.1", "@tsparticles/vue3": "^3.0.1",
"front-matter": "^4.0.2", "front-matter": "^4.0.2",
"highlight.js": "^11.10.0", "highlight.js": "^11.10.0",
"markdown-it": "^14.1.0" "markdown-it": "^14.1.0",
"date-fns": "4.1.0"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",

View file

@ -1,121 +1,61 @@
<script setup> <script setup lang="ts">
import { onMounted, watch, ref } from 'vue'; import { onMounted, watch, ref } from 'vue';
import fm from 'front-matter'; import fm from 'front-matter';
import PostCard from '../components/PostCard.vue'; import PostCard from '../components/PostCard.vue';
import { globalMarkdown, MarkdownInput, type MarkdownMetadata } from '~/assets/markdown_conf';
import * as pages from '~/utils/pageupdater/update_pagelist';
import type { PageInfo, PageInfoMetdata } from '~/utils/pageupdater/pages';
// Automatically maintained is a blog_list.json in assets/meta. This file contains a list of all blog posts and their metadata.
// This file is generated by a script in the utils/pageupdater folder.
const blog_list: pages.PageList = (await import('~/assets/meta/blog_list.json')) as pages.PageList;
// Automatically maintained is a blog_list.json in assets/. This file contains a list of all blog posts and their metadata. let route = useRoute()
// This file is generated by a script in the root directory of the project.
import blog_list from '../assets/blog_list.json';
const modules = import.meta.glob('/blog/');
for (const path in modules) {
const module = await modules[path]();
console.log(path, module.default); // module.default contains the image data
}
let route = useRoute();
console.log(route) console.log(route)
const url = ref(null) const url: Ref<string> = ref("")
url.value = route.query.post url.value = route.query.post as string
console.log(url.value) console.log(url.value)
const loading = ref(false) const loading: Ref<boolean> = ref(false)
const data = ref([])
const text = ref([])
const error = ref([])
const list = ref([]) const list: Ref<any> = ref([])
const tagList = ref([]) const tagList: Ref<any> = ref([])
const tagFilter = ref([]) const tagFilter: Ref<string[]> = ref([])
tagFilter.value = [] tagFilter.value = []
const article_contents: Ref<string> = ref("")
const background = ref(''); const metadata: Ref<PageInfoMetdata> = ref({
const title = ref(null); title: "",
const description = ref(null); description: "",
const date = ref(null); date: "",
const tags = ref(null); tags: [],
const previous = ref(null); background: ""
const next = ref(null); })
// watch the params of the route to fetch the data again // watch the params of the route to fetch the data again
watch(route, async () => { watch(route, async () => {
url.value = route.query.post url.value = route.query.post as string
await fetchData(route.query.post) if (url.value) {
console.log("Fetching article")
loading.value = true
try {
await fetchArticle(url.value)
}
finally {
loading.value = false
}
}
}, { }, {
immediate: true immediate: true
}); });
async function fetchData(url) {
error.value = data.value = null
loading.value = true
console.log(url)
try {
data.value = await $fetch(url)
const processed = fm(data.value)
text.value = processed.body
background.value = processed.attributes.background
title.value = processed.attributes.title
description.value = processed.attributes.description
date.value = processed.attributes.date.toLocaleDateString()
tags.value = processed.attributes.tags
if (processed.attributes.previous)
previous.value = "/blog/?post=/blog/" + processed.attributes.previous
else
previous.value = null
if (processed.attributes.next)
next.value = "/blog/?post=/blog/" + processed.attributes.next
else
next.value = null
} catch (err) {
error.value = err.toString()
console.error(err)
loading.value = false
} finally {
loading.value = false
}
}
/* If tags are specified, include only posts with those tags */
/* else, include all posts */
async function fetchList() { async function fetchList() {
/*Example formatting:
{
"posts": [
{
"metadata": {
"test": "mrrp"
},
"id": "test",
"url": "/blog/test"
},
{
"metadata": {
" title": "Awesome", "description": "A curated list of awesome stuff I like", "date": "2024-11-26", "tags": ["awesome", "curated"]
},
"id": "awesome",
"url": "/blog/awesome"
},
{
"metadata": {
"test": "mrrp",
"test2": "nya"
},
"id": "test2",
"url": "/blog/test2"
}
]
}
*/
// Extract the posts // Extract the posts
list.value = blog_list.posts list.value = blog_list.posts
@ -123,84 +63,109 @@ async function fetchList() {
tagList.value = blog_list.posts.flatMap(post => post.metadata.tags).filter((tag, index, self) => self.indexOf(tag) === index) tagList.value = blog_list.posts.flatMap(post => post.metadata.tags).filter((tag, index, self) => self.indexOf(tag) === index)
// Sort the posts by date, most recent first // Sort the posts by date, most recent first
list.value.sort((a, b) => new Date(b.metadata.date) - new Date(a.metadata.date)) list.value.sort((a: any, b: any) => b.metadata.date.localeCompare(a.metadata.date))
} }
fetchList() fetchList()
// Fetch the article contents from the URL
async function fetchArticle(url: string) {
const post = blog_list.posts.find(post => post.url === url)
if (post) {
const response = await fetch(post.url)
article_contents.value = await response.text()
}
}
function resetReadingPosition() { function resetReadingPosition() {
window.scrollTo(0, 0) window.scrollTo(0, 0)
} }
// Hook and check if the URL gets changed in the query params function updateMetadata(meta: MarkdownMetadata) {
watch(url, async () => { metadata.value = meta
await fetchData(url.value) }
}, {
immediate: true
});
</script> </script>
<template> <template>
<div class="relative z-50 flex w-full justify-center text-white"> <div class="relative z-50 flex w-full justify-center text-white">
<!-- Metadata -->
<MetaSet :title="metadata.title" :description="metadata.description" :date="metadata.date"
:background="metadata.background" :tags="metadata.tags" />
<!-- Main Content -->
<div class="mt-8 flex-col text-center"> <div class="mt-8 flex-col text-center">
<Transition name="list"> <Transition name="list">
<div v-if="url == null" :key="url"> <!-- Article List -->
<h1>Blog</h1> <div v-if="url == null" :key="url">
<div class="flex justify-center m-5"> <h1>Blog</h1>
<div v-for="tag in tagList" :key="tag" class="m-1 text-center"> <div class="flex justify-center">
<button
@click="tagFilter.includes(tag) ? tagFilter.splice(tagFilter.indexOf(tag), 1) : tagFilter.push(tag)" <div class="flex flex-wrap justify-center m-5 max-w-96">
class="text-xs bg-black border-purple-400 border text-white p-1 rounded-md" <div v-for="tag in tagList" :key="tag" class="m-1">
:class="tagFilter.includes(tag) ? 'border-2 border-white bg-slate-700' : 'border-2 bg-black text-white'">{{ <button
tag }}</button> @click="tagFilter.includes(tag) ? tagFilter.splice(tagFilter.indexOf(tag), 1) : tagFilter.push(tag)"
class="text-xs bg-black border-purple-400 border text-white p-1 rounded-md"
:class="tagFilter.includes(tag) ? 'border-2 border-white bg-slate-700' : 'border-2 bg-black text-white'">{{
tag }}</button>
</div>
</div>
</div>
<div>
<TransitionGroup name="list">
<div
v-for="post in tagFilter.length == 0 ? list : list.filter((post: PageInfo) => post.metadata.tags ? post.metadata.tags.some(tag => tagFilter.includes(tag)) : false)">
<PostCard :url="post.url" key="{{post.id}}" :tagFilter="tagFilter" />
</div>
</TransitionGroup>
</div> </div>
</div> </div>
<div> <!-- Article Viewer -->
<TransitionGroup name="list"> <div v-else>
<div v-for="post in tagFilter.length == 0 ? list : list.filter(post => tagFilter.some(tag => post.metadata.tags.includes(tag)))" :key="post.id"> <Transition name="list">
<PostCard :url="post.url" :key="post.id" :tagFilter="tagFilter"/> <div class="flex flex-col" :key="url">
<h1>{{ metadata.title }}</h1>
<small>{{ metadata.date ? new Date(metadata.date).toLocaleDateString() : "" }}</small>
<div class="max-w-50 flex flex-row justify-center">
<div v-for="tag in metadata.tags" :key="tag" class="m-1 text-center">
<span
class="text-xs bg-black border-purple-400 border-2 text-white p-1 rounded-md">{{
tag }}</span>
</div>
</div>
<div>
<!-- Next/Prev controls, on the left and right side using PostCards -->
<div class="flex max-w-4xl max-md:w-screen">
<div class="justify-start">
<NuxtLink v-if="metadata.previous" :onclick="resetReadingPosition" :to="metadata.previous"
class="m-2 text-white">Previous</NuxtLink>
</div>
<div class="justify-end">
<NuxtLink v-if="metadata.next" :onclick="resetReadingPosition" :to="metadata.next"
class="m-2 text-white">Next</NuxtLink>
</div>
</div>
</div>
<!-- Article Content -->
<Card class="text-pretty max-w-4xl mt-4 max-md:w-screen text-left">
<article>
<Markdown :input="article_contents" type="markdown" @metadata="updateMetadata" />
<!-- Aligned next/prev controls -->
<div class="flex">
<div class="justify-start">
<NuxtLink v-if="metadata.previous" :onclick="resetReadingPosition" :to="metadata.previous"
class="m-2 text-white">Previous</NuxtLink>
</div>
<div class="justify-end">
<NuxtLink v-if="metadata.next" :onclick="resetReadingPosition" :to="metadata.next"
class="m-2 text-white">Next</NuxtLink>
</div>
</div>
</article>
</Card>
</div> </div>
</TransitionGroup> </Transition>
</div> </div>
</div> </Transition>
<div v-else>
<Transition name="list">
<div class="flex flex-col" :key="url">
<h1>{{ title }}</h1>
<small>{{ date }}</small>
<div class="max-w-50 flex flex-row justify-center">
<div v-for="tag in tags" :key="tag" class="m-1 text-center">
<span class="text-xs bg-black border-purple-400 border-2 text-white p-1 rounded-md">{{
tag }}</span>
</div>
</div>
<div>
<!-- Next/Prev controls, on the left and right side using PostCards -->
<div class="flex max-w-4xl max-md:w-screen">
<div class="justify-start">
<NuxtLink v-if="previous" :onclick="resetReadingPosition" :to="previous" class="m-2 text-white">Previous</NuxtLink>
</div>
<div class="justify-end">
<NuxtLink v-if="next" :onclick="resetReadingPosition" :to="next" class="m-2 text-white">Next</NuxtLink>
</div>
</div>
</div>
<Card class="text-pretty max-w-4xl mt-4 max-md:w-screen text-left">
<Markdown :text="text"></Markdown>
<!-- Aligned next/prev controls -->
<div class="flex">
<div class="justify-start">
<NuxtLink v-if="previous" :onclick="resetReadingPosition" :to="previous" class="m-2 text-white">Previous</NuxtLink>
</div>
<div class="justify-end">
<NuxtLink v-if="next" :onclick="resetReadingPosition" :to="next" class="m-2 text-white">Next</NuxtLink>
</div>
</div>
</Card>
</div>
</Transition>
</div>
</Transition>
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,11 +1,7 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import markdownit from 'markdown-it'
import PostCard from '../components/PostCard.vue';
import configured_markdown from '~/assets/markdown_conf';
import Markdown from '~/components/Markdown.vue'; import Markdown from '~/components/Markdown.vue';
import Card from '~/components/Card.vue'; import Card from '~/components/Card.vue';
import * as siteConfig from "../assets/config.ts";
import MetaSet from '~/components/MetaSet.vue'; import MetaSet from '~/components/MetaSet.vue';
const aboutMe = ref(""); const aboutMe = ref("");
@ -20,25 +16,19 @@ fetch("/about_me.md")
</script> </script>
<template> <template>
<MetaSet title="Home" description="TheFelidae's personal site :3" tags="home, personal, author"/>
<div class="relative flex w-full justify-center text-white"> <div class="relative flex w-full justify-center text-white">
<MetaSet title="Home" description="TheFelidae's personal site :3" tags="home, personal, author"/>
<div class="mt-8 flex-col text-center"> <div class="mt-8 flex-col text-center">
<div class="flex justify-center"> <div class="flex justify-center">
<div id="PFP" class="p-1 shadow-md rounded-full bg-pink-500"> <div id="PFP" class="shadow-md rounded-full shadow-highlight">
<img class="transition-all w-40 h-40 md:w-56 md:h-56 rounded-full" <img class="transition-all w-40 h-40 md:w-56 md:h-56 rounded-full"
src="https://avatars.githubusercontent.com/u/94077364?v=4" alt="User PFP" /> src="https://avatars.githubusercontent.com/u/94077364?v=4" alt="User PFP" />
</div> </div>
</div> </div>
<Card class="max-w-4xl mt-4 max-md:w-screen"> <Card class="max-w-4xl mt-4 max-md:w-screen">
<Markdown :text="aboutMe"></Markdown> <Markdown :input="aboutMe" type="markdown"></Markdown>
</Card> </Card>
</div> </div>
</div> </div>
</template> </template>
<style scoped>
#PFP {
box-shadow: 0 0 10px 0 pink;
}
</style>

View file

@ -22,7 +22,38 @@ export default {
animation: { animation: {
appear: "appear 0.5s ease-in-out", appear: "appear 0.5s ease-in-out",
} }
} },
boxShadow: {
'md': '0px 0px 5px 5px',
'sm': '0px 0px 2px 2px'
},
colors: {
"primary": "#441196",
"secondary": "#4d0099",
"highlight": "#b805ff",
"success": "#10B981",
"danger": "#EF4444",
"warning": "#F59E0B",
"info": "#3B82F6",
"tip": "#6EE7B7",
"note": "#6B7280",
"important": "#F87171",
"dark": "#111827",
"light": "#F9FAFB",
"image": "#666666",
"gray": {
50: "#F9FAFB",
100: "#F3F4F6",
200: "#E5E7EB",
300: "#D1D5DB",
400: "#9CA3AF",
500: "#6B7280",
600: "#4B5563",
700: "#374151",
800: "#1F2937",
900: "#111827",
},
},
}, },
}, },
plugins: [], plugins: [],

View file

@ -115,5 +115,5 @@ except FileNotFoundError:
post_history = generate_post_history(pages_info, state) post_history = generate_post_history(pages_info, state)
# Output to assets/post_history.json (overwriting) # Output to assets/post_history.json (overwriting)
with open("assets/post_history.json", "w") as f: with open("assets/meta/post_history.json", "w") as f:
f.write(post_history) f.write(post_history)

View file

@ -0,0 +1,84 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as crypto from 'node:crypto';
import fm, { type FrontMatterResult } from 'front-matter';
export interface PageInfoMetdata {
title?: string;
description?: string;
date?: string;
tags?: string[];
background?: string;
next?: string ;
previous?: string;
}
// Type for metadata and page info
export interface PageInfo {
local_path: string;
absolute_path: string;
metadata: PageInfoMetdata;
hash: string;
char_count: number;
word_count: number;
path: string;
}
// Function to get metadata from a file
function getMetadata(filePath: string): PageInfoMetdata {
const fileContent = fs.readFileSync(filePath).toString();
return fm(fileContent).attributes as PageInfoMetdata
}
// Function to get SHA-256 hash of a file
function getSha256Hash(filePath: string): string {
const fileContent = fs.readFileSync(filePath);
const hash = crypto.createHash('sha256');
hash.update(fileContent.toString());
return hash.digest('hex');
}
// Function to get character count of a file
function getCharCount(filePath: string): number {
const fileContent = fs.readFileSync(filePath, 'utf8');
return fileContent.length;
}
// Function to get word count of a file
function getWordCount(filePath: string): number {
const fileContent = fs.readFileSync(filePath, 'utf8');
return fileContent.split(/\s+/).length;
}
// Function to get pages info
export function getPagesInfo(searchDirectory: string, rootDirectory: string): Record<string, PageInfo> {
const pageInfo: Record<string, PageInfo> = {};
const currentDirectory = path.join(rootDirectory, searchDirectory);
const files = fs.readdirSync(currentDirectory);
files.forEach((file) => {
const fullPath = path.join(currentDirectory, file);
const localPath = fullPath.replace(rootDirectory, '');
if (fs.lstatSync(fullPath).isDirectory()) {
Object.assign(pageInfo, getPagesInfo(path.join(searchDirectory, file), rootDirectory));
} else if (file.endsWith('.md')) {
const metadata = getMetadata(fullPath);
const sha256Hash = getSha256Hash(fullPath);
const charCount = getCharCount(fullPath);
const wordCount = getWordCount(fullPath);
pageInfo[fullPath] = {
local_path: localPath,
absolute_path: fullPath,
metadata: metadata,
hash: sha256Hash,
char_count: charCount,
word_count: wordCount,
path: fullPath
};
}
});
return pageInfo;
}

View file

@ -0,0 +1,51 @@
import * as fs from 'node:fs';
import * as pages from './pages';
import { format } from 'date-fns';
// Type for metadata and page info
export interface Page {
metadata: pages.PageInfoMetdata;
id: string;
url: string;
hash: string;
}
export interface PageList {
last_generated: string;
posts: Page[];
}
// Function to generate the page list
function generatePageList(pagesInfo: Record<string, any>): PageList {
const pageList: Page[] = [];
for (const [path, page] of Object.entries(pagesInfo)) {
const pageDict: Page = {
metadata: page.metadata,
id: page.local_path,
url: page.absolute_path.replace("public", ""),
hash: page.hash
};
pageList.push(pageDict);
}
pageList.forEach(page => {
if (page.metadata.date) {
page.metadata.date = format(new Date(page.metadata.date), 'yyyy-MM-dd HH:mm:ss');
}
});
const pageListDict: PageList = {
last_generated: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
posts: pageList
};
return pageListDict;
}
// Get the page list and print it
const postList = generatePageList(pages.getPagesInfo("", "public/blog"));
console.log(JSON.stringify(postList, null, 2));
// Output to assets/blog_list.json (overwriting)
fs.writeFileSync("assets/meta/blog_list.json", JSON.stringify(postList, null, 2));