feat: Refactor service management scripts to use a unified dev script
- Added package.json to manage development scripts. - Updated restart-services.ps1 to call the new dev script for starting services. - Refactored start-admin.ps1, start-backend.ps1, start-frontend.ps1, and start-mcp.ps1 to utilize the dev script for starting respective services. - Enhanced stop-services.ps1 to improve process termination logic by matching command patterns.
This commit is contained in:
247
admin/src/lib/markdown-document.ts
Normal file
247
admin/src/lib/markdown-document.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { normalizeMarkdown } from '@/lib/markdown-diff'
|
||||
|
||||
export type ParsedMarkdownMeta = {
|
||||
title: string
|
||||
slug: string
|
||||
description: string
|
||||
category: string
|
||||
postType: string
|
||||
image: string
|
||||
images: string[]
|
||||
pinned: boolean
|
||||
published: boolean
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export type ParsedMarkdownDocument = {
|
||||
meta: ParsedMarkdownMeta
|
||||
body: string
|
||||
markdown: string
|
||||
}
|
||||
|
||||
const defaultMeta: ParsedMarkdownMeta = {
|
||||
title: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
category: '',
|
||||
postType: 'article',
|
||||
image: '',
|
||||
images: [],
|
||||
pinned: false,
|
||||
published: true,
|
||||
tags: [],
|
||||
}
|
||||
|
||||
function parseScalar(value: string) {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (
|
||||
trimmed.startsWith('"') ||
|
||||
trimmed.startsWith("'") ||
|
||||
trimmed.startsWith('[') ||
|
||||
trimmed.startsWith('{')
|
||||
) {
|
||||
try {
|
||||
return JSON.parse(trimmed)
|
||||
} catch {
|
||||
return trimmed.replace(/^['"]|['"]$/g, '')
|
||||
}
|
||||
}
|
||||
|
||||
if (trimmed === 'true') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (trimmed === 'false') {
|
||||
return false
|
||||
}
|
||||
|
||||
return trimmed
|
||||
}
|
||||
|
||||
function toStringList(value: unknown) {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((item) => String(item).trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
.split(/[,,]/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export function parseMarkdownDocument(markdown: string): ParsedMarkdownDocument {
|
||||
const normalized = normalizeMarkdown(markdown)
|
||||
const meta: ParsedMarkdownMeta = { ...defaultMeta }
|
||||
|
||||
if (!normalized.startsWith('---\n')) {
|
||||
return {
|
||||
meta,
|
||||
body: normalized.trimStart(),
|
||||
markdown: normalized,
|
||||
}
|
||||
}
|
||||
|
||||
const endIndex = normalized.indexOf('\n---\n', 4)
|
||||
if (endIndex === -1) {
|
||||
return {
|
||||
meta,
|
||||
body: normalized.trimStart(),
|
||||
markdown: normalized,
|
||||
}
|
||||
}
|
||||
|
||||
const frontmatter = normalized.slice(4, endIndex)
|
||||
const body = normalized.slice(endIndex + 5).trimStart()
|
||||
let currentListKey: 'tags' | 'images' | 'categories' | null = null
|
||||
const categories: string[] = []
|
||||
|
||||
frontmatter.split('\n').forEach((line) => {
|
||||
const listItemMatch = line.match(/^\s*-\s*(.+)\s*$/)
|
||||
if (listItemMatch && currentListKey) {
|
||||
const parsed = parseScalar(listItemMatch[1])
|
||||
const nextValue = typeof parsed === 'string' ? parsed.trim() : String(parsed).trim()
|
||||
if (!nextValue) {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentListKey === 'tags') {
|
||||
meta.tags.push(nextValue)
|
||||
} else if (currentListKey === 'images') {
|
||||
meta.images.push(nextValue)
|
||||
} else {
|
||||
categories.push(nextValue)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
currentListKey = null
|
||||
|
||||
const keyMatch = line.match(/^([A-Za-z_]+):\s*(.*)$/)
|
||||
if (!keyMatch) {
|
||||
return
|
||||
}
|
||||
|
||||
const [, rawKey, rawValue] = keyMatch
|
||||
const key = rawKey.trim()
|
||||
const value = parseScalar(rawValue)
|
||||
|
||||
if (key === 'tags') {
|
||||
const tags = toStringList(value)
|
||||
if (tags.length) {
|
||||
meta.tags = tags
|
||||
} else if (!String(rawValue).trim()) {
|
||||
currentListKey = 'tags'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (key === 'images') {
|
||||
const images = toStringList(value)
|
||||
if (images.length) {
|
||||
meta.images = images
|
||||
} else if (!String(rawValue).trim()) {
|
||||
currentListKey = 'images'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (key === 'categories' || key === 'category') {
|
||||
const parsedCategories = toStringList(value)
|
||||
if (parsedCategories.length) {
|
||||
categories.push(...parsedCategories)
|
||||
} else if (!String(rawValue).trim()) {
|
||||
currentListKey = 'categories'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'title':
|
||||
meta.title = String(value).trim()
|
||||
break
|
||||
case 'slug':
|
||||
meta.slug = String(value).trim()
|
||||
break
|
||||
case 'description':
|
||||
meta.description = String(value).trim()
|
||||
break
|
||||
case 'post_type':
|
||||
meta.postType = String(value).trim() || 'article'
|
||||
break
|
||||
case 'image':
|
||||
meta.image = String(value).trim()
|
||||
break
|
||||
case 'pinned':
|
||||
meta.pinned = Boolean(value)
|
||||
break
|
||||
case 'published':
|
||||
meta.published = value !== false
|
||||
break
|
||||
case 'draft':
|
||||
if (value === true) {
|
||||
meta.published = false
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
meta.category = categories[0] ?? meta.category
|
||||
|
||||
return {
|
||||
meta,
|
||||
body,
|
||||
markdown: normalized,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildMarkdownDocument(meta: ParsedMarkdownMeta, body: string) {
|
||||
const lines = [
|
||||
'---',
|
||||
`title: ${JSON.stringify(meta.title.trim() || meta.slug || 'untitled-post')}`,
|
||||
`slug: ${meta.slug.trim() || 'untitled-post'}`,
|
||||
]
|
||||
|
||||
if (meta.description.trim()) {
|
||||
lines.push(`description: ${JSON.stringify(meta.description.trim())}`)
|
||||
}
|
||||
|
||||
if (meta.category.trim()) {
|
||||
lines.push(`category: ${JSON.stringify(meta.category.trim())}`)
|
||||
}
|
||||
|
||||
lines.push(`post_type: ${JSON.stringify(meta.postType.trim() || 'article')}`)
|
||||
lines.push(`pinned: ${meta.pinned ? 'true' : 'false'}`)
|
||||
lines.push(`published: ${meta.published ? 'true' : 'false'}`)
|
||||
|
||||
if (meta.image.trim()) {
|
||||
lines.push(`image: ${JSON.stringify(meta.image.trim())}`)
|
||||
}
|
||||
|
||||
if (meta.images.length) {
|
||||
lines.push('images:')
|
||||
meta.images.forEach((image) => {
|
||||
lines.push(` - ${JSON.stringify(image)}`)
|
||||
})
|
||||
}
|
||||
|
||||
if (meta.tags.length) {
|
||||
lines.push('tags:')
|
||||
meta.tags.forEach((tag) => {
|
||||
lines.push(` - ${JSON.stringify(tag)}`)
|
||||
})
|
||||
}
|
||||
|
||||
return `${lines.join('\n')}\n---\n\n${body.trim()}\n`
|
||||
}
|
||||
Reference in New Issue
Block a user