12 changed files with 9155 additions and 14110 deletions
@ -0,0 +1,586 @@
@@ -0,0 +1,586 @@
|
||||
import { Parser } from '../parser'; |
||||
import * as fs from 'fs'; |
||||
import * as path from 'path'; |
||||
import { ProcessResult } from '../types'; |
||||
|
||||
/** |
||||
* Shared utilities for generating test reports |
||||
*/ |
||||
|
||||
export interface TestData { |
||||
original: string; |
||||
result: ProcessResult; |
||||
} |
||||
|
||||
export interface ReportData { |
||||
markdown: TestData; |
||||
asciidoc: TestData; |
||||
} |
||||
|
||||
/** |
||||
* Generate HTML test report from parsed documents |
||||
*/ |
||||
export function generateHTMLReport(data: ReportData): string { |
||||
const { markdown, asciidoc } = data; |
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="UTF-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
<title>GC Parser Test Report</title> |
||||
<style> |
||||
* { |
||||
margin: 0; |
||||
padding: 0; |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
body { |
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; |
||||
line-height: 1.6; |
||||
color: #333; |
||||
background: #f5f5f5; |
||||
padding: 20px; |
||||
} |
||||
|
||||
.container { |
||||
max-width: 1400px; |
||||
margin: 0 auto; |
||||
} |
||||
|
||||
h1 { |
||||
color: #2c3e50; |
||||
margin-bottom: 10px; |
||||
font-size: 2.5em; |
||||
} |
||||
|
||||
.subtitle { |
||||
color: #7f8c8d; |
||||
margin-bottom: 30px; |
||||
font-size: 1.1em; |
||||
} |
||||
|
||||
.section { |
||||
background: white; |
||||
border-radius: 8px; |
||||
padding: 30px; |
||||
margin-bottom: 30px; |
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
||||
} |
||||
|
||||
.section h2 { |
||||
color: #34495e; |
||||
margin-bottom: 20px; |
||||
padding-bottom: 10px; |
||||
border-bottom: 2px solid #3498db; |
||||
font-size: 1.8em; |
||||
} |
||||
|
||||
.section h3 { |
||||
color: #2c3e50; |
||||
margin-top: 25px; |
||||
margin-bottom: 15px; |
||||
font-size: 1.3em; |
||||
} |
||||
|
||||
.tabs { |
||||
display: flex; |
||||
gap: 10px; |
||||
margin-bottom: 20px; |
||||
border-bottom: 2px solid #e0e0e0; |
||||
} |
||||
|
||||
.tab { |
||||
padding: 12px 24px; |
||||
background: #f8f9fa; |
||||
border: none; |
||||
border-top-left-radius: 6px; |
||||
border-top-right-radius: 6px; |
||||
cursor: pointer; |
||||
font-size: 1em; |
||||
font-weight: 500; |
||||
color: #555; |
||||
transition: all 0.2s; |
||||
} |
||||
|
||||
.tab:hover { |
||||
background: #e9ecef; |
||||
} |
||||
|
||||
.tab.active { |
||||
background: #3498db; |
||||
color: white; |
||||
} |
||||
|
||||
.tab-content { |
||||
display: none; |
||||
} |
||||
|
||||
.tab-content.active { |
||||
display: block; |
||||
} |
||||
|
||||
.metadata-grid { |
||||
display: grid; |
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
||||
gap: 15px; |
||||
margin-top: 15px; |
||||
} |
||||
|
||||
.metadata-item { |
||||
background: #f8f9fa; |
||||
padding: 12px; |
||||
border-radius: 4px; |
||||
border-left: 3px solid #3498db; |
||||
} |
||||
|
||||
.metadata-item strong { |
||||
color: #2c3e50; |
||||
display: block; |
||||
margin-bottom: 5px; |
||||
} |
||||
|
||||
.metadata-item code { |
||||
background: #e9ecef; |
||||
padding: 2px 6px; |
||||
border-radius: 3px; |
||||
font-size: 0.9em; |
||||
} |
||||
|
||||
.code-block { |
||||
background: #2d2d2d; |
||||
color: #f8f8f2; |
||||
padding: 15px; |
||||
border-radius: 6px; |
||||
overflow-x: auto; |
||||
font-family: 'Courier New', monospace; |
||||
font-size: 0.9em; |
||||
line-height: 1.5; |
||||
margin: 15px 0; |
||||
max-height: 400px; |
||||
overflow-y: auto; |
||||
} |
||||
|
||||
.code-block pre { |
||||
margin: 0; |
||||
white-space: pre-wrap; |
||||
word-wrap: break-word; |
||||
} |
||||
|
||||
.rendered-output { |
||||
background: white; |
||||
border: 1px solid #ddd; |
||||
padding: 20px; |
||||
border-radius: 6px; |
||||
margin: 15px 0; |
||||
min-height: 200px; |
||||
} |
||||
|
||||
.rendered-output * { |
||||
max-width: 100%; |
||||
} |
||||
|
||||
.stats { |
||||
display: grid; |
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); |
||||
gap: 15px; |
||||
margin-top: 20px; |
||||
} |
||||
|
||||
.stat-card { |
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
color: white; |
||||
padding: 20px; |
||||
border-radius: 8px; |
||||
text-align: center; |
||||
} |
||||
|
||||
.stat-card .number { |
||||
font-size: 2.5em; |
||||
font-weight: bold; |
||||
margin-bottom: 5px; |
||||
} |
||||
|
||||
.stat-card .label { |
||||
font-size: 0.9em; |
||||
opacity: 0.9; |
||||
} |
||||
|
||||
.list-item { |
||||
background: #f8f9fa; |
||||
padding: 8px 12px; |
||||
margin: 5px 0; |
||||
border-radius: 4px; |
||||
border-left: 3px solid #95a5a6; |
||||
} |
||||
|
||||
.list-item code { |
||||
background: #e9ecef; |
||||
padding: 2px 6px; |
||||
border-radius: 3px; |
||||
font-size: 0.85em; |
||||
} |
||||
|
||||
.success-badge { |
||||
display: inline-block; |
||||
background: #27ae60; |
||||
color: white; |
||||
padding: 4px 12px; |
||||
border-radius: 12px; |
||||
font-size: 0.85em; |
||||
font-weight: 500; |
||||
margin-left: 10px; |
||||
} |
||||
|
||||
.warning-badge { |
||||
display: inline-block; |
||||
background: #f39c12; |
||||
color: white; |
||||
padding: 4px 12px; |
||||
border-radius: 12px; |
||||
font-size: 0.85em; |
||||
font-weight: 500; |
||||
margin-left: 10px; |
||||
} |
||||
|
||||
.comparison { |
||||
display: grid; |
||||
grid-template-columns: 1fr 1fr; |
||||
gap: 20px; |
||||
margin-top: 20px; |
||||
} |
||||
|
||||
@media (max-width: 768px) { |
||||
.comparison { |
||||
grid-template-columns: 1fr; |
||||
} |
||||
} |
||||
|
||||
.json-view { |
||||
background: #f8f9fa; |
||||
padding: 15px; |
||||
border-radius: 6px; |
||||
overflow-x: auto; |
||||
font-family: 'Courier New', monospace; |
||||
font-size: 0.85em; |
||||
max-height: 300px; |
||||
overflow-y: auto; |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
<div class="container"> |
||||
<h1>GC Parser Test Report</h1> |
||||
<p class="subtitle">Generated: ${new Date().toLocaleString()}</p> |
||||
|
||||
<!-- Markdown Section --> |
||||
<div class="section"> |
||||
<h2>Markdown Document Test <span class="success-badge">✓ Parsed</span></h2> |
||||
|
||||
<div class="tabs"> |
||||
<button class="tab active" onclick="showTab('md-overview')">Overview</button> |
||||
<button class="tab" onclick="showTab('md-original')">Original Content</button> |
||||
<button class="tab" onclick="showTab('md-rendered')">Rendered Output</button> |
||||
<button class="tab" onclick="showTab('md-metadata')">Metadata</button> |
||||
</div> |
||||
|
||||
<div id="md-overview" class="tab-content active"> |
||||
<div class="stats"> |
||||
<div class="stat-card"> |
||||
<div class="number">${markdown.result.nostrLinks.length}</div> |
||||
<div class="label">Nostr Links</div> |
||||
</div> |
||||
<div class="stat-card"> |
||||
<div class="number">${markdown.result.wikilinks.length}</div> |
||||
<div class="label">Wikilinks</div> |
||||
</div> |
||||
<div class="stat-card"> |
||||
<div class="number">${markdown.result.hashtags.length}</div> |
||||
<div class="label">Hashtags</div> |
||||
</div> |
||||
<div class="stat-card"> |
||||
<div class="number">${markdown.result.links.length}</div> |
||||
<div class="label">Links</div> |
||||
</div> |
||||
<div class="stat-card"> |
||||
<div class="number">${markdown.result.media.length}</div> |
||||
<div class="label">Media URLs</div> |
||||
</div> |
||||
<div class="stat-card"> |
||||
<div class="number">${markdown.result.hasLaTeX ? 'Yes' : 'No'}</div> |
||||
<div class="label">Has LaTeX</div> |
||||
</div> |
||||
<div class="stat-card"> |
||||
<div class="number">${markdown.result.hasMusicalNotation ? 'Yes' : 'No'}</div> |
||||
<div class="label">Has Music</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<h3>Frontmatter</h3> |
||||
${markdown.result.frontmatter ? ` |
||||
<div class="metadata-grid"> |
||||
${Object.entries(markdown.result.frontmatter).map(([key, value]) => ` |
||||
<div class="metadata-item"> |
||||
<strong>${escapeHtml(key)}</strong> |
||||
<code>${escapeHtml(JSON.stringify(value))}</code> |
||||
</div> |
||||
`).join('')}
|
||||
</div> |
||||
` : '<p><em>No frontmatter found</em></p>'}
|
||||
</div> |
||||
|
||||
<div id="md-original" class="tab-content"> |
||||
<h3>Original Markdown Content</h3> |
||||
<div class="code-block"> |
||||
<pre>${escapeHtml(markdown.original)}</pre> |
||||
</div> |
||||
</div> |
||||
|
||||
<div id="md-rendered" class="tab-content"> |
||||
<h3>Rendered HTML Output</h3> |
||||
<div class="rendered-output"> |
||||
${markdown.result.content} |
||||
</div> |
||||
<details style="margin-top: 15px;"> |
||||
<summary style="cursor: pointer; color: #3498db; font-weight: 500;">View Raw HTML</summary> |
||||
<div class="code-block" style="margin-top: 10px;"> |
||||
<pre>${escapeHtml(markdown.result.content)}</pre> |
||||
</div> |
||||
</details> |
||||
</div> |
||||
|
||||
<div id="md-metadata" class="tab-content"> |
||||
<h3>Extracted Metadata</h3> |
||||
|
||||
${markdown.result.nostrLinks.length > 0 ? ` |
||||
<h4>Nostr Links (${markdown.result.nostrLinks.length})</h4> |
||||
${markdown.result.nostrLinks.map((link: any) => ` |
||||
<div class="list-item"> |
||||
<strong>${escapeHtml(link.type)}</strong>: <code>${escapeHtml(link.bech32)}</code> |
||||
${link.text ? ` - ${escapeHtml(link.text)}` : ''} |
||||
</div> |
||||
`).join('')}
|
||||
` : ''}
|
||||
|
||||
${markdown.result.wikilinks.length > 0 ? ` |
||||
<h4>Wikilinks (${markdown.result.wikilinks.length})</h4> |
||||
${markdown.result.wikilinks.map((wl: any) => ` |
||||
<div class="list-item"> |
||||
<code>${escapeHtml(wl.original)}</code> → dtag: <code>${escapeHtml(wl.dtag)}</code> |
||||
${wl.display ? ` (display: ${escapeHtml(wl.display)})` : ''} |
||||
</div> |
||||
`).join('')}
|
||||
` : ''}
|
||||
|
||||
${markdown.result.hashtags.length > 0 ? ` |
||||
<h4>Hashtags (${markdown.result.hashtags.length})</h4> |
||||
${markdown.result.hashtags.map((tag: string) => ` |
||||
<div class="list-item"> |
||||
<code>#${escapeHtml(tag)}</code> |
||||
</div> |
||||
`).join('')}
|
||||
` : ''}
|
||||
|
||||
${markdown.result.links.length > 0 ? ` |
||||
<h4>Links (${markdown.result.links.length})</h4> |
||||
${markdown.result.links.map((link: any) => ` |
||||
<div class="list-item"> |
||||
<a href="${escapeHtml(link.url)}" target="_blank">${escapeHtml(link.text || link.url)}</a> |
||||
${link.isExternal ? '<span class="warning-badge">External</span>' : ''} |
||||
</div> |
||||
`).join('')}
|
||||
` : ''}
|
||||
|
||||
${markdown.result.media.length > 0 ? ` |
||||
<h4>Media URLs (${markdown.result.media.length})</h4> |
||||
${markdown.result.media.map((url: string) => ` |
||||
<div class="list-item"> |
||||
<a href="${escapeHtml(url)}" target="_blank">${escapeHtml(url)}</a> |
||||
</div> |
||||
`).join('')}
|
||||
` : ''}
|
||||
|
||||
${markdown.result.tableOfContents ? ` |
||||
<h4>Table of Contents</h4> |
||||
<div class="rendered-output"> |
||||
${markdown.result.tableOfContents} |
||||
</div> |
||||
` : ''}
|
||||
</div> |
||||
</div> |
||||
|
||||
<!-- AsciiDoc Section --> |
||||
<div class="section"> |
||||
<h2>AsciiDoc Document Test <span class="success-badge">✓ Parsed</span></h2> |
||||
|
||||
<div class="tabs"> |
||||
<button class="tab active" onclick="showTab('ad-overview')">Overview</button> |
||||
<button class="tab" onclick="showTab('ad-original')">Original Content</button> |
||||
<button class="tab" onclick="showTab('ad-rendered')">Rendered Output</button> |
||||
<button class="tab" onclick="showTab('ad-metadata')">Metadata</button> |
||||
</div> |
||||
|
||||
<div id="ad-overview" class="tab-content active"> |
||||
<div class="stats"> |
||||
<div class="stat-card"> |
||||
<div class="number">${asciidoc.result.nostrLinks.length}</div> |
||||
<div class="label">Nostr Links</div> |
||||
</div> |
||||
<div class="stat-card"> |
||||
<div class="number">${asciidoc.result.wikilinks.length}</div> |
||||
<div class="label">Wikilinks</div> |
||||
</div> |
||||
<div class="stat-card"> |
||||
<div class="number">${asciidoc.result.hashtags.length}</div> |
||||
<div class="label">Hashtags</div> |
||||
</div> |
||||
<div class="stat-card"> |
||||
<div class="number">${asciidoc.result.links.length}</div> |
||||
<div class="label">Links</div> |
||||
</div> |
||||
<div class="stat-card"> |
||||
<div class="number">${asciidoc.result.media.length}</div> |
||||
<div class="label">Media URLs</div> |
||||
</div> |
||||
<div class="stat-card"> |
||||
<div class="number">${asciidoc.result.hasLaTeX ? 'Yes' : 'No'}</div> |
||||
<div class="label">Has LaTeX</div> |
||||
</div> |
||||
<div class="stat-card"> |
||||
<div class="number">${asciidoc.result.hasMusicalNotation ? 'Yes' : 'No'}</div> |
||||
<div class="label">Has Music</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<h3>Frontmatter</h3> |
||||
${asciidoc.result.frontmatter ? ` |
||||
<div class="metadata-grid"> |
||||
${Object.entries(asciidoc.result.frontmatter).map(([key, value]) => ` |
||||
<div class="metadata-item"> |
||||
<strong>${escapeHtml(key)}</strong> |
||||
<code>${escapeHtml(JSON.stringify(value))}</code> |
||||
</div> |
||||
`).join('')}
|
||||
</div> |
||||
` : '<p><em>No frontmatter found</em></p>'}
|
||||
</div> |
||||
|
||||
<div id="ad-original" class="tab-content"> |
||||
<h3>Original AsciiDoc Content</h3> |
||||
<div class="code-block"> |
||||
<pre>${escapeHtml(asciidoc.original)}</pre> |
||||
</div> |
||||
</div> |
||||
|
||||
<div id="ad-rendered" class="tab-content"> |
||||
<h3>Rendered HTML Output</h3> |
||||
<div class="rendered-output"> |
||||
${asciidoc.result.content} |
||||
</div> |
||||
<details style="margin-top: 15px;"> |
||||
<summary style="cursor: pointer; color: #3498db; font-weight: 500;">View Raw HTML</summary> |
||||
<div class="code-block" style="margin-top: 10px;"> |
||||
<pre>${escapeHtml(asciidoc.result.content)}</pre> |
||||
</div> |
||||
</details> |
||||
</div> |
||||
|
||||
<div id="ad-metadata" class="tab-content"> |
||||
<h3>Extracted Metadata</h3> |
||||
|
||||
${asciidoc.result.nostrLinks.length > 0 ? ` |
||||
<h4>Nostr Links (${asciidoc.result.nostrLinks.length})</h4> |
||||
${asciidoc.result.nostrLinks.map((link: any) => ` |
||||
<div class="list-item"> |
||||
<strong>${escapeHtml(link.type)}</strong>: <code>${escapeHtml(link.bech32)}</code> |
||||
${link.text ? ` - ${escapeHtml(link.text)}` : ''} |
||||
</div> |
||||
`).join('')}
|
||||
` : ''}
|
||||
|
||||
${asciidoc.result.wikilinks.length > 0 ? ` |
||||
<h4>Wikilinks (${asciidoc.result.wikilinks.length})</h4> |
||||
${asciidoc.result.wikilinks.map((wl: any) => ` |
||||
<div class="list-item"> |
||||
<code>${escapeHtml(wl.original)}</code> → dtag: <code>${escapeHtml(wl.dtag)}</code> |
||||
${wl.display ? ` (display: ${escapeHtml(wl.display)})` : ''} |
||||
</div> |
||||
`).join('')}
|
||||
` : ''}
|
||||
|
||||
${asciidoc.result.hashtags.length > 0 ? ` |
||||
<h4>Hashtags (${asciidoc.result.hashtags.length})</h4> |
||||
${asciidoc.result.hashtags.map((tag: string) => ` |
||||
<div class="list-item"> |
||||
<code>#${escapeHtml(tag)}</code> |
||||
</div> |
||||
`).join('')}
|
||||
` : ''}
|
||||
|
||||
${asciidoc.result.links.length > 0 ? ` |
||||
<h4>Links (${asciidoc.result.links.length})</h4> |
||||
${asciidoc.result.links.map((link: any) => ` |
||||
<div class="list-item"> |
||||
<a href="${escapeHtml(link.url)}" target="_blank">${escapeHtml(link.text || link.url)}</a> |
||||
${link.isExternal ? '<span class="warning-badge">External</span>' : ''} |
||||
</div> |
||||
`).join('')}
|
||||
` : ''}
|
||||
|
||||
${asciidoc.result.media.length > 0 ? ` |
||||
<h4>Media URLs (${asciidoc.result.media.length})</h4> |
||||
${asciidoc.result.media.map((url: string) => ` |
||||
<div class="list-item"> |
||||
<a href="${escapeHtml(url)}" target="_blank">${escapeHtml(url)}</a> |
||||
</div> |
||||
`).join('')}
|
||||
` : ''}
|
||||
|
||||
${asciidoc.result.tableOfContents ? ` |
||||
<h4>Table of Contents</h4> |
||||
<div class="rendered-output"> |
||||
${asciidoc.result.tableOfContents} |
||||
</div> |
||||
` : ''}
|
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<script> |
||||
function showTab(tabId) { |
||||
// Hide all tab contents
|
||||
const allContents = document.querySelectorAll('.tab-content'); |
||||
allContents.forEach(content => content.classList.remove('active')); |
||||
|
||||
// Remove active class from all tabs
|
||||
const allTabs = document.querySelectorAll('.tab'); |
||||
allTabs.forEach(tab => tab.classList.remove('active')); |
||||
|
||||
// Show selected tab content
|
||||
const selectedContent = document.getElementById(tabId); |
||||
if (selectedContent) { |
||||
selectedContent.classList.add('active'); |
||||
} |
||||
|
||||
// Add active class to clicked tab
|
||||
event.target.classList.add('active'); |
||||
} |
||||
</script> |
||||
</body> |
||||
</html>`;
|
||||
} |
||||
|
||||
/** |
||||
* Escape HTML special characters |
||||
*/ |
||||
export function escapeHtml(text: string): string { |
||||
const map: Record<string, string> = { |
||||
'&': '&', |
||||
'<': '<', |
||||
'>': '>', |
||||
'"': '"', |
||||
"'": ''', |
||||
}; |
||||
return text.replace(/[&<>"']/g, (m) => map[m]); |
||||
} |
||||
Loading…
Reference in new issue