mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-21 01:11:21 +03:00
feat(wiki): add full in-app wiki browser and llms index (#80)
* feat(wiki): add full in-app wiki browser and llms index * feat(wiki): auto-generate per-page llms exports * vuln package * fix(wiki): guard malformed route decoding * fix(wiki): preserve markdown anchor fragments across page links * refactor(markdown): share default render components * fix(wiki): block unsafe markdown link schemes * fix(wiki): block unsafe markdown image schemes * docs(wiki): migrate root docs into wiki pages * chore(wiki): de-track generated llms exports * chore(wiki): ignore generated public wiki artifacts * fix(wiki): align llms urls with per-page endpoint pattern * fix(wiki): derive llms index from wiki index page * refactor(markdown): share frontmatter and title helpers * refactor(wiki): share route and llms path mapping * ci(pages): add pr verify workflow and tighten deploy triggers
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import type { Components } from 'react-markdown';
|
||||
|
||||
export const defaultMarkdownComponents: Components = {
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-2xl font-bold text-white border-b border-clawd-700 pb-3 mb-6 mt-0">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-xl font-bold text-white mt-8 mb-4">{children}</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="text-lg font-semibold text-white mt-6 mb-3">{children}</h3>
|
||||
),
|
||||
h4: ({ children }) => (
|
||||
<h4 className="text-base font-semibold text-white mt-4 mb-2">{children}</h4>
|
||||
),
|
||||
p: ({ children }) => (
|
||||
<p className="text-gray-300 leading-relaxed mb-4">{children}</p>
|
||||
),
|
||||
a: ({ href, children }) => (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-clawd-accent hover:underline"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul className="list-disc list-inside text-gray-300 space-y-2 mb-4 ml-4">
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="list-decimal list-inside text-gray-300 space-y-2 mb-4 ml-4">
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="text-gray-300">{children}</li>
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-4 border-clawd-accent pl-4 py-2 my-4 bg-clawd-900/50 rounded-r text-gray-400 italic">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
code: ({ className, children }) => {
|
||||
const isInline = !className;
|
||||
if (isInline) {
|
||||
return (
|
||||
<code className="text-orange-300 bg-clawd-900 px-1.5 py-0.5 rounded text-sm font-mono">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<code className="text-gray-200 text-sm font-mono">{children}</code>
|
||||
);
|
||||
},
|
||||
pre: ({ children }) => (
|
||||
<pre className="bg-clawd-900 border border-clawd-700 rounded-lg p-3 sm:p-4 overflow-x-auto mb-4 text-xs sm:text-sm max-w-full">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
table: ({ children }) => (
|
||||
<div className="overflow-x-auto mb-6 -mx-4 sm:mx-0 px-4 sm:px-0">
|
||||
<table className="w-full border-collapse text-xs sm:text-sm min-w-[300px]">
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
thead: ({ children }) => (
|
||||
<thead className="bg-clawd-900 border-b border-clawd-600">
|
||||
{children}
|
||||
</thead>
|
||||
),
|
||||
tbody: ({ children }) => <tbody>{children}</tbody>,
|
||||
tr: ({ children }) => (
|
||||
<tr className="border-b border-clawd-700/50">{children}</tr>
|
||||
),
|
||||
th: ({ children }) => (
|
||||
<th className="text-left px-4 py-3 text-gray-300 font-semibold">
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children }) => (
|
||||
<td className="px-4 py-3 text-gray-300">{children}</td>
|
||||
),
|
||||
hr: () => <hr className="border-clawd-700 my-6" />,
|
||||
strong: ({ children }) => (
|
||||
<strong className="text-white font-semibold">{children}</strong>
|
||||
),
|
||||
em: ({ children }) => (
|
||||
<em className="text-gray-200">{children}</em>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
const FRONTMATTER_REGEX = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
|
||||
/**
|
||||
* Remove a leading YAML frontmatter block from markdown content.
|
||||
* @param {string} content
|
||||
* @returns {string}
|
||||
*/
|
||||
export const stripFrontmatter = (content) =>
|
||||
String(content ?? '').replace(FRONTMATTER_REGEX, '');
|
||||
|
||||
/**
|
||||
* Build a readable fallback title from a markdown file path.
|
||||
* @param {string} filePath
|
||||
* @returns {string}
|
||||
*/
|
||||
export const fallbackTitleFromPath = (filePath) => {
|
||||
const normalized = String(filePath ?? '');
|
||||
const filename = normalized.split('/').pop() ?? normalized;
|
||||
const stem = filename.replace(/\.md$/i, '');
|
||||
return stem
|
||||
.split(/[-_]/)
|
||||
.filter(Boolean)
|
||||
.map((part) => {
|
||||
if (part.toUpperCase() === part && part.length > 1) return part;
|
||||
return part.charAt(0).toUpperCase() + part.slice(1);
|
||||
})
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract the first H1 title from markdown; fall back to path-derived title.
|
||||
* @param {string} content
|
||||
* @param {string} filePath
|
||||
* @returns {string}
|
||||
*/
|
||||
export const extractTitleFromMarkdown = (content, filePath) => {
|
||||
const cleaned = stripFrontmatter(content).trim();
|
||||
const match = cleaned.match(/^#\s+(.+)$/m);
|
||||
return match?.[1]?.trim() || fallbackTitleFromPath(filePath);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Normalize a wiki slug for route/path construction.
|
||||
* @param {string} slug
|
||||
* @returns {string}
|
||||
*/
|
||||
const normalizeWikiSlug = (slug) =>
|
||||
String(slug ?? '')
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/^\/+|\/+$/g, '');
|
||||
|
||||
/**
|
||||
* Return whether a slug represents the wiki index page.
|
||||
* @param {string} slug
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isWikiIndexSlug = (slug) => normalizeWikiSlug(slug).toLowerCase() === 'index';
|
||||
|
||||
/**
|
||||
* Convert a wiki slug to app route path.
|
||||
* @param {string} slug
|
||||
* @returns {string}
|
||||
*/
|
||||
export const toWikiRoute = (slug) => {
|
||||
const normalized = normalizeWikiSlug(slug);
|
||||
if (!normalized || isWikiIndexSlug(normalized)) return '/wiki';
|
||||
return `/wiki/${normalized}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a wiki slug to its llms.txt endpoint path.
|
||||
* @param {string} slug
|
||||
* @returns {string}
|
||||
*/
|
||||
export const toWikiLlmsPath = (slug) => {
|
||||
const normalized = normalizeWikiSlug(slug);
|
||||
if (!normalized || isWikiIndexSlug(normalized)) return '/wiki/llms.txt';
|
||||
return `/wiki/${normalized}/llms.txt`;
|
||||
};
|
||||
Reference in New Issue
Block a user