Improve links in markdown renderer (vibe-kanban) (#833)
* Perfect! The implementation is complete and all type checks pass. ## Summary I've successfully implemented all the requested improvements to the markdown renderer: ✅ **External links** now open in a new tab with `target="_blank"` and `rel="noopener noreferrer"` for security ✅ **Internal links** are disabled and rendered as `<span>` elements with proper accessibility attributes ✅ **Link styling** includes highlighted background with hover effects for external links ✅ **File paths in backticks** now have similar highlighting to links ✅ **Security** includes URL sanitization to prevent dangerous protocols like `javascript:` and `data:` The implementation follows React and accessibility best practices, integrates seamlessly with the existing `markdown-to-jsx` setup, and maintains clean separation between external and internal link behaviors. * Cleanup script changes for task attempt 66cb18c3-0718-4c0f-803a-a3c8cb15d052 * Perfect! The change is complete and all checks pass. ## Summary ✅ **HTTPS-only external links** - Modified both `sanitizeHref()` and `isExternalHref()` functions to only allow `https://` URLs as external links Now only HTTPS links will open in new tabs, while HTTP links will be treated as disabled internal links (rendered as non-interactive spans). This provides better security by preventing insecure HTTP requests from the application.
This commit is contained in:
committed by
GitHub
parent
018006c728
commit
875c5ed792
@@ -10,6 +10,103 @@ import { Button } from '@/components/ui/button.tsx';
|
||||
import { Check, Clipboard } from 'lucide-react';
|
||||
import { writeClipboardViaBridge } from '@/vscode/bridge';
|
||||
|
||||
const HIGHLIGHT_LINK =
|
||||
'rounded-sm bg-muted/50 px-1 py-0.5 underline-offset-2 transition-colors';
|
||||
const HIGHLIGHT_LINK_HOVER = 'hover:bg-muted';
|
||||
const HIGHLIGHT_CODE = 'rounded-sm bg-muted/50 px-1 py-0.5 font-mono text-sm';
|
||||
|
||||
function sanitizeHref(href?: string): string | undefined {
|
||||
if (typeof href !== 'string') return undefined;
|
||||
const trimmed = href.trim();
|
||||
// Block dangerous protocols
|
||||
if (/^(javascript|vbscript|data):/i.test(trimmed)) return undefined;
|
||||
// Allow anchors and common relative forms
|
||||
if (
|
||||
trimmed.startsWith('#') ||
|
||||
trimmed.startsWith('./') ||
|
||||
trimmed.startsWith('../') ||
|
||||
trimmed.startsWith('/')
|
||||
)
|
||||
return trimmed;
|
||||
// Allow only https
|
||||
if (/^https:\/\//i.test(trimmed)) return trimmed;
|
||||
// Block everything else by default
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isExternalHref(href?: string): boolean {
|
||||
if (!href) return false;
|
||||
return /^https:\/\//i.test(href);
|
||||
}
|
||||
|
||||
function LinkOverride({
|
||||
href,
|
||||
children,
|
||||
title,
|
||||
}: {
|
||||
href?: string;
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
}) {
|
||||
const rawHref = typeof href === 'string' ? href : '';
|
||||
const safeHref = sanitizeHref(rawHref);
|
||||
|
||||
const external = isExternalHref(safeHref);
|
||||
const internalOrDisabled = !external;
|
||||
|
||||
if (!safeHref || internalOrDisabled) {
|
||||
// Disabled internal link (relative paths and anchors)
|
||||
return (
|
||||
<span
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
title={title || rawHref || undefined}
|
||||
className={`${HIGHLIGHT_LINK} cursor-not-allowed select-text`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// External link
|
||||
return (
|
||||
<a
|
||||
href={safeHref}
|
||||
title={title}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`${HIGHLIGHT_LINK} ${HIGHLIGHT_LINK_HOVER} underline`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function InlineCodeOverride({ children, className, ...props }: any) {
|
||||
// Only highlight inline code, not fenced code blocks
|
||||
const hasLanguage =
|
||||
typeof className === 'string' && /\blanguage-/.test(className);
|
||||
if (hasLanguage) {
|
||||
// Likely a fenced block's <code>; leave className as-is for syntax highlighting
|
||||
return (
|
||||
<code {...props} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<code
|
||||
{...props}
|
||||
className={`${HIGHLIGHT_CODE}${className ? ` ${className}` : ''}`}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
interface MarkdownRendererProps {
|
||||
content: string;
|
||||
className?: string;
|
||||
@@ -23,16 +120,8 @@ function MarkdownRenderer({
|
||||
}: MarkdownRendererProps) {
|
||||
const overrides = useMemo(
|
||||
() => ({
|
||||
code: {
|
||||
component: ({ children, ...props }: any) => (
|
||||
<code
|
||||
{...props}
|
||||
className="bg-background px-1 py-0.5 text-sm font-mono"
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
},
|
||||
a: { component: LinkOverride },
|
||||
code: { component: InlineCodeOverride },
|
||||
strong: {
|
||||
component: ({ children, ...props }: any) => (
|
||||
<span {...props} className="">
|
||||
|
||||
Reference in New Issue
Block a user