Commit 6d41ef5
Changed files (6)
src
components
misc
utils
pages
utils
src/components/misc/BackLinks.astro
@@ -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>
src/components/utils/Markdown.astro
@@ -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>
src/pages/posts/[article].astro
@@ -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} />}
src/pages/about.astro
@@ -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>
src/pages/links.astro
@@ -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>
src/utils/content-utils.ts
@@ -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;
+}