feat: lucide icon and latex formulas
This commit is contained in:
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
VITE_APP_TITLE=My Blog
|
VITE_APP_TITLE=<i icon=rss></i> My Blog
|
||||||
|
VITE_APP_TITLE_NO_HTML=My Blog
|
||||||
VITE_APP_SIGNATURE=By <b>me</b>
|
VITE_APP_SIGNATURE=By <b>me</b>
|
||||||
VITE_APP_LANG=en
|
VITE_APP_LANG=en
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
"name": "md-blog",
|
"name": "md-blog",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
"katex": "^0.16.45",
|
||||||
|
"lucide": "^1.3.0",
|
||||||
"vue": "^3.5.32",
|
"vue": "^3.5.32",
|
||||||
"vue-router": "^5.0.4",
|
"vue-router": "^5.0.4",
|
||||||
},
|
},
|
||||||
@@ -365,6 +367,8 @@
|
|||||||
|
|
||||||
"colorjs.io": ["colorjs.io@0.5.2", "", {}, "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="],
|
"colorjs.io": ["colorjs.io@0.5.2", "", {}, "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="],
|
||||||
|
|
||||||
|
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||||
|
|
||||||
"confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
|
"confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
|
||||||
|
|
||||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
@@ -511,6 +515,8 @@
|
|||||||
|
|
||||||
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||||
|
|
||||||
|
"katex": ["katex@0.16.45", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="],
|
||||||
|
|
||||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||||
|
|
||||||
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
|
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
|
||||||
@@ -549,6 +555,8 @@
|
|||||||
|
|
||||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
|
"lucide": ["lucide@1.3.0", "", {}, "sha512-8/98ZCQjNDdfOjSycUujwqxKNrq97WPzdlNBR6tG7BqZgiOFQqzJuFapIpjPceW57sJv3zGIBg0L3SWR7c4fqA=="],
|
||||||
|
|
||||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
"magic-string-ast": ["magic-string-ast@1.0.3", "", { "dependencies": { "magic-string": "^0.30.19" } }, "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA=="],
|
"magic-string-ast": ["magic-string-ast@1.0.3", "", { "dependencies": { "magic-string": "^0.30.19" } }, "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA=="],
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>%VITE_APP_TITLE%</title>
|
<title>%VITE_APP_TITLE_NO_HTML%</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no" />
|
||||||
<link href="https://fonts.googleapis.com/css?family=Lato&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css?family=Lato&display=swap" rel="stylesheet" />
|
||||||
<link rel="stylesheet" type="text/css" href="/src/style.scss" />
|
<link rel="stylesheet" type="text/css" href="/src/style.scss" />
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
"katex": "^0.16.45",
|
||||||
|
"lucide": "^1.3.0",
|
||||||
"vue": "^3.5.32",
|
"vue": "^3.5.32",
|
||||||
"vue-router": "^5.0.4"
|
"vue-router": "^5.0.4"
|
||||||
},
|
},
|
||||||
|
|||||||
+25
-1
@@ -1,3 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import hljs from 'highlight.js'
|
||||||
|
import { createIcons, icons } from 'lucide'
|
||||||
|
import { onMounted, onUpdated, nextTick } from 'vue'
|
||||||
|
|
||||||
|
async function update() {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await nextTick()
|
||||||
|
hljs.highlightAll()
|
||||||
|
createIcons({
|
||||||
|
icons,
|
||||||
|
nameAttr: 'icon',
|
||||||
|
attrs: {
|
||||||
|
width: '1.1em',
|
||||||
|
height: '1.1em',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(update)
|
||||||
|
onUpdated(update)
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterView />
|
<RouterView @vue:mounted="update" @vue:updated="update" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { REPOSITORY, NAME, VERSION } from '@/lib/meta'
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<template v-if="$route.fullPath != '/'">
|
<template v-if="$route.fullPath != '/'">
|
||||||
<RouterLink to="/">Back to home</RouterLink>
|
<RouterLink to="/"><i icon="undo-2"></i> Back to home</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
<hr />
|
<hr />
|
||||||
<footer>
|
<footer>
|
||||||
|
|||||||
+20
-1
@@ -1,5 +1,6 @@
|
|||||||
import type { MarkdownData, Article, ArticleMetadata } from '@interfaces'
|
import type { MarkdownData, Article, ArticleMetadata } from '@interfaces'
|
||||||
import { dateFromParts } from './dates'
|
import { dateFromParts } from './dates'
|
||||||
|
import katex from 'katex'
|
||||||
|
|
||||||
function parseMetadata(
|
function parseMetadata(
|
||||||
srcAttributes: Record<string, unknown>,
|
srcAttributes: Record<string, unknown>,
|
||||||
@@ -18,8 +19,26 @@ function parseMetadata(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LATEX_REGEX = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m
|
||||||
|
const INLINE_LATEX_REGEX = /\$([^$\n]*)\$/
|
||||||
|
|
||||||
function transformHtml(srcHtml: string, pathPrefix: string): string {
|
function transformHtml(srcHtml: string, pathPrefix: string): string {
|
||||||
return srcHtml.replaceAll('./', pathPrefix + '/')
|
let html: string = srcHtml.replaceAll('="./', '="' + pathPrefix + '/')
|
||||||
|
let match: RegExpExecArray | null
|
||||||
|
do {
|
||||||
|
match = LATEX_REGEX.exec(html)
|
||||||
|
if (match && match[1]) {
|
||||||
|
console.log(match)
|
||||||
|
html = html.replace(match[0], katex.renderToString(match[1]))
|
||||||
|
}
|
||||||
|
} while (match && match[1])
|
||||||
|
do {
|
||||||
|
match = INLINE_LATEX_REGEX.exec(html)
|
||||||
|
if (match && match[1]) {
|
||||||
|
html = html.replace(match[0], katex.renderToString(match[1]))
|
||||||
|
}
|
||||||
|
} while (match && match[1])
|
||||||
|
return html
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadArticle(date: Date): Promise<Article | null> {
|
export async function loadArticle(date: Date): Promise<Article | null> {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export function stripHTML(html: string): string {
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.innerHTML = html
|
||||||
|
return div.textContent || div.innerText || ''
|
||||||
|
}
|
||||||
+3
-2
@@ -1,2 +1,3 @@
|
|||||||
@import 'highlight.js/scss/arduino-light.scss';
|
@use 'highlight.js/scss/arduino-light.scss';
|
||||||
@import '@articles/style.scss';
|
@use '@articles/style.scss';
|
||||||
|
@use 'katex/dist/katex.min.css';
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Article } from '@interfaces'
|
import type { Article } from '@interfaces'
|
||||||
import { ref, onBeforeMount, onUpdated } 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 { dateFromParts, simpleDateFormat } from '@lib/dates'
|
||||||
import { SIGNATURE, TITLE } from '@lib/meta'
|
import { SIGNATURE, TITLE } from '@lib/meta'
|
||||||
import PageFooter from '@components/PageFooter.vue'
|
import PageFooter from '@components/PageFooter.vue'
|
||||||
import hljs from 'highlight.js'
|
import { stripHTML } from '@/lib/strings'
|
||||||
|
|
||||||
const article = ref<Article | null>(null)
|
const article = ref<Article | null>(null)
|
||||||
const loading = ref<boolean>(true)
|
const loading = ref<boolean>(true)
|
||||||
@@ -22,16 +22,13 @@ async function loadPage(target: RouteLocation) {
|
|||||||
target.params.day as string,
|
target.params.day as string,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
window.document.title = TITLE + ' — ' + (article.value?.metadata.title ?? 'Not Found')
|
window.document.title =
|
||||||
|
stripHTML(TITLE) + ' — ' + stripHTML(article.value?.metadata.title ?? 'Not Found')
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(() => loadPage(route))
|
onBeforeMount(() => loadPage(route))
|
||||||
onBeforeRouteUpdate(loadPage)
|
onBeforeRouteUpdate(loadPage)
|
||||||
|
|
||||||
onUpdated(() => {
|
|
||||||
hljs.highlightAll()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -45,7 +42,7 @@ onUpdated(() => {
|
|||||||
<main class="article">
|
<main class="article">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<RouterLink class="link-home" to="/">↑</RouterLink>
|
<RouterLink class="link-home" to="/">↑</RouterLink>
|
||||||
<h1>{{ article.metadata.title }}</h1>
|
<h1 v-html="article.metadata.title"></h1>
|
||||||
<span class="time">
|
<span class="time">
|
||||||
<span>{{ article.metadata.draft ? 'Drafted on' : 'Published on' }}</span>
|
<span>{{ article.metadata.draft ? 'Drafted on' : 'Published on' }}</span>
|
||||||
{{ simpleDateFormat(article.metadata.date) }}
|
{{ simpleDateFormat(article.metadata.date) }}
|
||||||
|
|||||||
@@ -5,23 +5,24 @@ import type { ArticleMetadata } from '@interfaces'
|
|||||||
import { simpleDateFormat } from '@lib/dates'
|
import { simpleDateFormat } from '@lib/dates'
|
||||||
import { TITLE } from '@lib/meta'
|
import { TITLE } from '@lib/meta'
|
||||||
import PageFooter from '@components/PageFooter.vue'
|
import PageFooter from '@components/PageFooter.vue'
|
||||||
|
import { stripHTML } from '@/lib/strings'
|
||||||
|
|
||||||
const articles = ref<ArticleMetadata[]>([])
|
const articles = ref<ArticleMetadata[]>([])
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
const newArticles = await listArticles()
|
const newArticles = await listArticles()
|
||||||
articles.value.splice(0, articles.value.length, ...newArticles)
|
articles.value.splice(0, articles.value.length, ...newArticles)
|
||||||
window.document.title = TITLE + ' — Home'
|
window.document.title = stripHTML(TITLE) + ' — Home'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
<h1 class="title">{{ TITLE }}</h1>
|
<h1 class="title" v-html="TITLE"></h1>
|
||||||
<template v-for="(metadata, index) in articles" v-bind:key="index">
|
<template v-for="(metadata, index) in articles" v-bind:key="index">
|
||||||
<div v-if="!metadata.draft && metadata.path" class="article">
|
<div v-if="!metadata.draft && metadata.path" class="article">
|
||||||
<RouterLink :to="metadata.path">
|
<RouterLink :to="metadata.path">
|
||||||
<h3>{{ metadata.title }}</h3>
|
<h3 v-html="metadata.title"></h3>
|
||||||
<span class="time"
|
<span class="time"
|
||||||
><span>Published on</span> {{ simpleDateFormat(metadata.date) }}</span
|
><span>Published on</span> {{ simpleDateFormat(metadata.date) }}</span
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { TITLE } from '@/lib/meta'
|
import { TITLE } from '@/lib/meta'
|
||||||
|
import { stripHTML } from '@/lib/strings'
|
||||||
import PageFooter from '@components/PageFooter.vue'
|
import PageFooter from '@components/PageFooter.vue'
|
||||||
import { onBeforeMount } from 'vue'
|
import { onBeforeMount } from 'vue'
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
window.document.title = TITLE + ' — Not Found'
|
window.document.title = stripHTML(TITLE) + ' — Not Found'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -14,5 +15,3 @@ onBeforeMount(() => {
|
|||||||
<PageFooter />
|
<PageFooter />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user