refactor: 移除文章导入功能及相关解析依赖
continuous-integration/drone/push Build is failing

This commit is contained in:
seahi committed 2026-09-11 09:37:26 +08:00
1 parent c91ef5aac9
commit 2b6578af92
5 files changed
+3 -389

No files matched your search

+1 -3
View File
@@ -54,8 +54,6 @@
"marked": "^18.0.9", "marked": "^18.0.9",
"marked-highlight": "^2.2.4", "marked-highlight": "^2.2.4",
"sanitize-html": "2.17.6", "sanitize-html": "2.17.6",
"smol-toml": "^1.7.1", "tocbot": "^4.36.8"
"tocbot": "^4.36.8",
"yaml": "^2.9.0"
} }
} }
+2 -13
View File
@@ -45,15 +45,9 @@ importers:
sanitize-html: sanitize-html:
specifier: 2.17.6 specifier: 2.17.6
version: 2.17.6 version: 2.17.6
smol-toml:
specifier: ^1.7.1
version: 1.7.1
tocbot: tocbot:
specifier: ^4.36.8 specifier: ^4.36.8
version: 4.36.8 version: 4.36.8
yaml:
specifier: ^2.9.0
version: 2.9.0
devDependencies: devDependencies:
'@eslint/js': '@eslint/js':
specifier: ^10.0.1 specifier: ^10.0.1
@@ -1631,10 +1625,6 @@ packages:
resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
engines: {node: '>=18'} engines: {node: '>=18'}
smol-toml@1.7.1:
resolution: {integrity: sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==}
engines: {node: '>= 18'}
source-map-js@1.2.1: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -3456,8 +3446,6 @@ snapshots:
mrmime: 2.0.1 mrmime: 2.0.1
totalist: 3.0.1 totalist: 3.0.1
smol-toml@1.7.1: {}
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
stackback@0.0.2: {} stackback@0.0.2: {}
@@ -3679,7 +3667,8 @@ snapshots:
yaml@1.10.3: {} yaml@1.10.3: {}
yaml@2.9.0: {} yaml@2.9.0:
optional: true
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
-144
View File
@@ -1,144 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { parse as parseToml } from 'smol-toml';
import { parse as parseYaml } from 'yaml';
export interface HugoPostData {
title: string;
slug: string;
content: string;
cover: string;
published_at: string;
updated_at: string;
categories: string[];
tags: string[];
series: string[];
series_weight: number | null;
}
/**
* 解析并转换时间,一旦解析失败或空缺,严格使用当前系统时间
*/
function parseOrCurrentDate(val: unknown): string {
if (!val) return new Date().toISOString();
try {
const dateObj = new Date(val as string | number | Date);
if (isNaN(dateObj.getTime())) {
return new Date().toISOString();
}
return dateObj.toISOString();
} catch (err: any) {
return new Date().toISOString();
}
}
/**
* 清洗分类和标签列表(同时支持数组与单字符格式)
*/
function parseArrayField(val: unknown): string[] {
if (Array.isArray(val)) {
return val.map((item) => String(item).trim()).filter(Boolean);
}
if (typeof val === 'string' && val.trim() !== '') {
return [val.trim()];
}
return [];
}
function parseIntegerField(val: unknown): number | null {
if (val === null || val === undefined || val === '') return null;
const parsed = typeof val === 'number' ? val : Number(String(val).trim());
return Number.isInteger(parsed) ? parsed : null;
}
/**
* 去掉文件名结尾的格式拓展名,提供纯文件名基础
*/
function stripExtension(filename: string): string {
return filename.replace(/\.(md|markdown|txt)$/i, '').trim();
}
/**
* Hugo Markdown 头部元数据及正文解析器
* 同时原生兼容 TOML (+++) 与 YAML (---) 语法,自动过滤并丢弃本项目无端对应参数
*/
export function parseHugoMarkdown(rawContent: string, fileName = ''): HugoPostData {
// 剔除可能存在的 UTF-8 BOM
const text = rawContent.replace(/^\uFEFF/, '').trim();
const defaultName = fileName ? stripExtension(fileName) : '新文章';
let meta: Record<string, unknown> = {};
let bodyContent = text;
try {
// 1. 优先检识 TOML (以 +++ 开头包络)
if (text.startsWith('+++')) {
const endIndex = text.indexOf('\n+++', 3);
if (endIndex !== -1) {
const frontmatter = text.substring(3, endIndex).trim();
bodyContent = text.substring(endIndex + 4).trim();
meta = parseToml(frontmatter) as Record<string, any>;
}
}
// 2. 检识常规 YAML (以 --- 开头包络)
else if (text.startsWith('---')) {
const endIndex = text.indexOf('\n---', 3);
if (endIndex !== -1) {
const frontmatter = text.substring(3, endIndex).trim();
bodyContent = text.substring(endIndex + 4).trim();
meta = parseYaml(frontmatter) as Record<string, any>;
}
}
} catch (err: any) {
console.error(
'解析 Markdown 头部参数时出现异常,直接回货底稿正文并对时差退回初始系统时刻:',
err
);
// 当解析受卡出错,空字典依然不阻挠对下方日期与稿费正文字面的顺利捕拾
}
// 取值并完成映射:除以下对应参数处,诸如 draft/twemoji/lightgallery 等全在空载转储里随水蒸丢!
const title =
typeof meta.title === 'string' && meta.title.trim() !== '' ? meta.title.trim() : defaultName;
let slug = typeof meta.slug === 'string' ? meta.slug.trim() : '';
if (!slug) {
// 没有声明明确的 slug 时,由名目精简转化出基础链接字符串
slug = defaultName
.toLowerCase()
.replace(/[^\w\u4e00-\u9fa5\s-]/g, '')
.replace(/\s+/g, '-');
if (!slug) {
slug = 'post-' + Date.now();
}
}
// 特色图片字段识别(首挑 featuredImage,也兼备处理 cover/image)
const cover =
typeof meta.featuredImage === 'string'
? meta.featuredImage.trim()
: typeof meta.cover === 'string'
? meta.cover.trim()
: typeof meta.image === 'string'
? meta.image.trim()
: '';
// 时间字段转化:严格按需求约定“一旦转换错漏或是全无,采用目前时刻填满”
const published_at = parseOrCurrentDate(meta.date || meta.published_at);
const updated_at = parseOrCurrentDate(
meta.lastmod || meta.updated_at || meta.date || meta.published_at
);
return {
title: title || '无名小贴',
slug,
content: bodyContent,
cover,
published_at,
updated_at,
categories: parseArrayField(meta.categories || meta.category),
tags: parseArrayField(meta.tags || meta.tag),
series: parseArrayField(meta.series),
series_weight: parseIntegerField(meta.series_weight)
};
}
-115
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import type { PageData, ActionData } from './$types'; import type { PageData, ActionData } from './$types';
@@ -9,7 +8,6 @@
import MarkdownEditor from '$lib/components/admin/MarkdownEditor.svelte'; import MarkdownEditor from '$lib/components/admin/MarkdownEditor.svelte';
import FeaturedImagePicker from '$lib/components/admin/FeaturedImagePicker.svelte'; import FeaturedImagePicker from '$lib/components/admin/FeaturedImagePicker.svelte';
import SelectDropdown from '$lib/components/admin/SelectDropdown.svelte'; import SelectDropdown from '$lib/components/admin/SelectDropdown.svelte';
import { parseHugoMarkdown } from '$lib/hugoParser';
import TextInput from '$lib/components/admin/TextInput.svelte'; import TextInput from '$lib/components/admin/TextInput.svelte';
import Button from '$lib/components/admin/Button.svelte'; import Button from '$lib/components/admin/Button.svelte';
import Alert from '$lib/components/admin/Alert.svelte'; import Alert from '$lib/components/admin/Alert.svelte';
@@ -28,7 +26,6 @@
let selectedCategoryId = $state<number | string>(initialData.post.category_id ?? ''); let selectedCategoryId = $state<number | string>(initialData.post.category_id ?? '');
let selectedSeriesId = $state<number | string>(initialData.post.series_id ?? ''); let selectedSeriesId = $state<number | string>(initialData.post.series_id ?? '');
let seriesWeight = $state<number | string>(initialData.post.series_weight ?? ''); let seriesWeight = $state<number | string>(initialData.post.series_weight ?? '');
let fileInput: HTMLInputElement;
let selectedTags = $state<number[]>(initialData.selectedTagIds || []); let selectedTags = $state<number[]>(initialData.selectedTagIds || []);
let tagsList = $state(initialData.tags || []); let tagsList = $state(initialData.tags || []);
let seriesList = $state(initialData.series || []); let seriesList = $state(initialData.series || []);
@@ -105,126 +102,14 @@
} }
} }
} }
async function handleImportMarkdown(e: Event) {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) return;
try {
const rawText = await file.text();
const imported = parseHugoMarkdown(rawText, file.name);
title = imported.title;
slug = imported.slug;
content = imported.content;
if (imported.cover) cover = imported.cover;
publishedAt = imported.published_at;
updatedAt = imported.updated_at;
// 1. 分类处理:遭遇未知新分类名称时跳过,保留由用户抉择或处理
if (imported.categories.length > 0) {
const targetName = imported.categories[0].trim().toLowerCase();
const matchedCat = data.categories.find((c) => c.name.trim().toLowerCase() === targetName);
if (matchedCat) {
selectedCategoryId = matchedCat.id;
}
}
selectedSeriesId = '';
seriesWeight = imported.series_weight ?? '';
if (imported.series.length > 0) {
if (imported.series.length > 1) {
alert(
`检测到多个系列:${imported.series.join('、')}。BlogFlare 每篇文章只能属于一个系列,本次将选择第一个系列。`
);
}
const seriesName = imported.series[0].trim();
let matchedSeries = seriesList.find(
(item) => item.name.trim().toLowerCase() === seriesName.toLowerCase()
);
if (!matchedSeries && seriesName) {
const formData = new FormData();
formData.append('name', seriesName);
const response = await fetch('/admin/series?/create', {
method: 'POST',
body: formData,
headers: { 'x-sveltekit-action': 'true' }
});
const actionResult = deserialize(await response.text()) as any;
if (actionResult.type === 'success' && actionResult.data?.series) {
const createdSeries = actionResult.data.series as {
id: number;
name: string;
slug: string;
};
matchedSeries = createdSeries;
seriesList = [...seriesList, createdSeries];
} else {
alert(
actionResult.data?.error ||
`无法创建系列“${seriesName}”,请在系列管理中检查名称和 URL 别名。`
);
}
}
if (matchedSeries) selectedSeriesId = matchedSeries.id;
}
// 3. 标签处理:同步点选或即刻新增入参
for (const tagName of imported.tags) {
const val = tagName.trim();
if (!val) continue;
const existingTag = tagsList.find((t) => t.name.toLowerCase() === val.toLowerCase());
if (existingTag) {
if (!selectedTags.includes(existingTag.id)) {
selectedTags = [...selectedTags, existingTag.id];
}
} else {
try {
const formData = new FormData();
formData.append('name', val);
const res = await fetch('/admin/tags?/create', {
method: 'POST',
body: formData,
headers: { 'x-sveltekit-action': 'true' }
});
const actionRes = deserialize(await res.text()) as any;
if (actionRes.type === 'success' && actionRes.data?.tag) {
tagsList = [...tagsList, actionRes.data.tag];
selectedTags = [...selectedTags, actionRes.data.tag.id];
}
} catch (err: any) {
console.error('自动拓展建构新标签错付:', err);
}
}
}
target.value = '';
} catch (err: any) {
console.error('导入报错:');
alert('文档内部解析受断,核验是否带有完整及恰当规范的内容格式');
}
}
</script> </script>
<div class="admin-page-header"> <div class="admin-page-header">
<h2>编辑</h2> <h2>编辑</h2>
<div class="header-actions"> <div class="header-actions">
<Button type="button" variant="secondary" onclick={() => fileInput?.click()}>
导入 Markdown
</Button>
<Button href={resolve('/admin/posts')} variant="secondary">&larr; 返回</Button> <Button href={resolve('/admin/posts')} variant="secondary">&larr; 返回</Button>
</div> </div>
</div> </div>
<input
type="file"
bind:this={fileInput}
accept=".md,.markdown,.txt"
style="display:none"
onchange={handleImportMarkdown}
/>
{#if form?.error} {#if form?.error}
<Alert message={form.error} /> <Alert message={form.error} />
-114
View File
@@ -9,7 +9,6 @@
import MarkdownEditor from '$lib/components/admin/MarkdownEditor.svelte'; import MarkdownEditor from '$lib/components/admin/MarkdownEditor.svelte';
import FeaturedImagePicker from '$lib/components/admin/FeaturedImagePicker.svelte'; import FeaturedImagePicker from '$lib/components/admin/FeaturedImagePicker.svelte';
import SelectDropdown from '$lib/components/admin/SelectDropdown.svelte'; import SelectDropdown from '$lib/components/admin/SelectDropdown.svelte';
import { parseHugoMarkdown } from '$lib/hugoParser';
import TextInput from '$lib/components/admin/TextInput.svelte'; import TextInput from '$lib/components/admin/TextInput.svelte';
import Button from '$lib/components/admin/Button.svelte'; import Button from '$lib/components/admin/Button.svelte';
import Alert from '$lib/components/admin/Alert.svelte'; import Alert from '$lib/components/admin/Alert.svelte';
@@ -30,7 +29,6 @@
let selectedCategoryId = $state<string | number>(''); let selectedCategoryId = $state<string | number>('');
let selectedSeriesId = $state<string | number>(''); let selectedSeriesId = $state<string | number>('');
let seriesWeight = $state<string | number>(''); let seriesWeight = $state<string | number>('');
let fileInput: HTMLInputElement;
let tagsList = $state(initialData.tags || []); let tagsList = $state(initialData.tags || []);
let seriesList = $state(initialData.series || []); let seriesList = $state(initialData.series || []);
@@ -122,126 +120,14 @@
} }
} }
} }
async function handleImportMarkdown(e: Event) {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) return;
try {
const rawText = await file.text();
const imported = parseHugoMarkdown(rawText, file.name);
title = imported.title;
slug = imported.slug;
content = imported.content;
if (imported.cover) cover = imported.cover;
publishedAt = imported.published_at;
updatedAt = imported.updated_at;
// 1. 分类匹配:严格遵守“遇到全新未知的分类名跳过,由用户决定”的规范
if (imported.categories.length > 0) {
const targetName = imported.categories[0].trim().toLowerCase();
const matchedCat = data.categories.find((c) => c.name.trim().toLowerCase() === targetName);
if (matchedCat) {
selectedCategoryId = matchedCat.id;
}
}
selectedSeriesId = '';
seriesWeight = imported.series_weight ?? '';
if (imported.series.length > 0) {
if (imported.series.length > 1) {
alert(
`检测到多个系列:${imported.series.join('、')}。BlogFlare 每篇文章只能属于一个系列,本次将选择第一个系列。`
);
}
const seriesName = imported.series[0].trim();
let matchedSeries = seriesList.find(
(item) => item.name.trim().toLowerCase() === seriesName.toLowerCase()
);
if (!matchedSeries && seriesName) {
const formData = new FormData();
formData.append('name', seriesName);
const response = await fetch('/admin/series?/create', {
method: 'POST',
body: formData,
headers: { 'x-sveltekit-action': 'true' }
});
const actionResult = deserialize(await response.text()) as any;
if (actionResult.type === 'success' && actionResult.data?.series) {
const createdSeries = actionResult.data.series as {
id: number;
name: string;
slug: string;
};
matchedSeries = createdSeries;
seriesList = [...seriesList, createdSeries];
} else {
alert(
actionResult.data?.error ||
`无法创建系列“${seriesName}”,请在系列管理中检查名称和 URL 别名。`
);
}
}
if (matchedSeries) selectedSeriesId = matchedSeries.id;
}
// 3. 标签匹配:库有即点取,不知则自主申请向库内补充建好
for (const tagName of imported.tags) {
const val = tagName.trim();
if (!val) continue;
const existingTag = tagsList.find((t) => t.name.toLowerCase() === val.toLowerCase());
if (existingTag) {
if (!selectedTags.includes(existingTag.id)) {
selectedTags = [...selectedTags, existingTag.id];
}
} else {
try {
const formData = new FormData();
formData.append('name', val);
const res = await fetch('/admin/tags?/create', {
method: 'POST',
body: formData,
headers: { 'x-sveltekit-action': 'true' }
});
const actionRes = deserialize(await res.text()) as any;
if (actionRes.type === 'success' && actionRes.data?.tag) {
tagsList = [...tagsList, actionRes.data.tag];
selectedTags = [...selectedTags, actionRes.data.tag.id];
}
} catch (err: any) {
console.error('新建导入对应新标签时出错:', err);
}
}
}
target.value = '';
} catch (err: any) {
console.error('导入转换出现错误:');
alert('解析文档内容受困,请稍后检查所填文书');
}
}
</script> </script>
<div class="admin-page-header"> <div class="admin-page-header">
<h2>新{$page.data.aliases?.post || '文章'}</h2> <h2>新{$page.data.aliases?.post || '文章'}</h2>
<div class="header-actions"> <div class="header-actions">
<Button type="button" variant="secondary" onclick={() => fileInput?.click()}>
导入 Markdown
</Button>
<Button href={resolve('/admin/posts')} variant="secondary">&larr; 返回</Button> <Button href={resolve('/admin/posts')} variant="secondary">&larr; 返回</Button>
</div> </div>
</div> </div>
<input
type="file"
bind:this={fileInput}
accept=".md,.markdown,.txt"
style="display:none"
onchange={handleImportMarkdown}
/>
{#if form?.error} {#if form?.error}
<Alert message={form.error} /> <Alert message={form.error} />