feat: better article parsing + html dom base
This commit is contained in:
@@ -2,6 +2,18 @@
|
|||||||
|
|
||||||
This template should help get you started developing with Vue 3 in Vite.
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- [ ] render code highlight
|
||||||
|
- [ ] render mathjax
|
||||||
|
- [ ] render plantuml
|
||||||
|
- [ ] custom css in sub repo
|
||||||
|
- [ ] custom layout in sub repo ?
|
||||||
|
- [ ] build RSS feed
|
||||||
|
- [ ] link to home
|
||||||
|
- [ ] link to previous/next article
|
||||||
|
- [ ] set page title
|
||||||
|
|
||||||
## Recommended IDE Setup
|
## Recommended IDE Setup
|
||||||
|
|
||||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|||||||
+1
-1
Submodule articles updated: 6c87d62e2a...ba3fd65916
@@ -3,6 +3,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"repository": "https://github.com/klemek/md-blog",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "run-p type-check \"build-only {@}\" --",
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
|
|||||||
+15
-8
@@ -1,11 +1,18 @@
|
|||||||
export interface MarkdownAttributes {
|
|
||||||
title?: string
|
|
||||||
draft?: string
|
|
||||||
thumbnail?: string
|
|
||||||
path?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MarkdownData {
|
export interface MarkdownData {
|
||||||
attributes: MarkdownAttributes
|
attributes: Record<string, unknown>
|
||||||
|
html: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArticleMetadata {
|
||||||
|
path: string
|
||||||
|
title: string
|
||||||
|
date: Date
|
||||||
|
author: string
|
||||||
|
thumbnail: string
|
||||||
|
draft: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Article {
|
||||||
|
metadata: ArticleMetadata
|
||||||
html: string
|
html: string
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-22
@@ -1,44 +1,59 @@
|
|||||||
import type { ArticleList, MarkdownAttributes } from '@/interfaces'
|
import type { MarkdownData, Article, ArticleMetadata } from '@interfaces'
|
||||||
import type { MarkdownData } from '@interfaces'
|
import { dateFromParts } from './dates'
|
||||||
|
|
||||||
function completeAttributes(
|
function parseMetadata(
|
||||||
srcAttributes: MarkdownAttributes,
|
srcAttributes: Record<string, unknown>,
|
||||||
pathPrefix: string,
|
pathPrefix: string,
|
||||||
): MarkdownAttributes {
|
date: Date,
|
||||||
|
): ArticleMetadata {
|
||||||
return {
|
return {
|
||||||
draft: srcAttributes.draft ?? 'false',
|
|
||||||
thumbnail: srcAttributes.thumbnail
|
|
||||||
? pathPrefix + srcAttributes.thumbnail
|
|
||||||
: pathPrefix + '/thumbnail.jpg',
|
|
||||||
title: srcAttributes.title ?? 'Untitled',
|
|
||||||
path: pathPrefix,
|
path: pathPrefix,
|
||||||
|
title: (srcAttributes.title as string) ?? 'Untitled',
|
||||||
|
date: date,
|
||||||
|
author: (srcAttributes.author as string) ?? '',
|
||||||
|
thumbnail: srcAttributes.thumbnail
|
||||||
|
? (srcAttributes.thumbnail as string).replaceAll('./', pathPrefix + '/')
|
||||||
|
: pathPrefix + '/thumbnail.jpg',
|
||||||
|
draft: !!srcAttributes.draft,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadArticle(year: number, month: number, day: number): MarkdownData | null {
|
function transformHtml(srcHtml: string, pathPrefix: string): string {
|
||||||
const path = `${year}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}`
|
return srcHtml.replaceAll('./', pathPrefix + '/')
|
||||||
console.log(path)
|
}
|
||||||
|
|
||||||
|
export async function loadArticle(date: Date): Promise<Article | null> {
|
||||||
|
const year = date.getFullYear().toString()
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||||
|
const day = date.getDate().toString().padStart(2, '0')
|
||||||
|
const path = `./articles/${year}/${month}/${day}`
|
||||||
try {
|
try {
|
||||||
const data = (await import(
|
const data = (await import(`@articles/${year}/${month}/${day}/index.md`)) as MarkdownData
|
||||||
`@articles/${year}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}/index.md`
|
|
||||||
)) as MarkdownData
|
|
||||||
return {
|
return {
|
||||||
attributes: completeAttributes(data.attributes, `./articles/${path}`),
|
metadata: parseMetadata(data.attributes, path, date),
|
||||||
html: data.html.replaceAll('./', `./articles/${path}/`),
|
html: transformHtml(data.html, path),
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listArticles(): Promise<MarkdownAttributes[]> {
|
export async function listArticles(): Promise<ArticleMetadata[]> {
|
||||||
const raw_articles = import.meta.glob('@articles/**/index.md')
|
const raw_articles = import.meta.glob('@articles/**/index.md')
|
||||||
const articles: MarkdownAttributes[] = await Promise.all(
|
const articles: ArticleMetadata[] = (
|
||||||
|
await Promise.all(
|
||||||
Object.keys(raw_articles).map(async (key) => {
|
Object.keys(raw_articles).map(async (key) => {
|
||||||
|
if (!raw_articles[key]) return null
|
||||||
const data = (await raw_articles[key]()) as MarkdownData
|
const data = (await raw_articles[key]()) as MarkdownData
|
||||||
return completeAttributes(data.attributes, key.replace('/index.md', ''))
|
const match = key.match(/\/(\d+)\/(\d+)\/(\d+)\//)
|
||||||
|
if (match === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const date = dateFromParts(match[1], match[2], match[3])
|
||||||
|
return parseMetadata(data.attributes, key.replace('/index.md', ''), date)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
articles.sort((article1, article2) => article1.path?.localeCompare(article2.path ?? '') ?? 0)
|
).filter((item) => item !== null)
|
||||||
|
articles.sort((article1, article2) => article2.date.valueOf() - article1.date.valueOf())
|
||||||
return articles
|
return articles
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export function dateFromParts(
|
||||||
|
year: string | undefined,
|
||||||
|
month: string | undefined,
|
||||||
|
day: string | undefined,
|
||||||
|
): Date {
|
||||||
|
return new Date(parseInt(year ?? ''), parseInt(month ?? '') - 1, parseInt(day ?? ''))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function simpleDateFormat(date: Date): string {
|
||||||
|
return (
|
||||||
|
date.getFullYear() +
|
||||||
|
'-' +
|
||||||
|
(date.getMonth() + 1).toString().padStart(2, '0') +
|
||||||
|
'-' +
|
||||||
|
date.getDate().toString().padStart(2, '0')
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import packageJson from '@/../package.json'
|
||||||
|
|
||||||
|
export const NAME = packageJson.name
|
||||||
|
export const VERSION = packageJson.version
|
||||||
|
export const REPOSITORY = packageJson.repository
|
||||||
+38
-17
@@ -1,39 +1,60 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { MarkdownData } from '@interfaces'
|
import type { Article } from '@interfaces'
|
||||||
import { ref, onBeforeMount } from 'vue'
|
import { ref, onBeforeMount } from 'vue'
|
||||||
import { loadArticle } from '@lib/articles'
|
import { loadArticle } from '@lib/articles'
|
||||||
import { useRoute, onBeforeRouteUpdate, type RouteLocation } from 'vue-router'
|
import { useRoute, onBeforeRouteUpdate, type RouteLocation } from 'vue-router'
|
||||||
import NotFoundView from './NotFoundView.vue'
|
import NotFoundView from './NotFoundView.vue'
|
||||||
|
import { dateFromParts, simpleDateFormat } from '@lib/dates'
|
||||||
|
import { NAME, REPOSITORY, VERSION } from '@lib/meta'
|
||||||
|
|
||||||
const markdownData = ref<MarkdownData | null>(null)
|
const article = ref<Article | null>(null)
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const articleDate = ref<Date>(new Date())
|
|
||||||
|
|
||||||
async function loadPage(target: RouteLocation) {
|
async function loadPage(target: RouteLocation) {
|
||||||
articleDate.value = new Date(
|
article.value = await loadArticle(
|
||||||
parseInt(target.params.year),
|
dateFromParts(
|
||||||
parseInt(target.params.month) - 1,
|
target.params.year as string,
|
||||||
parseInt(target.params.day),
|
target.params.month as string,
|
||||||
)
|
target.params.day as string,
|
||||||
markdownData.value = await loadArticle(
|
),
|
||||||
articleDate.value.getFullYear(),
|
|
||||||
articleDate.value.getMonth() + 1,
|
|
||||||
articleDate.value.getDate(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollTop() {
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
onBeforeMount(() => loadPage(route))
|
onBeforeMount(() => loadPage(route))
|
||||||
onBeforeRouteUpdate(loadPage)
|
onBeforeRouteUpdate(loadPage)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<template v-if="!markdownData">
|
<template v-if="!article">
|
||||||
<NotFoundView />
|
<NotFoundView />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h1>ArticleView - {{ articleDate }}</h1>
|
<main class="article">
|
||||||
<div v-html="markdownData.html"></div>
|
<div class="header">
|
||||||
|
<RouterLink class="link-home" to="/">↑</RouterLink>
|
||||||
|
<h1>{{ article.metadata.title }}</h1>
|
||||||
|
<span class="time">
|
||||||
|
<span>{{ article.metadata.draft ? 'Drafted on' : 'Published on' }}</span>
|
||||||
|
{{ simpleDateFormat(article.metadata.date) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<img :src="article.metadata.thumbnail" alt="thumbnail" />
|
||||||
|
</main>
|
||||||
|
<div id="text" v-html="article.html"></div>
|
||||||
|
<div id="signature">TODO signature</div>
|
||||||
|
<br />
|
||||||
|
<a @click.prevent="scrollTop" href="#">Go to top</a> -
|
||||||
|
<RouterLink to="/">Back to home</RouterLink>
|
||||||
|
<hr />
|
||||||
|
<footer>
|
||||||
|
<small>
|
||||||
|
{{ new Date().getFullYear() }} - Made with <a :href="REPOSITORY">{{ NAME }}</a>
|
||||||
|
{{ VERSION }}
|
||||||
|
</small>
|
||||||
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|||||||
+17
-8
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onBeforeMount } from 'vue'
|
import { ref, onBeforeMount } from 'vue'
|
||||||
import { listArticles } from '@/lib/articles'
|
import { listArticles } from '@lib/articles'
|
||||||
import type { MarkdownAttributes } from '@/interfaces'
|
import type { ArticleMetadata } from '@interfaces'
|
||||||
|
import { simpleDateFormat } from '@/lib/dates'
|
||||||
|
|
||||||
const articles = ref<MarkdownAttributes[]>([])
|
const articles = ref<ArticleMetadata[]>([])
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
const newArticles = await listArticles()
|
const newArticles = await listArticles()
|
||||||
@@ -13,10 +14,18 @@ onBeforeMount(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>Home View</h1>
|
<main>
|
||||||
<div v-for="(attr, index) in articles" v-bind:key="index">
|
<h1 class="title">Articles</h1>
|
||||||
<RouterLink :to="attr.path ?? ''">{{ attr.title }}</RouterLink>
|
<template v-for="(metadata, index) in articles" v-bind:key="index">
|
||||||
|
<div v-if="!metadata.draft && metadata.path">
|
||||||
|
<RouterLink :to="metadata.path">
|
||||||
|
<h3>{{ metadata.title }}</h3>
|
||||||
|
<span class="time"
|
||||||
|
><span>Published on</span> {{ simpleDateFormat(metadata.date) }}</span
|
||||||
|
>
|
||||||
|
<img alt="thumbnail" :src="metadata.thumbnail" />
|
||||||
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
</main>
|
||||||
<style scoped></style>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>Not found</h1>
|
<main>
|
||||||
|
<h1>Page not found</h1>
|
||||||
|
<RouterLink to="/">Back to home</RouterLink>
|
||||||
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
+5
-1
@@ -8,7 +8,11 @@
|
|||||||
|
|
||||||
// Path mapping for cleaner imports.
|
// Path mapping for cleaner imports.
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"],
|
||||||
|
"@lib/*": ["./src/lib/*"],
|
||||||
|
"@views/*": ["./src/views/*"],
|
||||||
|
"@interfaces": ["./src/interfaces.ts"],
|
||||||
|
"@articles/*": ["./articles/*"]
|
||||||
},
|
},
|
||||||
|
|
||||||
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||||
|
|||||||
Reference in New Issue
Block a user