:3
This commit is contained in:
parent
328a6bd178
commit
1dbf1e4074
21 changed files with 1046 additions and 545 deletions
8
.github/workflows/nuxtjs.yml
vendored
8
.github/workflows/nuxtjs.yml
vendored
|
@ -34,10 +34,14 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
- name: Activate Venv
|
||||
run: source venv/bin/activate
|
||||
- name: Setup Deno
|
||||
uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v2.x
|
||||
- name: Install dependencies
|
||||
run: pip install python-frontmatter
|
||||
- name: Update Page manifest
|
||||
run: python utils/pageupdater/page_list_gen.py
|
||||
- name: Update Page list
|
||||
run: deno utils/pageupdater/update_pagelist.ts
|
||||
- name: Update Page history
|
||||
run: python utils/pageupdater/commit_post_history.py
|
||||
- name: Generate RSS feed
|
||||
|
|
3
app.vue
3
app.vue
|
@ -3,7 +3,8 @@ import { main } from "@popperjs/core";
|
|||
import MainPage from "./pages/index.vue"
|
||||
import backgroundCalm from "./components/BackgroundCalm.vue";
|
||||
import Navbar from "./components/Navbar.vue"
|
||||
import './assets/style.css'
|
||||
import './assets/style/style.css'
|
||||
import siteConfig from '~/assets/config'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -6,7 +6,7 @@ export default {
|
|||
siteImage: '',
|
||||
|
||||
// Site personalization
|
||||
sitePrimaryColor: '#550077',
|
||||
siteColor: '#550077',
|
||||
|
||||
// Author information
|
||||
siteAuthor: 'TheFelidae',
|
||||
|
|
|
@ -1,16 +1,212 @@
|
|||
// TypeScript utilities for rendering Markdown/HTML content
|
||||
|
||||
import hljs from "highlight.js";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import Token from "markdown-it/lib/token.mjs";
|
||||
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 { tasklist } from "@mdit/plugin-tasklist";
|
||||
import { mark } from "@mdit/plugin-mark";
|
||||
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({
|
||||
breaks: true,
|
||||
typographer: true,
|
||||
|
@ -69,3 +265,11 @@ export default function configured_markdown(): MarkdownIt {
|
|||
|
||||
return md;
|
||||
}
|
||||
|
||||
export var globalMarkdown = new MarkdownContext(undefined);
|
||||
|
||||
export default {
|
||||
MarkdownInput,
|
||||
MarkdownContext,
|
||||
globalMarkdown
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"last_generated": "2025-01-04 02:30:21",
|
||||
"last_generated": "2025-01-03 20:00:53",
|
||||
"posts": [
|
||||
{
|
||||
"metadata": {
|
||||
"title": "3DS Programming - Using RomFS",
|
||||
"description": "A guide to using RomFS on the 3DS. (Old)",
|
||||
"date": "2025-01-01",
|
||||
"date": "2025-01-01T00:00:00.000Z",
|
||||
"tags": [
|
||||
"3ds",
|
||||
"programming",
|
||||
|
@ -20,25 +20,11 @@
|
|||
"url": "/blog/old3ds_romfs.md",
|
||||
"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": {
|
||||
"title": "Awesome",
|
||||
"description": "A curated list of awesome stuff I like",
|
||||
"date": "2024-11-26",
|
||||
"date": "2024-11-26T00:00:00.000Z",
|
||||
"tags": [
|
||||
"awesome",
|
||||
"curated"
|
||||
|
@ -50,41 +36,24 @@
|
|||
},
|
||||
{
|
||||
"metadata": {
|
||||
"title": "LGBTQ+ Resources",
|
||||
"description": "A list of resources for LGBTQ+ individuals",
|
||||
"date": "2025-01-02",
|
||||
"title": "Badges!",
|
||||
"description": "A collection of 88x31 badges for various things",
|
||||
"date": "2024-12-21T00:00:00.000Z",
|
||||
"tags": [
|
||||
"lgbtq+",
|
||||
"resources"
|
||||
"badges",
|
||||
"retro",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
"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-01",
|
||||
"tags": [
|
||||
"3ds",
|
||||
"programming",
|
||||
"c",
|
||||
"devkitpro",
|
||||
"old"
|
||||
],
|
||||
"next": "old3ds_romfs.md"
|
||||
},
|
||||
"id": "/old3ds_helloworld.md",
|
||||
"url": "/blog/old3ds_helloworld.md",
|
||||
"hash": "86e0bd1deae0d00b17ab0960634ea7292d6387063f70600cec4001564fde9514"
|
||||
"id": "/badges.md",
|
||||
"url": "/blog/badges.md",
|
||||
"hash": "338ccfecc6523dff93708330a8b43af715f1e80d55e1cc3bea2d1a7306fc4f00"
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"title": "3DS Programming - Touchscreen Input",
|
||||
"description": "A guide to using the touchscreen on the 3DS. (Old)",
|
||||
"date": "2025-01-01",
|
||||
"date": "2025-01-01T00:00:00.000Z",
|
||||
"tags": [
|
||||
"3ds",
|
||||
"programming",
|
||||
|
@ -100,18 +69,49 @@
|
|||
},
|
||||
{
|
||||
"metadata": {
|
||||
"title": "Badges!",
|
||||
"description": "A collection of 88x31 badges for various things",
|
||||
"date": "2024-12-21",
|
||||
"title": "Styling Test",
|
||||
"description": "A test post to see how the site styling looks",
|
||||
"date": "2025-01-01T00:00:00.000Z",
|
||||
"tags": [
|
||||
"badges",
|
||||
"retro",
|
||||
"meta",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
"id": "/badges.md",
|
||||
"url": "/blog/badges.md",
|
||||
"hash": "338ccfecc6523dff93708330a8b43af715f1e80d55e1cc3bea2d1a7306fc4f00"
|
||||
"id": "/styling_test.md",
|
||||
"url": "/blog/styling_test.md",
|
||||
"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
250
assets/style/markdown.scss
Normal 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;
|
||||
}
|
|
@ -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 |
|
@ -1,11 +1,3 @@
|
|||
<script setup lang="ts">
|
||||
import { Engine } from '@tsparticles/engine';
|
||||
|
||||
const onLoad = (container: Container) => {
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NuxtParticles
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -8,9 +8,8 @@
|
|||
|
||||
/* Glow effect on border, plus a slight response to hover by changing the border color near the cursor */
|
||||
.container {
|
||||
@apply border-primary border-2 shadow-primary shadow-md;
|
||||
transition: border-color 0.5s;
|
||||
border-color: #A020F0;
|
||||
box-shadow: 0 0 10px #A020F0;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -1,306 +1,63 @@
|
|||
<script setup lang="ts">
|
||||
import configured_markdown from '~/assets/markdown_conf';
|
||||
import { globalMarkdown, MarkdownInput } from '~/assets/markdown_conf';
|
||||
import { useSlots } from 'vue';
|
||||
import fm, { type FrontMatterResult } from 'front-matter';
|
||||
import '~/assets/style/markdown.scss';
|
||||
|
||||
const text = ref("");
|
||||
const loading = ref(true);
|
||||
|
||||
const props = defineProps({
|
||||
text: String,
|
||||
// External inputs (reactive)
|
||||
const input = defineProps({
|
||||
input: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "markdown",
|
||||
validator: (value: string) => ["markdown", "html"].includes(value)
|
||||
}
|
||||
});
|
||||
|
||||
function render_markdown(data: string | undefined) {
|
||||
// Validate that the data is a string
|
||||
if (typeof data !== 'string') {
|
||||
loading.value = false;
|
||||
// Internal state
|
||||
const contents = ref("");
|
||||
const loading = ref(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;
|
||||
}
|
||||
text.value = configured_markdown().render(data);
|
||||
loading.value = false
|
||||
const markdown_input: MarkdownInput = type ?
|
||||
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) => {
|
||||
render_markdown(newVal);
|
||||
}, {
|
||||
immediate: true
|
||||
});
|
||||
watch(() => input.input, (newVal) => {
|
||||
updateContents(newVal, input.type as "markdown" | "html");
|
||||
}, { immediate: true });
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="loading" class="text-center animate-pulse">
|
||||
<h2>Loading...</h2>
|
||||
</div>
|
||||
<div class="md-contents" v-html="text"></div>
|
||||
</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>
|
||||
<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>
|
|
@ -4,11 +4,26 @@ import { ref } from 'vue';
|
|||
import siteConfig from '../assets/config';
|
||||
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
description: String,
|
||||
date: String,
|
||||
tags: Array<String>,
|
||||
background: String
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Untitled'
|
||||
},
|
||||
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 || []);
|
||||
|
@ -39,7 +54,7 @@ useSeoMeta({
|
|||
ogSiteName: siteConfig.siteTitle,
|
||||
ogLocale: 'en_US',
|
||||
ogLocaleAlternate: 'en_GB',
|
||||
themeColor: siteConfig.sitePrimaryColor,
|
||||
themeColor: siteConfig.siteColor,
|
||||
twitterCard: 'summary',
|
||||
twitterTitle: fullTitle,
|
||||
twitterDescription: description,
|
||||
|
@ -54,7 +69,7 @@ useHead({
|
|||
{ name: 'keywords', content: tagsToString(tags.value) },
|
||||
{ name: 'author', content: siteConfig.siteAuthor },
|
||||
{ name: 'date', content: date },
|
||||
{ name: 'theme-color', content: siteConfig.sitePrimaryColor },
|
||||
{ name: 'theme-color', content: siteConfig.siteColor },
|
||||
{ name: 'twitter:card', content: 'summary' },
|
||||
{ name: 'twitter:title', content: fullTitle },
|
||||
{ name: 'twitter:description', content: description },
|
||||
|
@ -72,4 +87,31 @@ useHead({
|
|||
]
|
||||
})
|
||||
|
||||
</script>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
130
deno.lock
generated
130
deno.lock
generated
|
@ -16,6 +16,7 @@
|
|||
"npm:@types/markdown-it@^14.1.2": "14.1.2",
|
||||
"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:date-fns@4.1.0": "4.1.0",
|
||||
"npm:defu@^6.1.4": "6.1.4",
|
||||
"npm:front-matter@^4.0.2": "4.0.2",
|
||||
"npm:highlight.js@^11.10.0": "11.11.0",
|
||||
|
@ -27,6 +28,7 @@
|
|||
"npm:ofetch@^1.4.1": "1.4.1",
|
||||
"npm:postcss-loader@^8.1.1": "8.1.1_postcss@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:tex-to-svg@0.2": "0.2.0",
|
||||
"npm:texsvg@^2.2.2": "2.2.2",
|
||||
|
@ -281,6 +283,9 @@
|
|||
"@babel/helper-validator-identifier"
|
||||
]
|
||||
},
|
||||
"@bufbuild/protobuf@2.2.3": {
|
||||
"integrity": "sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg=="
|
||||
},
|
||||
"@cloudflare/kv-asset-handler@0.3.4": {
|
||||
"integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==",
|
||||
"dependencies": [
|
||||
|
@ -2101,6 +2106,9 @@
|
|||
"update-browserslist-db"
|
||||
]
|
||||
},
|
||||
"buffer-builder@0.2.0": {
|
||||
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg=="
|
||||
},
|
||||
"buffer-crc32@1.0.0": {
|
||||
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="
|
||||
},
|
||||
|
@ -2297,6 +2305,9 @@
|
|||
"colorette@1.4.0": {
|
||||
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="
|
||||
},
|
||||
"colorjs.io@0.5.2": {
|
||||
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="
|
||||
},
|
||||
"commander@2.20.3": {
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
|
@ -2562,6 +2573,9 @@
|
|||
"csstype@3.1.3": {
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||
},
|
||||
"date-fns@4.1.0": {
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
|
||||
},
|
||||
"db0@0.2.1": {
|
||||
"integrity": "sha512-BWSFmLaCkfyqbSEZBQINMVNjCVfrogi7GQ2RSy1tmtfK9OXlsup6lUMwLsqSD7FbAjD04eWFdXowSHHUp6SE/Q=="
|
||||
},
|
||||
|
@ -3367,6 +3381,9 @@
|
|||
"image-meta@0.2.1": {
|
||||
"integrity": "sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw=="
|
||||
},
|
||||
"immutable@5.0.3": {
|
||||
"integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw=="
|
||||
},
|
||||
"import-fresh@3.3.0": {
|
||||
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
|
||||
"dependencies": [
|
||||
|
@ -5071,12 +5088,111 @@
|
|||
"queue-microtask"
|
||||
]
|
||||
},
|
||||
"rxjs@7.8.1": {
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dependencies": [
|
||||
"tslib"
|
||||
]
|
||||
},
|
||||
"safe-buffer@5.1.2": {
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"safe-buffer@5.2.1": {
|
||||
"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": {
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"dependencies": [
|
||||
|
@ -5437,6 +5553,15 @@
|
|||
"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": {
|
||||
"integrity": "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="
|
||||
},
|
||||
|
@ -5824,6 +5949,9 @@
|
|||
"uuid@9.0.1": {
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="
|
||||
},
|
||||
"varint@6.0.0": {
|
||||
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="
|
||||
},
|
||||
"vite-hot-client@0.2.3_vite@5.4.11": {
|
||||
"integrity": "sha512-rOGAV7rUlUHX89fP2p2v0A2WWvV3QMX2UYq0fRqsWSvFvev4atHWqjwGoKaZT1VTKyLGk533ecu3eyd0o59CAg==",
|
||||
"dependencies": [
|
||||
|
@ -6141,6 +6269,7 @@
|
|||
"npm:@types/markdown-it@^14.1.2",
|
||||
"npm:autoprefixer@^10.4.20",
|
||||
"npm:css-loader@^7.1.2",
|
||||
"npm:date-fns@4.1.0",
|
||||
"npm:front-matter@^4.0.2",
|
||||
"npm:highlight.js@^11.10.0",
|
||||
"npm:markdown-it-checkbox@^1.1.0",
|
||||
|
@ -6151,6 +6280,7 @@
|
|||
"npm:ofetch@^1.4.1",
|
||||
"npm:postcss-loader@^8.1.1",
|
||||
"npm:postcss@^8.4.49",
|
||||
"npm:sass-embedded@^1.83.1",
|
||||
"npm:tailwindcss@^3.4.17",
|
||||
"npm:tex-to-svg@0.2",
|
||||
"npm:texsvg@^2.2.2",
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"nuxt": "^3.14.1592",
|
||||
"ofetch": "^1.4.1",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"sass-embedded": "^1.83.1",
|
||||
"tex-to-svg": "^0.2.0",
|
||||
"texsvg": "^2.2.2",
|
||||
"tsparticles": "^3.7.1",
|
||||
|
@ -37,7 +38,8 @@
|
|||
"@tsparticles/vue3": "^3.0.1",
|
||||
"front-matter": "^4.0.2",
|
||||
"highlight.js": "^11.10.0",
|
||||
"markdown-it": "^14.1.0"
|
||||
"markdown-it": "^14.1.0",
|
||||
"date-fns": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.20",
|
||||
|
|
275
pages/blog.vue
275
pages/blog.vue
|
@ -1,121 +1,61 @@
|
|||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch, ref } from 'vue';
|
||||
import fm from 'front-matter';
|
||||
|
||||
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.
|
||||
// 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();
|
||||
let route = useRoute()
|
||||
console.log(route)
|
||||
|
||||
const url = ref(null)
|
||||
url.value = route.query.post
|
||||
const url: Ref<string> = ref("")
|
||||
url.value = route.query.post as string
|
||||
|
||||
console.log(url.value)
|
||||
|
||||
const loading = ref(false)
|
||||
const data = ref([])
|
||||
const text = ref([])
|
||||
const error = ref([])
|
||||
const loading: Ref<boolean> = ref(false)
|
||||
|
||||
const list = ref([])
|
||||
const tagList = ref([])
|
||||
const tagFilter = ref([])
|
||||
const list: Ref<any> = ref([])
|
||||
const tagList: Ref<any> = ref([])
|
||||
const tagFilter: Ref<string[]> = ref([])
|
||||
tagFilter.value = []
|
||||
|
||||
const article_contents: Ref<string> = ref("")
|
||||
|
||||
const background = ref('');
|
||||
const title = ref(null);
|
||||
const description = ref(null);
|
||||
const date = ref(null);
|
||||
const tags = ref(null);
|
||||
const previous = ref(null);
|
||||
const next = ref(null);
|
||||
const metadata: Ref<PageInfoMetdata> = ref({
|
||||
title: "",
|
||||
description: "",
|
||||
date: "",
|
||||
tags: [],
|
||||
background: ""
|
||||
})
|
||||
|
||||
// watch the params of the route to fetch the data again
|
||||
|
||||
watch(route, async () => {
|
||||
url.value = route.query.post
|
||||
await fetchData(route.query.post)
|
||||
url.value = route.query.post as string
|
||||
if (url.value) {
|
||||
console.log("Fetching article")
|
||||
loading.value = true
|
||||
try {
|
||||
await fetchArticle(url.value)
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}, {
|
||||
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() {
|
||||
/*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
|
||||
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)
|
||||
|
||||
// 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()
|
||||
|
||||
// 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() {
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
// Hook and check if the URL gets changed in the query params
|
||||
watch(url, async () => {
|
||||
await fetchData(url.value)
|
||||
}, {
|
||||
immediate: true
|
||||
});
|
||||
function updateMetadata(meta: MarkdownMetadata) {
|
||||
metadata.value = meta
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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">
|
||||
<Transition name="list">
|
||||
<div v-if="url == null" :key="url">
|
||||
<h1>Blog</h1>
|
||||
<div class="flex justify-center m-5">
|
||||
<div v-for="tag in tagList" :key="tag" class="m-1 text-center">
|
||||
<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>
|
||||
<!-- Article List -->
|
||||
<div v-if="url == null" :key="url">
|
||||
<h1>Blog</h1>
|
||||
<div class="flex justify-center">
|
||||
|
||||
<div class="flex flex-wrap justify-center m-5 max-w-96">
|
||||
<div v-for="tag in tagList" :key="tag" class="m-1">
|
||||
<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>
|
||||
<TransitionGroup name="list">
|
||||
<div v-for="post in tagFilter.length == 0 ? list : list.filter(post => tagFilter.some(tag => post.metadata.tags.includes(tag)))" :key="post.id">
|
||||
<PostCard :url="post.url" :key="post.id" :tagFilter="tagFilter"/>
|
||||
<!-- Article Viewer -->
|
||||
<div v-else>
|
||||
<Transition name="list">
|
||||
<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>
|
||||
</TransitionGroup>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
<script setup>
|
||||
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 Card from '~/components/Card.vue';
|
||||
import * as siteConfig from "../assets/config.ts";
|
||||
import MetaSet from '~/components/MetaSet.vue';
|
||||
|
||||
const aboutMe = ref("");
|
||||
|
@ -20,25 +16,19 @@ fetch("/about_me.md")
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<MetaSet title="Home" description="TheFelidae's personal site :3" tags="home, personal, author"/>
|
||||
|
||||
<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="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"
|
||||
src="https://avatars.githubusercontent.com/u/94077364?v=4" alt="User PFP" />
|
||||
</div>
|
||||
</div>
|
||||
<Card class="max-w-4xl mt-4 max-md:w-screen">
|
||||
<Markdown :text="aboutMe"></Markdown>
|
||||
<Markdown :input="aboutMe" type="markdown"></Markdown>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#PFP {
|
||||
box-shadow: 0 0 10px 0 pink;
|
||||
}
|
||||
</style>
|
||||
</template>
|
|
@ -22,7 +22,38 @@ export default {
|
|||
animation: {
|
||||
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: [],
|
||||
|
|
|
@ -115,5 +115,5 @@ except FileNotFoundError:
|
|||
post_history = generate_post_history(pages_info, state)
|
||||
|
||||
# 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)
|
84
utils/pageupdater/pages.ts
Normal file
84
utils/pageupdater/pages.ts
Normal 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;
|
||||
}
|
51
utils/pageupdater/update_pagelist.ts
Normal file
51
utils/pageupdater/update_pagelist.ts
Normal 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));
|
Loading…
Add table
Reference in a new issue