mirror of
https://codeberg.org/HPCesia/AstralHalo.git
synced 2025-04-08 17:34:27 +08:00
fix: bidirectional references in spec pages
This commit is contained in:
parent
a3ae7b72a5
commit
6d41ef585a
@ -1,11 +1,14 @@
|
||||
---
|
||||
import I18nKey from '@i18n/I18nKey';
|
||||
import { i18n } from '@i18n/translation';
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
|
||||
interface Props {
|
||||
backLinks: {
|
||||
reference: CollectionEntry<'posts'>;
|
||||
refBy: {
|
||||
title: string;
|
||||
collection: 'posts' | 'spec';
|
||||
id: string;
|
||||
};
|
||||
context: string;
|
||||
offset: [number, number];
|
||||
id: string;
|
||||
@ -21,20 +24,23 @@ const { backLinks } = Astro.props;
|
||||
<div class="collapse-content">
|
||||
<ul class="list">
|
||||
{
|
||||
backLinks.map(({ reference: { data }, context, id, offset }) => (
|
||||
<li class="list-row">
|
||||
<a href={`/posts/${data.slug}#wiki-${id}`} title={data.title} class="list-col-grow">
|
||||
<span class="text-base font-bold">{data.title}</span>
|
||||
<div class="text-base-content/60 mt-2 flex flex-wrap items-start gap-x-4 gap-y-2 text-sm">
|
||||
<span>
|
||||
{context.slice(0, offset[0])}
|
||||
<strong class="text-primary">{context.slice(offset[0], offset[1])}</strong>
|
||||
{context.slice(offset[1])}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
backLinks.map(({ refBy, context, id, offset }) => {
|
||||
const url = refBy.collection === 'posts' ? `/posts/${refBy.id}` : `/${refBy.id}`;
|
||||
return (
|
||||
<li class="list-row">
|
||||
<a href={`${url}#wiki-${id}`} title={refBy.title} class="list-col-grow">
|
||||
<span class="text-base font-bold">{refBy.title}</span>
|
||||
<div class="text-base-content/60 mt-2 flex flex-wrap items-start gap-x-4 gap-y-2 text-sm">
|
||||
<span>
|
||||
{context.slice(0, offset[0])}
|
||||
<strong class="text-primary">{context.slice(offset[0], offset[1])}</strong>
|
||||
{context.slice(offset[1])}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -1,12 +1,52 @@
|
||||
---
|
||||
import '@/styles/markdown.css';
|
||||
import type { HTMLAttributes } from 'astro/types';
|
||||
import Replacer from './Replacer.astro';
|
||||
|
||||
type Props = HTMLAttributes<'article'>;
|
||||
interface Props extends HTMLAttributes<'article'> {
|
||||
'bidirectional-references'?: {
|
||||
references: {
|
||||
reference: string;
|
||||
context: string;
|
||||
id: string;
|
||||
}[];
|
||||
allRefByCurrent: {
|
||||
refTo: {
|
||||
title: string;
|
||||
collection: 'posts' | 'spec';
|
||||
id: string;
|
||||
};
|
||||
context: string;
|
||||
offset: [number, number];
|
||||
id: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
const { class: className, ...rest } = Astro.props;
|
||||
const {
|
||||
class: className,
|
||||
'bidirectional-references': bidirectionalReferences,
|
||||
...rest
|
||||
} = Astro.props;
|
||||
|
||||
const references = bidirectionalReferences?.references;
|
||||
const allRefByCurrent = bidirectionalReferences?.allRefByCurrent;
|
||||
const replacer = (_: string, reference: string, alias: string) => {
|
||||
const id = references?.find((item) => item.reference === reference)?.id;
|
||||
if (!id) return '';
|
||||
const refTo = allRefByCurrent?.find((it) => it.id === id);
|
||||
if (!refTo) return '';
|
||||
const url =
|
||||
refTo.refTo.collection === 'posts' ? `/posts/${refTo.refTo.id}/` : `/${refTo.refTo.id}/`;
|
||||
return `<a href="${url}" id="wiki-${id}">${alias || reference}</a>`;
|
||||
};
|
||||
const referencePattern = /%%%%(.*?)(?:%%(.*?))?%%%%/g;
|
||||
|
||||
const Fragment = bidirectionalReferences ? Replacer : 'Fragment';
|
||||
---
|
||||
|
||||
<article class={className} {...rest}>
|
||||
<slot />
|
||||
<Fragment pattern={referencePattern} replacer={replacer}>
|
||||
<slot />
|
||||
</Fragment>
|
||||
</article>
|
||||
|
@ -4,20 +4,48 @@ import Markdown from '@components/utils/Markdown.astro';
|
||||
import I18nKey from '@i18n/I18nKey';
|
||||
import { i18n } from '@i18n/translation';
|
||||
import PostPageLayout from '@layouts/PostPageLayout.astro';
|
||||
import { getAllReferences } from '@utils/content-utils';
|
||||
import { getEntry, render } from 'astro:content';
|
||||
|
||||
const aboutMd = await getEntry('spec', 'about');
|
||||
const { Content } = aboutMd ? await render(aboutMd) : Fragment;
|
||||
const md = await getEntry('spec', 'about');
|
||||
|
||||
const { Content, headings, remarkPluginFrontmatter } = md
|
||||
? await render(md)
|
||||
: {
|
||||
Content: Fragment,
|
||||
headings: [],
|
||||
remarkPluginFrontmatter: { references: [] },
|
||||
};
|
||||
|
||||
const allReferences = await getAllReferences();
|
||||
let allRefByCurrent: typeof allReferences = [];
|
||||
let references: {
|
||||
reference: string;
|
||||
context: string;
|
||||
id: string;
|
||||
}[] = [];
|
||||
if (md) {
|
||||
allRefByCurrent = allReferences.filter((it) => it.refBy.id === md.id);
|
||||
references = remarkPluginFrontmatter.references || [];
|
||||
}
|
||||
---
|
||||
|
||||
<PostPageLayout
|
||||
title={aboutMd?.data.title || (i18n(I18nKey.about) as string)}
|
||||
comment={aboutMd?.data.comment}
|
||||
title={md?.data.title || (i18n(I18nKey.about) as string)}
|
||||
headings={headings}
|
||||
comment={md?.data.comment}
|
||||
>
|
||||
<Fragment slot="header-content">
|
||||
<PostInfo title={i18n(I18nKey.about) as string} />
|
||||
<PostInfo title={md?.data.title || (i18n(I18nKey.about) as string)} />
|
||||
</Fragment>
|
||||
<Markdown>
|
||||
<Markdown
|
||||
bidirectional-references={md
|
||||
? {
|
||||
references,
|
||||
allRefByCurrent,
|
||||
}
|
||||
: undefined}
|
||||
>
|
||||
<Content />
|
||||
</Markdown>
|
||||
</PostPageLayout>
|
||||
|
@ -6,22 +6,41 @@ import Markdown from '@components/utils/Markdown.astro';
|
||||
import I18nKey from '@i18n/I18nKey';
|
||||
import { i18n } from '@i18n/translation';
|
||||
import PostPageLayout from '@layouts/PostPageLayout.astro';
|
||||
import { getAllReferences } from '@utils/content-utils';
|
||||
import { getEntry, render } from 'astro:content';
|
||||
|
||||
const linksMd = await getEntry('spec', 'links');
|
||||
const md = await getEntry('spec', 'links');
|
||||
|
||||
const { Content, headings, remarkPluginFrontmatter } = md
|
||||
? await render(md)
|
||||
: {
|
||||
Content: Fragment,
|
||||
headings: [],
|
||||
remarkPluginFrontmatter: { references: [] },
|
||||
};
|
||||
|
||||
const allReferences = await getAllReferences();
|
||||
let allRefByCurrent: typeof allReferences = [];
|
||||
let references: {
|
||||
reference: string;
|
||||
context: string;
|
||||
id: string;
|
||||
}[] = [];
|
||||
if (md) {
|
||||
allRefByCurrent = allReferences.filter((it) => it.refBy.id === md.id);
|
||||
references = remarkPluginFrontmatter.references || [];
|
||||
}
|
||||
|
||||
const groupHeadings = linksConfig.items.map((item) => ({
|
||||
depth: 2,
|
||||
slug: `links-group-${item.groupName.toLowerCase().replace(/\s/g, '-')}`,
|
||||
text: item.groupName,
|
||||
}));
|
||||
const { Content, headings } = linksMd
|
||||
? await render(linksMd)
|
||||
: { Content: Fragment, headings: [] };
|
||||
---
|
||||
|
||||
<PostPageLayout
|
||||
title={i18n(I18nKey.links) as string}
|
||||
comment={linksMd?.data.comment}
|
||||
comment={md?.data.comment}
|
||||
headings={[...groupHeadings, ...headings]}
|
||||
>
|
||||
<Fragment slot="header-content">
|
||||
@ -77,7 +96,14 @@ const { Content, headings } = linksMd
|
||||
))
|
||||
}
|
||||
<hr class="text-base-content/25 mt-8" />
|
||||
<Markdown>
|
||||
<Markdown
|
||||
bidirectional-references={md
|
||||
? {
|
||||
references,
|
||||
allRefByCurrent,
|
||||
}
|
||||
: undefined}
|
||||
>
|
||||
<Content />
|
||||
</Markdown>
|
||||
</PostPageLayout>
|
||||
|
@ -1,17 +1,16 @@
|
||||
---
|
||||
import { buildConfig, siteConfig } from '@/config';
|
||||
import { siteConfig } from '@/config';
|
||||
import BackLinks from '@components/misc/BackLinks.astro';
|
||||
import License from '@components/misc/License.astro';
|
||||
import PostInfo from '@components/misc/PostInfo.astro';
|
||||
import ImageWrapper from '@components/utils/ImageWrapper.astro';
|
||||
import Markdown from '@components/utils/Markdown.astro';
|
||||
import Replacer from '@components/utils/Replacer.astro';
|
||||
import I18nKey from '@i18n/I18nKey';
|
||||
import { i18n } from '@i18n/translation';
|
||||
import PostPageLayout from '@layouts/PostPageLayout.astro';
|
||||
import { getPosts } from '@utils/content-utils';
|
||||
import { getAllReferences, getPosts } from '@utils/content-utils';
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { render, type CollectionEntry } from 'astro:content';
|
||||
import { render } from 'astro:content';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
@ -29,79 +28,26 @@ const coverSrc =
|
||||
const description = article.data.description || remarkPluginFrontmatter.excerpt;
|
||||
const isDraft = article.data.draft === true;
|
||||
|
||||
const referencePattern = /%%%%(.*?)(?:%%(.*?))?%%%%/g;
|
||||
const allPosts = await getPosts();
|
||||
const pathIdMap = allPosts.reduce(
|
||||
(acc, post) => {
|
||||
const slug = post.id;
|
||||
const path = post.filePath!.replace('src/content/', '').split('.').slice(0, -1).join('.');
|
||||
acc[path] = slug;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
const getArticle = (refPath: string) => {
|
||||
let collection = refPath.split('/')[0];
|
||||
if (!['posts', 'drafts', 'spec'].includes(collection)) {
|
||||
collection = 'posts';
|
||||
refPath = `posts/${refPath}`;
|
||||
}
|
||||
const id = pathIdMap[refPath];
|
||||
if (id) {
|
||||
return allPosts.find((post) => post.id === id);
|
||||
}
|
||||
};
|
||||
const allReferences = await getAllReferences();
|
||||
const allRefByCurrent = allReferences.filter((it) => it.refBy.id === article.id);
|
||||
const allRefToCurrent = allReferences.filter((it) => it.refTo.id === article.id);
|
||||
|
||||
const references: {
|
||||
reference: string;
|
||||
context: string;
|
||||
id: string;
|
||||
}[] = remarkPluginFrontmatter.references || [];
|
||||
const replacer = (_: string, reference: string, alias: string) => {
|
||||
const refArticle = getArticle(reference);
|
||||
if (
|
||||
refArticle &&
|
||||
(refArticle.data.draft === false || (buildConfig.showDraftsOnDev && import.meta.env.DEV))
|
||||
) {
|
||||
const url = `/posts/${refArticle.id}`;
|
||||
const id = references.find((item) => item.reference === reference)?.id;
|
||||
return `<a href="${url}" id="wiki-${id}">${alias || reference}</a>`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const backLinks: {
|
||||
reference: CollectionEntry<'posts'>;
|
||||
refBy: {
|
||||
title: string;
|
||||
collection: 'posts' | 'spec';
|
||||
id: string;
|
||||
};
|
||||
context: string;
|
||||
offset: [number, number];
|
||||
id: string;
|
||||
}[] = (
|
||||
await Promise.all(
|
||||
allPosts.map(async (post) => {
|
||||
const { remarkPluginFrontmatter } = await render(post);
|
||||
const references: {
|
||||
reference: string;
|
||||
context: string;
|
||||
offset: [number, number];
|
||||
id: string;
|
||||
}[] = remarkPluginFrontmatter.references || [];
|
||||
return references
|
||||
.map((reference) => {
|
||||
const refArticle = getArticle(reference.reference);
|
||||
if (refArticle && refArticle.id === article.id) {
|
||||
return {
|
||||
reference: post,
|
||||
context: reference.context,
|
||||
offset: reference.offset,
|
||||
id: reference.id,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter((item) => item !== undefined);
|
||||
})
|
||||
)
|
||||
).flat();
|
||||
}[] = allRefToCurrent;
|
||||
---
|
||||
|
||||
<PostPageLayout
|
||||
@ -129,7 +75,12 @@ const backLinks: {
|
||||
<ImageWrapper src={coverSrc!} class="mb-6 rounded-xl shadow" alt={article.data.title} />
|
||||
)
|
||||
}
|
||||
<Markdown>
|
||||
<Markdown
|
||||
bidirectional-references={{
|
||||
references,
|
||||
allRefByCurrent,
|
||||
}}
|
||||
>
|
||||
{
|
||||
isDraft && (
|
||||
<div class="admonition admonition-note">
|
||||
@ -141,9 +92,7 @@ const backLinks: {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Replacer pattern={referencePattern} replacer={replacer}>
|
||||
<Content />
|
||||
</Replacer>
|
||||
<Content />
|
||||
</Markdown>
|
||||
<License time={article.data.published} lang={article.data.lang} />
|
||||
{backLinks.length > 0 && <BackLinks backLinks={backLinks} />}
|
||||
|
@ -80,3 +80,146 @@ export function getTagUrl(tag: string) {
|
||||
? `/archives/tags/${I18nKey.untagged}/1`
|
||||
: `/archives/tags/${tag.replaceAll(/[\\/]/g, '-')}/1`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有的引用,返回一个数组
|
||||
*/
|
||||
export async function getAllReferences() {
|
||||
type frontmatterRef = {
|
||||
/** 引用的文章,即 [[ref|alias]] 中的 ref 部分 */
|
||||
reference: string;
|
||||
/** 引用的上下文,即整个 xxxx[[ref|alias]]xxxx 的内容,截取前后 20 个字符 */
|
||||
context: string;
|
||||
/** 引用的偏移量,即 [[ref|alias]] 在上下文字符串中的起始和结束位置 */
|
||||
offset: [number, number];
|
||||
/** 引用应该被分配的 id,用于通过 #id 跳转 */
|
||||
id: string;
|
||||
};
|
||||
type Article = {
|
||||
/** 文章的标题 */
|
||||
title: string;
|
||||
/** 文章的集合,posts 或 spec,drafts 会被处理为 posts */
|
||||
collection: 'posts' | 'spec';
|
||||
/** 文章的 id,对 posts 来说是 slug 参数,对 spec 来说是文件名 */
|
||||
id: string;
|
||||
};
|
||||
|
||||
const posts = await getPosts();
|
||||
const specs = await getCollection('spec');
|
||||
|
||||
const pathMap = [
|
||||
...posts.map((it) => ({
|
||||
id: it.id,
|
||||
title: it.data.title,
|
||||
collection: it.collection,
|
||||
filePath: it.filePath,
|
||||
})),
|
||||
...specs.map((it) => ({
|
||||
id: it.id,
|
||||
title: it.data.title,
|
||||
collection: it.collection,
|
||||
filePath: it.filePath,
|
||||
})),
|
||||
].reduce(
|
||||
(acc, it) => {
|
||||
const path = it.filePath!.replace('src/content/', '').split('.').slice(0, -1).join('.');
|
||||
acc[path] = {
|
||||
id: it.id,
|
||||
title: it.title || path.split('/').slice(-1)[-1],
|
||||
collection: it.collection,
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as Record<
|
||||
string,
|
||||
{
|
||||
id: string;
|
||||
title: string;
|
||||
collection: 'posts' | 'spec';
|
||||
}
|
||||
>
|
||||
);
|
||||
|
||||
const postsRefData = posts.map(async (post) => {
|
||||
const { remarkPluginFrontmatter } = await render(post);
|
||||
const references: frontmatterRef[] = remarkPluginFrontmatter.references || [];
|
||||
return {
|
||||
title: post.data.title,
|
||||
colletion: 'posts',
|
||||
id: post.id,
|
||||
references,
|
||||
};
|
||||
});
|
||||
const specRefData = specs.map(async (spec) => {
|
||||
const { remarkPluginFrontmatter } = await render(spec);
|
||||
const references: frontmatterRef[] = remarkPluginFrontmatter.references || [];
|
||||
return {
|
||||
title: spec.data.title || spec.filePath?.split('/').slice(-1)[0],
|
||||
colletion: 'spec',
|
||||
id: spec.id,
|
||||
references,
|
||||
};
|
||||
});
|
||||
|
||||
const getArticle = (refPath: string): Article | null => {
|
||||
let collection = refPath.split('/')[0];
|
||||
if (!['posts', 'drafts', 'spec'].includes(collection)) {
|
||||
collection = 'posts';
|
||||
refPath = `posts/${refPath}`;
|
||||
}
|
||||
const { id, title } = pathMap[refPath];
|
||||
if (id) {
|
||||
if (collection === 'spec') {
|
||||
const article = specs.find((it) => it.id === id);
|
||||
if (article) return { title, collection, id };
|
||||
} else {
|
||||
const article = posts.find((it) => it.id === id);
|
||||
if (article) return { title, collection: 'posts', id };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const references: {
|
||||
refBy: Article;
|
||||
refTo: Article;
|
||||
/** 引用的上下文,即整个 xxxx[[ref|alias]]xxxx 的内容,截取前后 20 个字符 */
|
||||
context: string;
|
||||
/** 引用的偏移量,即 [[ref|alias]] 在上下文字符串中的起始和结束位置 */
|
||||
offset: [number, number];
|
||||
/** 引用应该被分配的 id,用于通过 #id 跳转 */
|
||||
id: string;
|
||||
}[] = [
|
||||
...(await Promise.all(postsRefData)).flatMap((data) => {
|
||||
const article: Article = {
|
||||
title: data.title,
|
||||
collection: data.colletion as 'posts' | 'spec',
|
||||
id: data.id,
|
||||
};
|
||||
return data.references
|
||||
.map((ref) => {
|
||||
const { reference, context, offset, id } = ref;
|
||||
const refTo = getArticle(reference);
|
||||
if (refTo) return { refBy: article, refTo, context, offset, id };
|
||||
return null;
|
||||
})
|
||||
.filter((it) => it !== null);
|
||||
}),
|
||||
...(await Promise.all(specRefData)).flatMap((data) => {
|
||||
const article: Article = {
|
||||
title: data.title || data.id,
|
||||
collection: data.colletion as 'posts' | 'spec',
|
||||
id: data.id,
|
||||
};
|
||||
return data.references
|
||||
.map((ref) => {
|
||||
const { reference, context, offset, id } = ref;
|
||||
const refTo = getArticle(reference);
|
||||
if (refTo) return { refBy: article, refTo, context, offset, id };
|
||||
return null;
|
||||
})
|
||||
.filter((it) => it !== null);
|
||||
}),
|
||||
];
|
||||
return references;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user