Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
132 changes: 132 additions & 0 deletions models/Wiki.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { ContentModel } from 'mobx-github';
import { treeFrom } from 'web-utility';
import { githubClient } from './Base';

export interface WikiNode {
name?: string;
title: string;
path: string;
parent_path?: string;
children?: WikiNode[];
type?: string;
size?: number;
sha?: string;
url?: string;
html_url?: string;
git_url?: string;
download_url?: string;
content?: string;
metadata?: Record<string, string>;
labels?: string[];
}

export const contentStore = new ContentModel('fpsig', 'open-source-policy');

class WikiModel {
private contentModel = contentStore;

async getAllContent(): Promise<WikiNode[]> {
try {
const items: WikiNode[] = [];

// Use traverseTree to get all markdown files recursively from China/政策
for await (const item of this.contentModel.traverseTree()) {
if (item.type === 'file' && item.name.endsWith('.md') && item.path.startsWith('China/政策/')) {
// Remove the 'China/政策/' prefix to get relative path within wiki
const relativePath = item.path.replace('China/政策/', '');
const pathParts = relativePath.split('/');
const fileName = pathParts.pop();
const parent_path = pathParts.length > 0 ? pathParts.join('/') : undefined;

const wikiNode: WikiNode = {
name: fileName || '',
path: relativePath.replace('.md', ''),
parent_path,
title: fileName?.replace('.md', '') || '',
type: item.type,
size: item.size,
sha: item.sha,
url: item.url,
html_url: item.html_url || undefined,
git_url: item.git_url || undefined,
download_url: item.download_url || undefined,
content: '',
labels: [],
};

items.push(wikiNode);
}
}

return items;
} catch (error) {
console.error('Error fetching content from GitHub:', error);
return [];
}
}

async getContentTree(): Promise<WikiNode[]> {
const allContent = await this.getAllContent();
return treeFrom(allContent, 'path', 'parent_path', 'children');
}

async getWikiContent(pathParam: string): Promise<WikiNode> {
const fullPath = pathParam.endsWith('.md') ? pathParam : `${pathParam}.md`;
const filePath = `China/政策/${fullPath}`;

const item = await this.contentModel.getOne(filePath);

if (!item || item.type !== 'file') {
throw new Error(`Content not found at path: ${pathParam}`);
}

// Decode Base64 content
const content = item.content ? atob(item.content) : '';

// Parse frontmatter
let metadata: Record<string, string> = {};
let markdownContent = '';

if (content.startsWith('---\n')) {
const parts = content.split('\n---\n');
if (parts.length >= 2) {
const frontmatter = parts[0].substring(4); // Remove first '---\n'
markdownContent = parts.slice(1).join('\n---\n');

// Simple YAML parsing for metadata
const lines = frontmatter.split('\n');
for (const line of lines) {
const [key, ...valueParts] = line.split(': ');
if (key && valueParts.length > 0) {
metadata[key.trim()] = valueParts.join(': ').trim();
}
}
}
} else {
markdownContent = content;
}

const pathParts = pathParam.split('/');
const fileName = pathParts.pop();
const parent_path = pathParts.length > 0 ? pathParts.join('/') : undefined;

return {
name: fileName || '',
path: pathParam.replace('.md', ''),
parent_path,
title: metadata['name'] || fileName?.replace('.md', '') || '',
type: item.type,
size: item.size,
sha: item.sha,
url: item.url,
html_url: item.html_url || undefined,
git_url: item.git_url || undefined,
download_url: item.download_url || undefined,
content: markdownContent,
metadata,
labels: [],
};
}
}

export const wikiStore = new WikiModel();
Comment thread
TechQuery marked this conversation as resolved.
Outdated
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@
"@next/mdx": "^15.5.0",
"core-js": "^3.45.1",
"file-type": "^21.0.0",
"github-markdown-css": "^5.8.1",
"idea-react": "^2.0.0-rc.13",
"koa": "^3.0.1",
"koajax": "^3.1.2",
"license-filter": "^0.2.5",
"marked": "^16.2.0",
"mime": "^4.0.7",
"mobx": "^6.13.7",
"mobx-github": "^0.3.11",
"mobx-github": "^0.4.0-rc.1",
"mobx-i18n": "^0.7.1",
"mobx-lark": "^2.4.0",
"mobx-react": "^9.2.0",
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
156 changes: 130 additions & 26 deletions pages/wiki/[...slug].tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,155 @@
import { marked } from 'marked';
import { DataObject } from 'mobx-restful';
import { GetStaticPaths, GetStaticProps, NextPage } from 'next';
import { GetStaticPaths, GetStaticProps } from 'next';
import Link from 'next/link';
import { ParsedUrlQuery } from 'querystring';
import { FC } from 'react';
import { Badge, Breadcrumb, Button, Container } from 'react-bootstrap';

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

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 wikiStore.getAllContent();
const paths = nodes.map(({ path }) => ({
params: { slug: path.split('/') }
}));

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

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

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

const { meta, markdown } = await splitFrontMatter(`public/${slug.join('/')}.md`);
const node = await wikiStore.getWikiContent(nodePath);
const markup = marked(node.content || '') as string;
Comment thread
TechQuery marked this conversation as resolved.
Outdated

const markup = marked(markdown) as string;

return { props: JSON.parse(JSON.stringify({ meta, markup })) };
return {
props: { node, markup },
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<WikiPageProps> = ({ node, markup }) => (
<Container className="py-4">
<PageHead title={node.title} />

<Breadcrumb className="mb-4">
<Breadcrumb.Item linkAs={Link} linkProps={{ href: '/wiki' }}>
Wiki
</Breadcrumb.Item>
{node.parent_path?.split('/').map((segment, index, array) => {
const breadcrumbPath = array.slice(0, index + 1).join('/');

return (
<Breadcrumb.Item
key={breadcrumbPath}
linkAs={Link}
linkProps={{ href: `/wiki/${breadcrumbPath}` }}
>
{segment}
</Breadcrumb.Item>
);
})}
<Breadcrumb.Item active>
{node.title}
</Breadcrumb.Item>
</Breadcrumb>

<article>
<header className="mb-4">
<h1>{node.title}</h1>

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

<div className="d-flex justify-content-between align-items-center text-muted small mb-3">
<div>
{node.metadata?.['成文日期'] && (
<span>成文日期: {node.metadata['成文日期']}</span>
)}
{node.metadata?.['发布日期'] && node.metadata['发布日期'] !== node.metadata['成文日期'] && (
<span className="ms-3">发布日期: {node.metadata['发布日期']}</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/政策/${node.path}`}
target="_blank"
rel="noopener noreferrer"
>
在 GitHub 编辑
</Button>
{node.metadata?.url && (
<Button
variant="outline-secondary"
size="sm"
href={node.metadata.url}
target="_blank"
rel="noopener noreferrer"
>
查看原文
</Button>
)}
</div>
</div>
</header>

<div
dangerouslySetInnerHTML={{ __html: markup }}
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/政策/${node.path}`}
target="_blank"
rel="noopener noreferrer"
className="ms-2"
>
在 GitHub 上查看或编辑此内容
</a>
</p>
</div>
</footer>
</Container>
);

export default WikiPage;
Loading
Loading