Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
147 changes: 147 additions & 0 deletions models/Wiki.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { marked } from 'marked';
import { treeFrom } from 'web-utility';
import * as fs from 'fs';
import * as path from 'path';

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;
// Frontmatter fields
metadata?: Record<string, string>;
content?: string;
}

export class WikiModel {
// For now, only use filesystem reading for simplicity

// Method for build-time static generation
async getAllContentStatic(): Promise<WikiNode[]> {
const items: WikiNode[] = [];
const policyDir = path.join(process.cwd(), 'public/wiki/policy/China/政策');

if (!fs.existsSync(policyDir)) {
return items;
}

const processDirectory = (dir: string, baseDir: string = '') => {
const files = fs.readdirSync(dir);

for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);

if (stat.isDirectory()) {
const subDir = baseDir ? `${baseDir}/${file}` : file;
processDirectory(fullPath, subDir);
} else if (file.endsWith('.md')) {
const relativePath = baseDir ? `${baseDir}/${file}` : file;
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: 'file',
size: stat.size,
sha: '',
url: '',
html_url: `https://github.com/fpsig/open-source-policy/blob/main/China/政策/${relativePath}`,
git_url: '',
download_url: `https://raw.githubusercontent.com/fpsig/open-source-policy/main/China/政策/${relativePath}`,
content: '',
};

items.push(wikiNode);
}
}
};

processDirectory(policyDir);
return items;
}

// Method for all environments - use static reading
async getAllContent(): Promise<WikiNode[]> {
return this.getAllContentStatic();
}

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

async getOne(path: string): Promise<WikiNode> {
return this.getOneStatic(path);
}

// Static method for build-time reading
async getOneStatic(pathParam: string): Promise<WikiNode> {
const fullPath = pathParam.endsWith('.md') ? pathParam : `${pathParam}.md`;
const filePath = path.join(process.cwd(), 'public/wiki/policy/China/政策', fullPath);

if (!fs.existsSync(filePath)) {
throw new Error(`Content not found at path: ${pathParam}`);
}

const fileContent = fs.readFileSync(filePath, 'utf-8');
const stat = fs.statSync(filePath);

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

if (fileContent.startsWith('---\n')) {
const parts = fileContent.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 = fileContent;
}

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: 'file',
size: stat.size,
sha: '',
url: '',
html_url: `https://github.com/fpsig/open-source-policy/blob/main/China/政策/${fullPath}`,
git_url: '',
download_url: `https://raw.githubusercontent.com/fpsig/open-source-policy/main/China/政策/${fullPath}`,
content: markdownContent,
metadata,
};
}
}

export const wikiStore = new WikiModel();
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
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.getOne(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 && node.parent_path.split('/').map((segment, index, array) => {
Comment thread
TechQuery marked this conversation as resolved.
Outdated
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