Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/node_modules
/.pnp
.pnp.js
package-lock.json

# testing
/coverage
Expand Down
2 changes: 1 addition & 1 deletion components/Git/Issue/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const IssueCard: FC<IssueCardProps> = ({
</Card.Header>
<Card.Body
as="article"
dangerouslySetInnerHTML={{ __html: marked(body || '') }}
dangerouslySetInnerHTML={{ __html: marked(body || '') as string }}
/>
<Card.Footer className="d-flex justify-content-between align-items-center">
{user && <Nameplate name={user.name || ''} avatar={user.avatar_url} />}
Expand Down
11 changes: 11 additions & 0 deletions models/Wiki.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Content, ContentModel } from 'mobx-github';
import { DataObject } from 'mobx-restful';

import './Base';

export interface XContent extends Content {
meta?: DataObject;
children?: XContent[];
}

export const policyContentStore = new ContentModel('fpsig', 'open-source-policy');
3 changes: 1 addition & 2 deletions models/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ export const LARK_API_HOST = `${API_Host}/api/Lark/`;

export const ProxyBaseURL = 'https://bazaar.fcc-cd.dev/proxy';

export const GithubToken =
(globalThis.document && parseCookie().token) || process.env.GITHUB_TOKEN;
export const GithubToken = (globalThis.document && parseCookie().token) || process.env.GH_PAT;

export const LarkAppMeta = {
host: process.env.NEXT_PUBLIC_LARK_API_HOST,
Expand Down
24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,32 @@
"@koa/router": "^14.0.0",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@next/mdx": "^15.5.0",
"@next/mdx": "^15.5.2",
"core-js": "^3.45.1",
"file-type": "^21.0.0",
"idea-react": "^2.0.0-rc.13",
"koa": "^3.0.1",
"koajax": "^3.1.2",
"license-filter": "^0.2.5",
"marked": "^16.2.0",
"marked": "^16.2.1",
"mime": "^4.0.7",
"mobx": "^6.13.7",
"mobx-github": "^0.3.11",
"mobx-github": "^0.4.0",
"mobx-i18n": "^0.7.1",
"mobx-lark": "^2.4.0",
"mobx-react": "^9.2.0",
"mobx-react-helper": "^0.5.1",
"mobx-restful": "^2.1.0",
"mobx-restful-table": "^2.5.3",
"next": "^15.5.0",
"next": "^15.5.2",
"next-pwa": "^5.6.0",
"next-ssr-middleware": "^1.0.2",
"react": "^19.1.1",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.1.1",
"react-typed-component": "^1.0.6",
"undici": "^7.15.0",
"web-utility": "^4.5.1",
"web-utility": "^4.5.3",
"yaml": "^2.8.1"
},
"devDependencies": {
Expand All @@ -49,18 +49,18 @@
"@babel/preset-react": "^7.27.1",
"@cspell/eslint-plugin": "^9.2.0",
"@eslint/js": "^9.34.0",
"@next/eslint-plugin-next": "^15.5.0",
"@next/eslint-plugin-next": "^15.5.2",
"@softonus/prettier-plugin-duplicate-remover": "^1.1.2",
"@stylistic/eslint-plugin": "^5.2.3",
"@types/eslint-config-prettier": "^6.11.3",
"@types/koa": "^3.0.0",
"@types/koa__router": "^12.0.4",
"@types/next-pwa": "^5.6.9",
"@types/node": "^22.17.2",
"@types/react": "^19.1.11",
"@types/react-dom": "^19.1.7",
"@types/node": "^22.18.0",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"eslint": "^9.34.0",
"eslint-config-next": "^15.5.0",
"eslint-config-next": "^15.5.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-simple-import-sort": "^12.1.1",
Expand All @@ -73,9 +73,9 @@
"next-with-less": "^3.0.1",
"prettier": "^3.6.2",
"prettier-plugin-css-order": "^2.1.2",
"sass": "^1.90.0",
"sass": "^1.91.0",
"typescript": "~5.9.2",
"typescript-eslint": "^8.40.0"
"typescript-eslint": "^8.41.0"
},
"resolutions": {
"next": "$next"
Expand Down
4 changes: 4 additions & 0 deletions pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export default class CustomDocument extends Document<CustomDocumentProps> {
rel="stylesheet"
href="https://unpkg.com/bootstrap-icons@1.13.1/font/bootstrap-icons.css"
/>
<link
rel="stylesheet"
href="https://unpkg.com/github-markdown-css@5.8.1/github-markdown.css"
/>
</Head>

<body>
Expand Down
21 changes: 10 additions & 11 deletions pages/api/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,31 +58,28 @@ export interface ArticleMeta {
subs: ArticleMeta[];
}

const MDX_pattern = /\.mdx?$/;

export async function splitFrontMatter(path: string) {
const { readFile } = await import('fs/promises');

const file = await readFile(path, 'utf-8');
export const MD_pattern = /\.(md|markdown)$/i,
MDX_pattern = /\.mdx?$/i;

export function splitFrontMatter(raw: string) {
const [, frontMatter, markdown] =
file.trim().match(/^---[\r\n]([\s\S]+?[\r\n])---[\r\n]([\s\S]*)/) || [];
raw.trim().match(/^---[\r\n]([\s\S]+?[\r\n])---[\r\n]([\s\S]*)/) || [];

if (!frontMatter) return { markdown: file };
if (!frontMatter) return { markdown: raw };

try {
const meta = parse(frontMatter) as DataObject;

return { markdown, meta };
} catch (error) {
console.error(`Error reading front matter for ${path}:`, error);
console.error(`Error parsing Front Matter:`, error);

return { markdown };
}
}

export async function* pageListOf(path: string, prefix = 'pages'): AsyncGenerator<ArticleMeta> {
const { readdir } = await import('fs/promises');
const { readdir, readFile } = await import('fs/promises');

const list = await readdir(prefix + path, { withFileTypes: true });

Expand All @@ -99,7 +96,9 @@ export async function* pageListOf(path: string, prefix = 'pages'): AsyncGenerato
if (node.isFile() && isMDX) {
const article: ArticleMeta = { name, path, subs: [] };

const { meta } = await splitFrontMatter(`${node.path}/${node.name}`);
const file = await readFile(`${node.path}/${node.name}`, 'utf-8');

const { meta } = splitFrontMatter(file);

if (meta) article.meta = meta;

Expand Down
149 changes: 120 additions & 29 deletions pages/wiki/[...slug].tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,142 @@
import { marked } from 'marked';
import { DataObject } from 'mobx-restful';
import { GetStaticPaths, GetStaticProps, NextPage } from 'next';
import { GetStaticPaths, GetStaticProps } from 'next';
import { ParsedUrlQuery } from 'querystring';
import { FC } from 'react';
import { Badge, Breadcrumb, Button, Container } from 'react-bootstrap';
import { decodeBase64 } from 'web-utility';

import { pageListOf, splitFrontMatter, traverseTree } from '../api/core';
import { PageHead } from '../../components/Layout/PageHead';
import { policyContentStore, XContent } from '../../models/Wiki';
import { splitFrontMatter } from '../api/core';

interface WikiPageParams extends ParsedUrlQuery {
slug: string[];
}

export const getStaticPaths: GetStaticPaths<WikiPageParams> = async () => {
const tree = await Array.fromAsync(pageListOf('wiki', 'public/'));
const list = tree.map(root => [...traverseTree(root, 'subs')]).flat();
const paths = list
.map(({ path }) => path && { params: { slug: path.split('/') } })
.filter(Boolean) as { params: WikiPageParams }[];
const nodes = await policyContentStore.getAll();

const paths = nodes
.filter(({ type }) => type === 'file')
.map(({ path }) => ({ params: { slug: path.split('/') } }));

return { paths, fallback: 'blocking' };
};

interface WikiPageProps {
meta?: DataObject;
markup: string;
}

export const getStaticProps: GetStaticProps<WikiPageProps, WikiPageParams> = async ({ params }) => {
export const getStaticProps: GetStaticProps<XContent, WikiPageParams> = async ({ params }) => {
const { slug } = params!;
// https://github.com/vercel/next.js/issues/12851
if (slug[0] !== 'wiki') slug.unshift('wiki');

const { meta, markdown } = await splitFrontMatter(`public/${slug.join('/')}.md`);
const node = await policyContentStore.getOne(slug.join('/'));

const { meta, markdown } = splitFrontMatter(decodeBase64(node.content!));

const markup = marked(markdown) as string;

return { props: JSON.parse(JSON.stringify({ meta, markup })) };
return {
props: JSON.parse(JSON.stringify({ ...node, content: markup, meta })),
revalidate: 300, // Revalidate every 5 minutes
};
};

const WikiPage: NextPage<WikiPageProps> = ({ meta, markup }) => (
<>
{meta && (
<blockquote>
<a target="_blank" href={meta.url} rel="noreferrer">
{meta.url}
</a>
</blockquote>
)}
<article dangerouslySetInnerHTML={{ __html: markup }} />
</>
const WikiPage: FC<XContent> = ({ name, path, parent_path, content, meta }) => (
<Container className="py-4">
<PageHead title={name} />

<Breadcrumb className="mb-4">
<Breadcrumb.Item href="/wiki">Wiki</Breadcrumb.Item>

{parent_path?.split('/').map((segment, index, array) => {
const breadcrumbPath = array.slice(0, index + 1).join('/');

return (
<Breadcrumb.Item key={breadcrumbPath} href={`/wiki/${breadcrumbPath}`}>
{segment}
</Breadcrumb.Item>
);
})}
<Breadcrumb.Item active>{name}</Breadcrumb.Item>
</Breadcrumb>

<article>
<header className="mb-4">
<h1>{name}</h1>

{meta && (
<div className="d-flex flex-wrap align-items-center gap-3 mb-3">
<ul className="mb-0">
{meta['主题分类'] && (
<li>
<Badge bg="primary">{meta['主题分类']}</Badge>
</li>
)}
{meta['发文机构'] && (
<li>
<Badge bg="secondary">{meta['发文机构']}</Badge>
</li>
)}
{meta['有效性'] && (
<li>
<Badge bg={meta['有效性'] === '现行有效' ? 'success' : 'warning'}>
{meta['有效性']}
</Badge>
</li>
)}
</ul>
</div>
)}

<div className="d-flex justify-content-between align-items-center text-muted small mb-3">
<div>
{meta?.['成文日期'] && <span>成文日期: {meta['成文日期']}</span>}
{meta?.['发布日期'] && meta['发布日期'] !== meta['成文日期'] && (
<span className="ms-3">发布日期: {meta['发布日期']}</span>
)}
</div>

<div className="d-flex gap-2">
<Button
variant="outline-primary"
size="sm"
href={`https://github.com/fpsig/open-source-policy/blob/main/China/政策/${path}`}
target="_blank"
rel="noopener noreferrer"
>
在 GitHub 编辑
</Button>
{meta?.url && (
<Button
variant="outline-secondary"
size="sm"
href={meta.url}
target="_blank"
rel="noopener noreferrer"
>
查看原文
</Button>
)}
</div>
</div>
</header>

<div dangerouslySetInnerHTML={{ __html: content || '' }} className="markdown-body" />
</article>

<footer className="mt-5 pt-4 border-top">
<div className="text-center">
<p className="text-muted">
这是一个基于 GitHub 仓库的政策文档页面。
<a
href={`https://github.com/fpsig/open-source-policy/blob/main/China/政策/${path}`}
target="_blank"
rel="noopener noreferrer"
className="ms-2"
>
在 GitHub 上查看或编辑此内容
</a>
</p>
</div>
</footer>
</Container>
);

export default WikiPage;
Loading
Loading