-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathposts.data.ts
More file actions
88 lines (82 loc) · 3.05 KB
/
Copy pathposts.data.ts
File metadata and controls
88 lines (82 loc) · 3.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import { readdirSync, readFileSync } from 'fs'
import { join } from 'path'
import matter from 'gray-matter'
import { nbspBeforeDash } from './.vitepress/typography.js'
interface Post {
title: string
url: string
date: {
time: number
string: string
}
excerpt: string
}
function extractExcerpt(content: string, maxLen = 120): string {
const lines = content.split('\n')
let inCodeBlock = false
for (const line of lines) {
const trimmed = line.trim()
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
inCodeBlock = !inCodeBlock
continue
}
if (inCodeBlock) continue
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('- ') || trimmed.startsWith('* ') || trimmed.startsWith('+ ') || /^\d+\.\s/.test(trimmed) || /^\*+$/.test(trimmed) || /^[-=]{3,}$/.test(trimmed) || trimmed.startsWith('>') || trimmed.startsWith('![') || trimmed.startsWith('<') || trimmed.startsWith('|')) continue
const plain = trimmed
.replace(/\[([^\]]+)\]\((?:[^)(]|\([^)]*\))*\)/g, '$1')
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/\*(.+?)\*/g, '$1')
.replace(/__(.+?)__/g, '$1')
.replace(/_(.+?)_/g, '$1')
.replace(/`(.+?)`/g, '$1')
.replace(/ /g, ' ')
.replace(/—/g, '—')
.replace(/–/g, '–')
.replace(/&/g, '&')
.replace(/«/g, '«')
.replace(/»/g, '»')
if (plain.length < 10) continue
// Типографика сайта: nbsp перед — (markdown-it правило сюда не дотягивается —
// excerpt извлекается из сырого markdown мимо рендера).
const typo = nbspBeforeDash(plain)
return typo.length > maxLen ? typo.slice(0, maxLen) + '…' : typo
}
return ''
}
function buildPost(file: string, postsDir: string): Post {
const raw = readFileSync(join(postsDir, file), 'utf-8')
const { data: fm, content } = matter(raw)
if (!fm.date) throw new Error(`Post "${file}" is missing a date in frontmatter`)
if (!fm.title) throw new Error(`Post "${file}" is missing a title in frontmatter`)
const rawDate = fm.date instanceof Date ? fm.date.toISOString().slice(0, 10) : String(fm.date).slice(0, 10)
const date = new Date(rawDate + 'T12:00:00Z')
if (isNaN(date.getTime())) throw new Error(`Post "${file}" has an unparseable date: ${fm.date}`)
return {
title: nbspBeforeDash(fm.title as string),
url: '/posts/' + file.replace(/\.md$/, ''),
date: {
time: +date,
string: date.toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric',
}),
},
excerpt: extractExcerpt(content),
}
}
function loadPosts(): Post[] {
const postsDir = join(__dirname, 'posts')
return readdirSync(postsDir)
.filter((f: string) => f.endsWith('.md'))
.map((file: string) => buildPost(file, postsDir))
.sort((a: Post, b: Post) => b.date.time - a.date.time)
}
declare const data: Post[]
export { data }
export default {
watch: ['posts/*.md'],
async load(): Promise<Post[]> {
return loadPosts()
},
}