Skip to content

Commit 082e03c

Browse files
committed
file-review: render frontmatter metadata card and bump v1.5.3
1 parent 75aa349 commit 082e03c

8 files changed

Lines changed: 368 additions & 10 deletions

File tree

file-review/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "file-review",
33
"private": true,
4-
"version": "1.5.2",
4+
"version": "1.5.3",
55
"type": "module",
66
"scripts": {
77
"dev": "tauri dev --",

file-review/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

file-review/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "file-review"
3-
version = "1.5.2"
3+
version = "1.5.3"
44
description = "A Tauri application for reviewing and managing files."
55
authors = ["contact@desplega.ai"]
66
edition = "2021"

file-review/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "file-review",
4-
"version": "1.5.2",
4+
"version": "1.5.3",
55
"identifier": "com.desplega.file-review",
66
"build": {
77
"beforeDevCommand": "bun run dev:vite",

file-review/src/markdown-preview.ts

Lines changed: 187 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ export interface RenderPreviewResult {
3535
ranges: CommentableRange[];
3636
}
3737

38+
interface FrontmatterEntry {
39+
key: string;
40+
label: string;
41+
value: string | string[];
42+
isArray: boolean;
43+
}
44+
45+
interface FrontmatterParseResult {
46+
entries: FrontmatterEntry[];
47+
bodyMarkdown: string;
48+
consumedChars: number;
49+
}
50+
3851
export function initPreview(container: HTMLElement) {
3952
previewContainer = container;
4053
}
@@ -172,6 +185,168 @@ function injectHighlightSpans(content: string, comments: ReviewComment[]): strin
172185
return result;
173186
}
174187

188+
function escapeHtml(value: string): string {
189+
return value
190+
.replace(/&/g, '&')
191+
.replace(/</g, '&lt;')
192+
.replace(/>/g, '&gt;')
193+
.replace(/"/g, '&quot;')
194+
.replace(/'/g, '&#39;');
195+
}
196+
197+
function toFrontmatterLabel(key: string): string {
198+
return key
199+
.split(/[_-]+/)
200+
.filter((part) => part.length > 0)
201+
.map((part) => part[0].toUpperCase() + part.slice(1))
202+
.join(' ');
203+
}
204+
205+
function parseArrayValue(rawValue: string): string[] {
206+
const inner = rawValue.slice(1, -1).trim();
207+
if (!inner) {
208+
return [];
209+
}
210+
211+
return inner
212+
.split(',')
213+
.map((part) => part.trim())
214+
.filter((part) => part.length > 0)
215+
.map((part) => {
216+
const quote = part[0];
217+
if (
218+
(quote === '"' || quote === "'") &&
219+
part.length >= 2 &&
220+
part[part.length - 1] === quote
221+
) {
222+
return part.slice(1, -1);
223+
}
224+
return part;
225+
});
226+
}
227+
228+
function parseLeadingFrontmatter(content: string): FrontmatterParseResult {
229+
const bomOffset = content.startsWith('\uFEFF') ? 1 : 0;
230+
const working = content.slice(bomOffset);
231+
232+
if (!(working.startsWith('---\n') || working.startsWith('---\r\n'))) {
233+
return { entries: [], bodyMarkdown: content, consumedChars: 0 };
234+
}
235+
236+
const afterOpening = working.startsWith('---\r\n') ? 5 : 4;
237+
let cursor = afterOpening;
238+
let closingLineEnd = -1;
239+
240+
while (cursor < working.length) {
241+
const nextNewline = working.indexOf('\n', cursor);
242+
const lineEnd = nextNewline === -1 ? working.length : nextNewline + 1;
243+
const line = working
244+
.slice(cursor, nextNewline === -1 ? working.length : nextNewline)
245+
.replace(/\r$/, '');
246+
247+
if (/^---[ \t]*$/.test(line)) {
248+
closingLineEnd = lineEnd;
249+
break;
250+
}
251+
252+
cursor = lineEnd;
253+
}
254+
255+
if (closingLineEnd < 0) {
256+
return { entries: [], bodyMarkdown: content, consumedChars: 0 };
257+
}
258+
259+
const rawFrontmatter = working.slice(afterOpening, cursor);
260+
const entries: FrontmatterEntry[] = [];
261+
262+
for (const rawLine of rawFrontmatter.split(/\r?\n/)) {
263+
if (!rawLine.trim() || rawLine.trim().startsWith('#')) {
264+
continue;
265+
}
266+
267+
const separatorIndex = rawLine.indexOf(':');
268+
if (separatorIndex <= 0) {
269+
continue;
270+
}
271+
272+
const key = rawLine.slice(0, separatorIndex).trim();
273+
if (!key) {
274+
continue;
275+
}
276+
277+
const rawValue = rawLine.slice(separatorIndex + 1).trim();
278+
const isArray = rawValue.startsWith('[') && rawValue.endsWith(']');
279+
280+
entries.push({
281+
key,
282+
label: toFrontmatterLabel(key),
283+
value: isArray ? parseArrayValue(rawValue) : rawValue,
284+
isArray,
285+
});
286+
}
287+
288+
const consumedChars = bomOffset + closingLineEnd;
289+
const bodyMarkdown = content.slice(consumedChars);
290+
291+
return { entries, bodyMarkdown, consumedChars };
292+
}
293+
294+
function renderFrontmatterHtml(entries: FrontmatterEntry[]): string {
295+
if (entries.length === 0) {
296+
return '';
297+
}
298+
299+
const rows = entries
300+
.map((entry) => {
301+
const renderedValue = entry.isArray
302+
? (() => {
303+
const items = entry.value as string[];
304+
if (items.length === 0) {
305+
return '<span class="frontmatter-empty">-</span>';
306+
}
307+
308+
const chips = items
309+
.map((item) => `<span class="frontmatter-chip">${escapeHtml(item)}</span>`)
310+
.join('');
311+
return `<span class="frontmatter-chip-list">${chips}</span>`;
312+
})()
313+
: (() => {
314+
const value = entry.value as string;
315+
if (!value) {
316+
return '<span class="frontmatter-empty">-</span>';
317+
}
318+
return `<span class="frontmatter-text">${escapeHtml(value)}</span>`;
319+
})();
320+
321+
return [
322+
'<div class="frontmatter-row">',
323+
`<span class="frontmatter-label">${escapeHtml(entry.label)}</span>`,
324+
`<span class="frontmatter-value">${renderedValue}</span>`,
325+
'</div>',
326+
].join('');
327+
})
328+
.join('');
329+
330+
return [
331+
'<div class="frontmatter-card" data-frontmatter="true">',
332+
'<div class="frontmatter-title">Metadata</div>',
333+
`<div class="frontmatter-grid">${rows}</div>`,
334+
'</div>',
335+
].join('');
336+
}
337+
338+
function offsetRanges(ranges: CommentableRange[], offset: number): CommentableRange[] {
339+
if (offset === 0) {
340+
return ranges;
341+
}
342+
343+
return ranges.map((range) => ({
344+
...range,
345+
start: range.start + offset,
346+
end: range.end + offset,
347+
}));
348+
}
349+
175350
function trimTrailingLineBreaks(raw: string): number {
176351
let end = raw.length;
177352
while (end > 0) {
@@ -421,19 +596,26 @@ function applyCommentHighlights(root: ParentNode, comments: ReviewComment[]) {
421596
* Render markdown with highlights
422597
*/
423598
export function renderMarkdown(content: string, comments: ReviewComment[]): RenderPreviewResult {
424-
// Step 1: Replace comment markers with HTML spans BEFORE markdown parsing
425-
const contentWithSpans = injectHighlightSpans(content, comments);
599+
const frontmatter = parseLeadingFrontmatter(content);
426600

427-
// Step 2: Compute exact source ranges from the original markdown content.
428-
const ranges = collectCommentableRanges(content);
601+
// Step 1: Replace comment markers with HTML spans BEFORE markdown parsing.
602+
const contentWithSpans = injectHighlightSpans(frontmatter.bodyMarkdown, comments);
603+
604+
// Step 2: Compute exact source ranges from markdown body, then remap to original source offsets.
605+
const ranges = offsetRanges(
606+
collectCommentableRanges(frontmatter.bodyMarkdown),
607+
frontmatter.consumedChars
608+
);
429609

430610
// Step 3: Parse markdown - marked preserves inline HTML by default.
431-
const html = marked.parse(contentWithSpans, {
611+
const markdownHtml = marked.parse(contentWithSpans, {
432612
async: false,
433613
gfm: true,
434614
breaks: true,
435615
}) as string;
436616

617+
const html = renderFrontmatterHtml(frontmatter.entries) + markdownHtml;
618+
437619
return { html, ranges };
438620
}
439621

file-review/src/styles.css

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,78 @@ body {
148148
color: var(--text-primary);
149149
}
150150

151+
#preview-container .frontmatter-card {
152+
margin-bottom: 1.5em;
153+
border: 1px solid var(--border-color);
154+
background: var(--bg-secondary);
155+
border-radius: 8px;
156+
overflow: hidden;
157+
}
158+
159+
#preview-container .frontmatter-title {
160+
padding: 10px 14px;
161+
border-bottom: 1px solid var(--border-color);
162+
font-size: 12px;
163+
font-weight: 700;
164+
letter-spacing: 0.04em;
165+
text-transform: uppercase;
166+
color: var(--text-secondary);
167+
}
168+
169+
#preview-container .frontmatter-grid {
170+
display: grid;
171+
}
172+
173+
#preview-container .frontmatter-row {
174+
display: grid;
175+
grid-template-columns: minmax(140px, 220px) 1fr;
176+
gap: 12px;
177+
padding: 10px 14px;
178+
border-top: 1px solid var(--border-color);
179+
}
180+
181+
#preview-container .frontmatter-row:first-child {
182+
border-top: none;
183+
}
184+
185+
#preview-container .frontmatter-label {
186+
color: var(--text-secondary);
187+
font-size: 13px;
188+
font-weight: 600;
189+
}
190+
191+
#preview-container .frontmatter-value {
192+
font-size: 13px;
193+
color: var(--text-primary);
194+
}
195+
196+
#preview-container .frontmatter-text {
197+
font-family: 'SF Mono', Monaco, Consolas, monospace;
198+
font-size: 12px;
199+
overflow-wrap: anywhere;
200+
}
201+
202+
#preview-container .frontmatter-chip-list {
203+
display: flex;
204+
flex-wrap: wrap;
205+
gap: 6px;
206+
}
207+
208+
#preview-container .frontmatter-chip {
209+
display: inline-flex;
210+
align-items: center;
211+
border: 1px solid var(--border-color);
212+
background: var(--bg-tertiary);
213+
border-radius: 999px;
214+
padding: 2px 8px;
215+
font-size: 12px;
216+
line-height: 1.4;
217+
}
218+
219+
#preview-container .frontmatter-empty {
220+
color: var(--text-secondary);
221+
}
222+
151223
#preview-container h1,
152224
#preview-container h2,
153225
#preview-container h3,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
date: 2026-02-19T11:00:00+00:00
3+
researcher: Claude
4+
git_commit: e7efbbc12da967192eaba6dc7517850cedd19206
5+
branch: main
6+
repository: cope
7+
topic: "TestAgentV2 Speedrun Challenge Failures - Root Cause Analysis"
8+
tags: [research, test-agent-v2, drag-and-drop, set-input-files, loading-detection, speedrun]
9+
status: complete
10+
autonomy: critical
11+
last_updated: 2026-02-19
12+
last_updated_by: Claude
13+
---
14+
15+
# Frontmatter Visual Regression Fixture
16+
17+
Use this file in preview mode to quickly verify:
18+
19+
- metadata card renders instead of raw YAML
20+
- array values are shown as chips
21+
- long scalar values wrap cleanly
22+
- body markdown still renders and remains commentable
23+
24+
## Body Content
25+
26+
This paragraph should appear after the metadata card.
27+
28+
| Case | Result |
29+
| --- | --- |
30+
| Leading frontmatter | Card |
31+
| Non-leading delimiter | Normal markdown |
32+

0 commit comments

Comments
 (0)