fix(ui): improve review dialog rendering for tool calls and tables (#657)

* fix(ui): improve review dialog rendering for tool calls and tables

- Replace Markdown component with LogViewer in plan-approval-dialog to
  properly format tool calls with collapsible sections and JSON highlighting
- Add remark-gfm plugin to Markdown component for GitHub Flavored Markdown
  support including tables, task lists, and strikethrough
- Add table styling classes to Markdown component for proper table rendering
- Install remark-gfm and rehype-sanitize dependencies

Fixes mixed/broken rendering in review dialog where tool calls showed as
raw text and markdown tables showed as pipe-separated text.

* chore: fix git+ssh URL and prettier formatting

- Convert git+ssh:// to git+https:// in package-lock.json for @electron/node-gyp
- Apply prettier formatting to plan-approval-dialog.tsx

* fix(ui): create PlanContentViewer for better plan display

The previous LogViewer approach showed tool calls prominently but hid
the actual plan/specification markdown content. The new PlanContentViewer:

- Separates tool calls (exploration) from plan markdown
- Shows the plan/specification markdown prominently using Markdown component
- Collapses tool calls by default in an "Exploration" section
- Properly renders GFM tables in the plan content

This provides a better UX where users see the important plan content
first, with tool calls available but not distracting.

* fix(ui): add show more/less toggle for feature description

The feature description in the plan approval dialog header was
truncated at 150 characters with no way to see the full text.
Now users can click "show more" to expand and "show less" to collapse.

* fix(ui): increase description limit and add feature title to dialog

- Increase description character limit from 150 to 250 characters
- Add feature title to dialog header (e.g., "Review Plan - Feature Title")
  only if title exists and is <= 50 characters

* feat(ui): render tasks code blocks as proper checkbox lists

When markdown contains a ```tasks code block, it now renders as:
- Phase headers (## Phase 1: ...) as styled section headings
- Task items (- [ ] or - [x]) with proper checkbox icons
- Checked items show green checkmark and strikethrough text
- Unchecked items show empty square icon

This makes implementation task lists in plans much more readable
compared to rendering them as raw code blocks.

* fix(ui): improve plan content parsing robustness

Address CodeRabbit review feedback:

1. Relax heading detection regex to match emoji and non-word chars
   - Change \w to \S so headings like "##  Plan" are detected
   - Change \*\*[A-Z] to \*\*\S for bold section detection

2. Flush active tool call when heading is detected
   - Prevents plan content being dropped when heading follows tool call
     without a blank line separator

3. Support tool names with dots/hyphens
   - Change \w+ to [^\s]+ so names like "web.run" or "file-read" work

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Stefan de Vogelaere
2026-01-23 01:41:45 +01:00
committed by GitHub
parent 655f254538
commit 7773db559d
5 changed files with 635 additions and 9 deletions

View File

@@ -102,6 +102,8 @@
"react-markdown": "10.1.0",
"react-resizable-panels": "3.0.6",
"rehype-raw": "7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"sonner": "2.0.7",
"tailwind-merge": "3.4.0",
"usehooks-ts": "3.1.1",

View File

@@ -1,13 +1,97 @@
import ReactMarkdown from 'react-markdown';
import ReactMarkdown, { Components } from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import remarkGfm from 'remark-gfm';
import { cn } from '@/lib/utils';
import { Square, CheckSquare } from 'lucide-react';
interface MarkdownProps {
children: string;
className?: string;
}
/**
* Renders a tasks code block as a proper task list with checkboxes
*/
function TasksBlock({ content }: { content: string }) {
const lines = content.split('\n');
return (
<div className="my-4 space-y-1">
{lines.map((line, idx) => {
const trimmed = line.trim();
// Check for phase/section headers (## Phase 1: ...)
const headerMatch = trimmed.match(/^##\s+(.+)$/);
if (headerMatch) {
return (
<div key={idx} className="text-foreground font-semibold mt-4 mb-2 text-sm">
{headerMatch[1]}
</div>
);
}
// Check for task items (- [ ] or - [x])
const taskMatch = trimmed.match(/^-\s*\[([ xX])\]\s*(.+)$/);
if (taskMatch) {
const isChecked = taskMatch[1].toLowerCase() === 'x';
const taskText = taskMatch[2];
return (
<div key={idx} className="flex items-start gap-2 py-1">
{isChecked ? (
<CheckSquare className="w-4 h-4 text-emerald-500 mt-0.5 flex-shrink-0" />
) : (
<Square className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
)}
<span
className={cn(
'text-sm',
isChecked ? 'text-muted-foreground line-through' : 'text-foreground-secondary'
)}
>
{taskText}
</span>
</div>
);
}
// Empty lines
if (!trimmed) {
return <div key={idx} className="h-2" />;
}
// Other content (render as-is)
return (
<div key={idx} className="text-sm text-foreground-secondary">
{trimmed}
</div>
);
})}
</div>
);
}
/**
* Custom components for ReactMarkdown
*/
const markdownComponents: Components = {
// Handle code blocks - special case for 'tasks' language
code({ className, children }) {
const match = /language-(\w+)/.exec(className || '');
const language = match ? match[1] : '';
const content = String(children).replace(/\n$/, '');
// Special handling for tasks code blocks
if (language === 'tasks') {
return <TasksBlock content={content} />;
}
// Regular code (inline or block)
return <code className={className}>{children}</code>;
},
};
/**
* Reusable Markdown component for rendering markdown content
* Theme-aware styling that adapts to all predefined themes
@@ -42,10 +126,20 @@ export function Markdown({ children, className }: MarkdownProps) {
'[&_hr]:border-border [&_hr]:my-4',
// Images
'[&_img]:max-w-full [&_img]:h-auto [&_img]:rounded-lg [&_img]:my-2 [&_img]:border [&_img]:border-border',
// Tables
'[&_table]:w-full [&_table]:border-collapse [&_table]:my-4',
'[&_th]:border [&_th]:border-border [&_th]:bg-muted [&_th]:px-3 [&_th]:py-2 [&_th]:text-left [&_th]:text-foreground [&_th]:font-semibold',
'[&_td]:border [&_td]:border-border [&_td]:px-3 [&_td]:py-2 [&_td]:text-foreground-secondary',
className
)}
>
<ReactMarkdown rehypePlugins={[rehypeRaw, rehypeSanitize]}>{children}</ReactMarkdown>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
components={markdownComponents}
>
{children}
</ReactMarkdown>
</div>
);
}

View File

@@ -11,7 +11,7 @@ import {
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Markdown } from '@/components/ui/markdown';
import { PlanContentViewer } from './plan-content-viewer';
import { Label } from '@/components/ui/label';
import { Feature } from '@/store/app-store';
import { Check, RefreshCw, Edit2, Eye } from 'lucide-react';
@@ -42,6 +42,10 @@ export function PlanApprovalDialog({
const [editedPlan, setEditedPlan] = useState(planContent);
const [showRejectFeedback, setShowRejectFeedback] = useState(false);
const [rejectFeedback, setRejectFeedback] = useState('');
const [showFullDescription, setShowFullDescription] = useState(false);
const DESCRIPTION_LIMIT = 250;
const TITLE_LIMIT = 50;
// Reset state when dialog opens or plan content changes
useEffect(() => {
@@ -50,6 +54,7 @@ export function PlanApprovalDialog({
setIsEditMode(false);
setShowRejectFeedback(false);
setRejectFeedback('');
setShowFullDescription(false);
}
}, [open, planContent]);
@@ -82,15 +87,31 @@ export function PlanApprovalDialog({
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl" data-testid="plan-approval-dialog">
<DialogHeader>
<DialogTitle>{viewOnly ? 'View Plan' : 'Review Plan'}</DialogTitle>
<DialogTitle>
{viewOnly ? 'View Plan' : 'Review Plan'}
{feature?.title && feature.title.length <= TITLE_LIMIT && (
<span className="font-normal text-muted-foreground"> - {feature.title}</span>
)}
</DialogTitle>
<DialogDescription>
{viewOnly
? 'View the generated plan for this feature.'
: 'Review the generated plan before implementation begins.'}
{feature && (
<span className="block mt-2 text-primary">
Feature: {feature.description.slice(0, 150)}
{feature.description.length > 150 ? '...' : ''}
Feature:{' '}
{showFullDescription || feature.description.length <= DESCRIPTION_LIMIT
? feature.description
: `${feature.description.slice(0, DESCRIPTION_LIMIT)}...`}
{feature.description.length > DESCRIPTION_LIMIT && (
<button
type="button"
onClick={() => setShowFullDescription(!showFullDescription)}
className="ml-1 text-muted-foreground hover:text-foreground underline text-sm"
>
{showFullDescription ? 'show less' : 'show more'}
</button>
)}
</span>
)}
</DialogDescription>
@@ -135,9 +156,7 @@ export function PlanApprovalDialog({
disabled={isLoading}
/>
) : (
<div className="p-4 overflow-auto">
<Markdown>{editedPlan || 'No plan content available.'}</Markdown>
</div>
<PlanContentViewer content={editedPlan || ''} className="p-4" />
)}
</div>

View File

@@ -0,0 +1,216 @@
'use client';
import { useMemo, useState } from 'react';
import { ChevronDown, ChevronRight, Wrench } from 'lucide-react';
import { Markdown } from '@/components/ui/markdown';
import { cn } from '@/lib/utils';
interface ToolCall {
tool: string;
input: string;
}
interface ParsedPlanContent {
toolCalls: ToolCall[];
planMarkdown: string;
}
/**
* Parses plan content to separate tool calls from the actual plan/specification markdown.
* Tool calls appear at the beginning (exploration phase), followed by the plan markdown.
*/
function parsePlanContent(content: string): ParsedPlanContent {
const lines = content.split('\n');
const toolCalls: ToolCall[] = [];
let planStartIndex = -1;
let currentTool: string | null = null;
let currentInput: string[] = [];
let inJsonBlock = false;
let braceDepth = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
// Check if this line starts the actual plan/spec (markdown heading)
// Plans typically start with # or ## headings
if (
!inJsonBlock &&
(trimmed.match(/^#{1,3}\s+\S/) || // Markdown headings (including emoji like ## ✅ Plan)
trimmed.startsWith('---') || // Horizontal rule often used as separator
trimmed.match(/^\*\*\S/)) // Bold text starting a section
) {
// Flush any active tool call before starting the plan
if (currentTool && currentInput.length > 0) {
toolCalls.push({
tool: currentTool,
input: currentInput.join('\n').trim(),
});
currentTool = null;
currentInput = [];
}
planStartIndex = i;
break;
}
// Detect tool call start (supports tool names with dots/hyphens like web.run, file-read)
const toolMatch = trimmed.match(/^(?:🔧\s*)?Tool:\s*([^\s]+)/i);
if (toolMatch && !inJsonBlock) {
// Save previous tool call if exists
if (currentTool && currentInput.length > 0) {
toolCalls.push({
tool: currentTool,
input: currentInput.join('\n').trim(),
});
}
currentTool = toolMatch[1];
currentInput = [];
continue;
}
// Detect Input: line
if (trimmed.startsWith('Input:') && currentTool) {
const inputContent = trimmed.replace(/^Input:\s*/, '');
if (inputContent) {
currentInput.push(inputContent);
// Check if JSON starts
if (inputContent.includes('{')) {
braceDepth =
(inputContent.match(/\{/g) || []).length - (inputContent.match(/\}/g) || []).length;
inJsonBlock = braceDepth > 0;
}
}
continue;
}
// If we're collecting input for a tool
if (currentTool) {
if (inJsonBlock) {
currentInput.push(line);
braceDepth += (trimmed.match(/\{/g) || []).length - (trimmed.match(/\}/g) || []).length;
if (braceDepth <= 0) {
inJsonBlock = false;
// Save tool call
toolCalls.push({
tool: currentTool,
input: currentInput.join('\n').trim(),
});
currentTool = null;
currentInput = [];
}
} else if (trimmed.startsWith('{')) {
// JSON block starting
currentInput.push(line);
braceDepth = (trimmed.match(/\{/g) || []).length - (trimmed.match(/\}/g) || []).length;
inJsonBlock = braceDepth > 0;
if (!inJsonBlock) {
// Single-line JSON
toolCalls.push({
tool: currentTool,
input: currentInput.join('\n').trim(),
});
currentTool = null;
currentInput = [];
}
} else if (trimmed === '') {
// Empty line might end the tool call section
if (currentInput.length > 0) {
toolCalls.push({
tool: currentTool,
input: currentInput.join('\n').trim(),
});
currentTool = null;
currentInput = [];
}
}
}
}
// Save any remaining tool call
if (currentTool && currentInput.length > 0) {
toolCalls.push({
tool: currentTool,
input: currentInput.join('\n').trim(),
});
}
// Extract plan markdown
let planMarkdown = '';
if (planStartIndex >= 0) {
planMarkdown = lines.slice(planStartIndex).join('\n').trim();
} else if (toolCalls.length === 0) {
// No tool calls found, treat entire content as markdown
planMarkdown = content.trim();
}
return { toolCalls, planMarkdown };
}
interface PlanContentViewerProps {
content: string;
className?: string;
}
export function PlanContentViewer({ content, className }: PlanContentViewerProps) {
const [showToolCalls, setShowToolCalls] = useState(false);
const { toolCalls, planMarkdown } = useMemo(() => parsePlanContent(content), [content]);
if (!content || !content.trim()) {
return (
<div className={cn('text-muted-foreground text-center py-8', className)}>
No plan content available.
</div>
);
}
return (
<div className={cn('space-y-4', className)}>
{/* Tool Calls Section - Collapsed by default */}
{toolCalls.length > 0 && (
<div className="border border-border rounded-lg overflow-hidden">
<button
onClick={() => setShowToolCalls(!showToolCalls)}
className="w-full px-4 py-2 flex items-center gap-2 bg-muted/30 hover:bg-muted/50 transition-colors text-left"
>
{showToolCalls ? (
<ChevronDown className="w-4 h-4 text-muted-foreground" />
) : (
<ChevronRight className="w-4 h-4 text-muted-foreground" />
)}
<Wrench className="w-4 h-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
Exploration ({toolCalls.length} tool call{toolCalls.length !== 1 ? 's' : ''})
</span>
</button>
{showToolCalls && (
<div className="p-3 space-y-2 bg-muted/10 max-h-[300px] overflow-y-auto">
{toolCalls.map((tc, idx) => (
<div key={idx} className="text-xs font-mono">
<div className="text-cyan-400 mb-1">Tool: {tc.tool}</div>
<pre className="bg-muted/50 rounded p-2 overflow-x-auto text-foreground-secondary whitespace-pre-wrap">
{tc.input}
</pre>
</div>
))}
</div>
)}
</div>
)}
{/* Plan/Specification Content - Main focus */}
{planMarkdown ? (
<div className="min-h-[200px]">
<Markdown>{planMarkdown}</Markdown>
</div>
) : toolCalls.length > 0 ? (
<div className="text-muted-foreground text-center py-8 border border-dashed border-border rounded-lg">
<p className="text-sm">No specification content found.</p>
<p className="text-xs mt-1">The plan appears to only contain exploration tool calls.</p>
</div>
) : null}
</div>
);
}

295
package-lock.json generated
View File

@@ -163,6 +163,8 @@
"react-markdown": "10.1.0",
"react-resizable-panels": "3.0.6",
"rehype-raw": "7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"sonner": "2.0.7",
"tailwind-merge": "3.4.0",
"usehooks-ts": "3.1.1",
@@ -12130,6 +12132,16 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/markdown-table": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
"integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/matcher": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
@@ -12153,6 +12165,34 @@
"node": ">= 0.4"
}
},
"node_modules/mdast-util-find-and-replace": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
"integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"escape-string-regexp": "^5.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mdast-util-from-markdown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
@@ -12177,6 +12217,107 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
"integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
"license": "MIT",
"dependencies": {
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-gfm-autolink-literal": "^2.0.0",
"mdast-util-gfm-footnote": "^2.0.0",
"mdast-util-gfm-strikethrough": "^2.0.0",
"mdast-util-gfm-table": "^2.0.0",
"mdast-util-gfm-task-list-item": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm-autolink-literal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
"integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"ccount": "^2.0.0",
"devlop": "^1.0.0",
"mdast-util-find-and-replace": "^3.0.0",
"micromark-util-character": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm-footnote": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
"integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"devlop": "^1.1.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0",
"micromark-util-normalize-identifier": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm-strikethrough": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
"integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm-table": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
"integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"markdown-table": "^3.0.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm-task-list-item": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
"integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-mdx-expression": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
@@ -12396,6 +12537,127 @@
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-extension-gfm": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
"integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
"license": "MIT",
"dependencies": {
"micromark-extension-gfm-autolink-literal": "^2.0.0",
"micromark-extension-gfm-footnote": "^2.0.0",
"micromark-extension-gfm-strikethrough": "^2.0.0",
"micromark-extension-gfm-table": "^2.0.0",
"micromark-extension-gfm-tagfilter": "^2.0.0",
"micromark-extension-gfm-task-list-item": "^2.0.0",
"micromark-util-combine-extensions": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-autolink-literal": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
"integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
"license": "MIT",
"dependencies": {
"micromark-util-character": "^2.0.0",
"micromark-util-sanitize-uri": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-footnote": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
"integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
"micromark-core-commonmark": "^2.0.0",
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-normalize-identifier": "^2.0.0",
"micromark-util-sanitize-uri": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-strikethrough": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
"integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
"micromark-util-chunked": "^2.0.0",
"micromark-util-classify-character": "^2.0.0",
"micromark-util-resolve-all": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-table": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
"integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-tagfilter": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
"integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
"license": "MIT",
"dependencies": {
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-task-list-item": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
"integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-factory-destination": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
@@ -14184,6 +14446,24 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-gfm": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
"integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-gfm": "^3.0.0",
"micromark-extension-gfm": "^3.0.0",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-parse": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
@@ -14217,6 +14497,21 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-stringify": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
"integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-to-markdown": "^2.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",