feat: recognize macros

This commit is contained in:
2026-02-13 16:16:43 +01:00
parent 55f37f4dfa
commit 1aa44e675d
5 changed files with 919 additions and 0 deletions

View File

@@ -666,3 +666,208 @@
margin: 0;
font-size: 13px;
}
/* =================================
MACROS SECTION STYLES
================================= */
.macro-summary-counts {
display: inline-flex;
gap: 8px;
margin-left: auto;
font-size: 11px;
font-weight: normal;
}
.macro-summary-counts .mapped-count {
color: var(--vscode-charts-green, #89d185);
}
.macro-summary-counts .unmapped-count {
color: var(--vscode-charts-orange, #cca700);
}
.macros-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
}
.macro-item {
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border, transparent);
border-radius: 6px;
overflow: hidden;
}
.macro-item.mapped {
border-left: 3px solid var(--vscode-charts-green, #89d185);
}
.macro-item.unmapped {
border-left: 3px solid var(--vscode-charts-orange, #cca700);
}
.macro-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
cursor: pointer;
transition: background 0.15s ease;
}
.macro-header:hover {
background: var(--vscode-list-hoverBackground, rgba(255, 255, 255, 0.04));
}
.toggle-icon.small {
font-size: 8px;
width: 10px;
}
.macro-name {
font-weight: 600;
font-family: var(--vscode-editor-font-family, monospace);
color: var(--vscode-foreground);
}
.macro-status-badge {
font-size: 10px;
padding: 2px 6px;
border-radius: 3px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.macro-status-badge.mapped {
background: rgba(137, 209, 133, 0.15);
color: var(--vscode-charts-green, #89d185);
}
.macro-status-badge.unmapped {
background: rgba(204, 167, 0, 0.15);
color: var(--vscode-charts-orange, #cca700);
}
.macro-count {
margin-left: auto;
font-size: 11px;
color: var(--vscode-descriptionForeground);
}
.macro-usages {
padding: 0 12px 12px 30px;
}
.macro-usages-header {
font-size: 11px;
color: var(--vscode-descriptionForeground);
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid var(--vscode-input-border, rgba(255, 255, 255, 0.1));
}
.macro-posts-label {
font-style: italic;
}
.macro-usages-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.macro-usage {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 8px 10px;
border-radius: 4px;
border-left: 3px solid transparent;
background: var(--vscode-sideBar-background);
}
.macro-usage.valid {
border-left-color: var(--vscode-charts-green, #89d185);
}
.macro-usage.invalid {
border-left-color: var(--vscode-errorForeground, #f85149);
background: rgba(248, 81, 73, 0.08);
}
.macro-usage.unknown {
border-left-color: var(--vscode-descriptionForeground);
}
.macro-usage-params {
display: flex;
flex-wrap: wrap;
gap: 6px;
font-family: var(--vscode-editor-font-family, monospace);
font-size: 12px;
}
.no-params {
color: var(--vscode-descriptionForeground);
font-style: italic;
font-family: inherit;
}
.macro-param {
display: inline-flex;
align-items: center;
gap: 1px;
}
.param-key {
color: var(--vscode-symbolIcon-propertyForeground, #9cdcfe);
}
.param-value {
color: var(--vscode-symbolIcon-stringForeground, #ce9178);
}
.macro-usage-meta {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.validation-badge {
width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
}
.validation-badge.valid {
background: rgba(137, 209, 133, 0.2);
color: var(--vscode-charts-green, #89d185);
}
.validation-badge.invalid {
background: rgba(248, 81, 73, 0.2);
color: var(--vscode-errorForeground, #f85149);
}
.validation-badge.unknown {
background: rgba(128, 128, 128, 0.2);
color: var(--vscode-descriptionForeground);
}
.usage-count {
font-size: 11px;
color: var(--vscode-descriptionForeground);
min-width: 24px;
text-align: right;
}

View File

@@ -11,6 +11,7 @@ interface AnalysisReport {
media: MediaSection;
categories: TaxonomyItem[];
tags: TaxonomyItem[];
macros: MacroAnalysisSummary;
}
interface ItemSection {
@@ -75,6 +76,35 @@ interface TaxonomyItem {
mappedTo?: string; // When set, indicates this item should be mapped to the given name on import
}
/** Validation status for a macro usage */
type MacroValidationStatus = 'valid' | 'invalid' | 'unknown';
/** A single unique usage pattern of a macro */
interface MacroUsage {
params: Record<string, string>;
count: number;
validationStatus: MacroValidationStatus;
validationError?: string;
paramsKey: string;
}
/** A discovered macro from the import content */
interface DiscoveredMacro {
name: string;
mapped: boolean;
totalCount: number;
usages: MacroUsage[];
postSlugs: string[];
}
/** Summary of macro analysis */
interface MacroAnalysisSummary {
total: number;
mappedCount: number;
unmappedCount: number;
discovered: DiscoveredMacro[];
}
interface ImportAnalysisViewProps {
definitionId: string;
}
@@ -368,6 +398,14 @@ export const ImportAnalysisView: React.FC<ImportAnalysisViewProps> = ({ definiti
onMappingUpdated={handleSingleMappingUpdated}
/>
)}
{report.macros && report.macros.total > 0 && (
<MacrosSection
macros={report.macros}
expanded={expandedSections['macros'] ?? false}
onToggle={() => toggleSection('macros')}
/>
)}
</>
)}
</div>
@@ -1015,3 +1053,104 @@ const TaxonomyPill: React.FC<{
</span>
);
};
/**
* MacrosSection - Shows discovered macros from import content
* Displays macro names, their different usages (parameters), and validation status
*/
const MacrosSection: React.FC<{
macros: MacroAnalysisSummary;
expanded: boolean;
onToggle: () => void;
}> = ({ macros, expanded, onToggle }) => {
const [expandedMacros, setExpandedMacros] = useState<Set<string>>(new Set());
const toggleMacro = (name: string) => {
setExpandedMacros(prev => {
const next = new Set(prev);
if (next.has(name)) {
next.delete(name);
} else {
next.add(name);
}
return next;
});
};
return (
<div className="import-detail-section">
<h3 onClick={onToggle}>
<span className={`toggle-icon ${expanded ? 'open' : ''}`}>&#9654;</span>
Macros ({macros.total})
<span className="macro-summary-counts">
<span className="mapped-count">{macros.mappedCount} mapped</span>
{macros.unmappedCount > 0 && (
<span className="unmapped-count">{macros.unmappedCount} unmapped</span>
)}
</span>
</h3>
{expanded && (
<div className="macros-list">
{macros.discovered.map(macro => (
<div key={macro.name} className={`macro-item ${macro.mapped ? 'mapped' : 'unmapped'}`}>
<div className="macro-header" onClick={() => toggleMacro(macro.name)}>
<span className={`toggle-icon small ${expandedMacros.has(macro.name) ? 'open' : ''}`}>&#9654;</span>
<span className="macro-name">[{macro.name}]</span>
<span className={`macro-status-badge ${macro.mapped ? 'mapped' : 'unmapped'}`}>
{macro.mapped ? 'Mapped' : 'Unknown'}
</span>
<span className="macro-count">{macro.totalCount} uses</span>
</div>
{expandedMacros.has(macro.name) && (
<div className="macro-usages">
<div className="macro-usages-header">
<span className="macro-posts-label">Used in: {macro.postSlugs.slice(0, 5).join(', ')}{macro.postSlugs.length > 5 ? `, +${macro.postSlugs.length - 5} more` : ''}</span>
</div>
<div className="macro-usages-list">
{macro.usages.map((usage, idx) => (
<div
key={idx}
className={`macro-usage ${getValidationClass(usage.validationStatus)}`}
title={usage.validationError || ''}
>
<div className="macro-usage-params">
{Object.keys(usage.params).length === 0 ? (
<span className="no-params">(no parameters)</span>
) : (
Object.entries(usage.params).map(([key, value]) => (
<span key={key} className="macro-param">
<span className="param-key">{key}</span>=<span className="param-value">"{value}"</span>
</span>
))
)}
</div>
<div className="macro-usage-meta">
<span className={`validation-badge ${usage.validationStatus}`}>
{usage.validationStatus === 'valid' && '✓'}
{usage.validationStatus === 'invalid' && '✗'}
{usage.validationStatus === 'unknown' && '?'}
</span>
<span className="usage-count">×{usage.count}</span>
</div>
</div>
))}
</div>
</div>
)}
</div>
))}
</div>
)}
</div>
);
};
function getValidationClass(status: MacroValidationStatus): string {
switch (status) {
case 'valid': return 'valid';
case 'invalid': return 'invalid';
case 'unknown': return 'unknown';
}
}