@sverka/sarif-viewer-web 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bin.mjs","names":[],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n// @sverka/sarif-viewer-web — CLI entry point. Spec 47.\n\nimport process from \"node:process\";\nimport { readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { resolveFindings } from \"./input.js\";\nimport { generateSarifHtml } from \"./viewer.js\";\n\nfunction parseArgs(argv: string[]): {
|
|
1
|
+
{"version":3,"file":"bin.mjs","names":[],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n// @sverka/sarif-viewer-web — CLI entry point. Spec 47.\n\nimport process from \"node:process\";\nimport { readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { resolveFindings } from \"./input.js\";\nimport { generateSarifHtml } from \"./viewer.js\";\n\nfunction parseArgs(argv: string[]): {\n inputPath: string | null;\n outputPath: string;\n} {\n let inputPath: string | null = null;\n let outputPath = \"sarif-report.html\";\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === undefined) continue;\n if (arg === \"-o\" || arg === \"--output\") {\n const next = argv[i + 1];\n if (next === undefined || next.startsWith(\"-\")) {\n process.stderr.write(\n \"sarif-viewer-web: -o requires a file path argument.\\n\",\n );\n process.exit(1);\n }\n outputPath = next;\n i++;\n } else if (arg === \"--help\" || arg === \"-h\") {\n process.stdout.write(\n \"Usage: sarif-viewer-web <file> [-o report.html]\\n Reads SARIF from a file argument.\\n Writes HTML to -o path (default: sarif-report.html).\\n\",\n );\n process.exit(0);\n } else if (!arg.startsWith(\"-\")) {\n if (inputPath !== null) {\n process.stderr.write(\n \"sarif-viewer-web: only one input file may be provided.\\n\",\n );\n process.exit(1);\n }\n inputPath = arg;\n } else {\n process.stderr.write(`sarif-viewer-web: unknown option: ${arg}\\n`);\n process.exit(1);\n }\n }\n return { inputPath, outputPath };\n}\n\nfunction main(): void {\n const { inputPath, outputPath } = parseArgs(process.argv.slice(2));\n\n let findings;\n try {\n if (inputPath !== null) {\n const raw = readFileSync(inputPath, \"utf8\");\n const parsed: unknown = JSON.parse(raw);\n findings = resolveFindings({ sarif: parsed });\n } else if (!process.stdin.isTTY) {\n const raw = readFileSync(0, \"utf8\");\n if (raw.trim() === \"\") {\n process.stderr.write(\"sarif-viewer-web: stdin is empty.\\n\");\n process.exit(1);\n }\n const parsed: unknown = JSON.parse(raw);\n findings = resolveFindings({ sarif: parsed });\n } else {\n process.stderr.write(\n \"sarif-viewer-web: no input. Provide a SARIF file argument or pipe SARIF via stdin.\\n\",\n );\n process.exit(1);\n }\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`sarif-viewer-web: ${msg}\\n`);\n process.exit(1);\n }\n\n try {\n const html = generateSarifHtml(findings);\n mkdirSync(dirname(outputPath), { recursive: true });\n writeFileSync(outputPath, html, \"utf-8\");\n process.stdout.write(`Wrote ${outputPath}\\n`);\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`sarif-viewer-web: failed to write HTML: ${msg}\\n`);\n process.exit(1);\n }\n}\n\nmain();\n"],"mappings":";;;;;;;AASA,SAAS,UAAU,MAGjB;CACA,IAAI,YAA2B;CAC/B,IAAI,aAAa;CACjB,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EACjB,IAAI,QAAQ,KAAA,GAAW;EACvB,IAAI,QAAQ,QAAQ,QAAQ,YAAY;GACtC,MAAM,OAAO,KAAK,IAAI;GACtB,IAAI,SAAS,KAAA,KAAa,KAAK,WAAW,GAAG,GAAG;IAC9C,QAAQ,OAAO,MACb,uDACF;IACA,QAAQ,KAAK,CAAC;GAChB;GACA,aAAa;GACb;EACF,OAAO,IAAI,QAAQ,YAAY,QAAQ,MAAM;GAC3C,QAAQ,OAAO,MACb,gJACF;GACA,QAAQ,KAAK,CAAC;EAChB,OAAO,IAAI,CAAC,IAAI,WAAW,GAAG,GAAG;GAC/B,IAAI,cAAc,MAAM;IACtB,QAAQ,OAAO,MACb,0DACF;IACA,QAAQ,KAAK,CAAC;GAChB;GACA,YAAY;EACd,OAAO;GACL,QAAQ,OAAO,MAAM,qCAAqC,IAAI,GAAG;GACjE,QAAQ,KAAK,CAAC;EAChB;CACF;CACA,OAAO;EAAE;EAAW;CAAW;AACjC;AAEA,SAAS,OAAa;CACpB,MAAM,EAAE,WAAW,eAAe,UAAU,QAAQ,KAAK,MAAM,CAAC,CAAC;CAEjE,IAAI;CACJ,IAAI;EACF,IAAI,cAAc,MAAM;GACtB,MAAM,MAAM,aAAa,WAAW,MAAM;GAC1C,MAAM,SAAkB,KAAK,MAAM,GAAG;GACtC,WAAW,gBAAgB,EAAE,OAAO,OAAO,CAAC;EAC9C,OAAO,IAAI,CAAC,QAAQ,MAAM,OAAO;GAC/B,MAAM,MAAM,aAAa,GAAG,MAAM;GAClC,IAAI,IAAI,KAAK,MAAM,IAAI;IACrB,QAAQ,OAAO,MAAM,qCAAqC;IAC1D,QAAQ,KAAK,CAAC;GAChB;GACA,MAAM,SAAkB,KAAK,MAAM,GAAG;GACtC,WAAW,gBAAgB,EAAE,OAAO,OAAO,CAAC;EAC9C,OAAO;GACL,QAAQ,OAAO,MACb,sFACF;GACA,QAAQ,KAAK,CAAC;EAChB;CACF,SAAS,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;EACrD,QAAQ,OAAO,MAAM,qBAAqB,IAAI,GAAG;EACjD,QAAQ,KAAK,CAAC;CAChB;CAEA,IAAI;EACF,MAAM,OAAO,kBAAkB,QAAQ;EACvC,UAAU,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;EAClD,cAAc,YAAY,MAAM,OAAO;EACvC,QAAQ,OAAO,MAAM,SAAS,WAAW,GAAG;CAC9C,SAAS,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;EACrD,QAAQ,OAAO,MAAM,2CAA2C,IAAI,GAAG;EACvE,QAAQ,KAAK,CAAC;CAChB;AACF;AAEA,KAAK"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html-generator.d.mts","names":[],"sources":["../src/html-generator.ts"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"html-generator.d.mts","names":[],"sources":["../src/html-generator.ts"],"mappings":";;;;;;;;wBAqPgB,kBAAkB,mBAAmB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html-generator.mjs","names":[],"sources":["../src/html-generator.ts"],"sourcesContent":["// @sverka/sarif-viewer-web — pure HTML generation (browser-safe).\n// No Node.js imports. Safe to import from browser code.\n\nimport type { Finding } from \"@sverka/verification\";\n\n/** Escape HTML special characters to prevent XSS. Pure. */\nfunction escapeHtml(text: string): string {\n return text\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n\n/** Escape JSON data for safe embedding in <script> tags.\n * Prevents </script> breakout XSS by replacing < with \\u003c. */\nfunction escapeScriptData(json: string): string {\n return json.replaceAll(\"<\", \"\\\\u003c\");\n}\n\n/** Extract tool name from the first finding's source, or \"unknown\". */\nfunction toolName(findings: readonly Finding[]): string {\n return findings[0]?.source.tool ?? \"unknown\";\n}\n\n/** Count findings by severity for the summary header. Pure. */\nfunction severityBreakdown(findings: readonly Finding[]): Record<string, number> {\n const counts: Record<string, number> = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n };\n for (const f of findings) {\n if (f.severity in counts) {\n counts[f.severity]! += 1;\n }\n }\n return counts;\n}\n\n/** Render the summary header with total count, severity breakdown, and tool. */\nfunction renderSummary(findings: readonly Finding[]): string {\n const breakdown = severityBreakdown(findings);\n const total = findings.length;\n const tool = escapeHtml(toolName(findings));\n const parts = [\n `<span class=\"summary-count\">${total}</span> findings`,\n `<span class=\"summary-tool\">${tool}</span>`,\n `<span class=\"severity-critical\">${breakdown.critical} critical</span>`,\n `<span class=\"severity-high\">${breakdown.high} high</span>`,\n `<span class=\"severity-medium\">${breakdown.medium} medium</span>`,\n `<span class=\"severity-low\">${breakdown.low} low</span>`,\n `<span class=\"severity-info\">${breakdown.info} info</span>`,\n ];\n return `<div class=\"summary-header\">${parts.join(\" · \")}</div>`;\n}\n\n/** Render the findings table body rows. Pure. */\nfunction renderFindingsRows(findings: readonly Finding[]): string {\n if (findings.length === 0) {\n return '<tr><td colspan=\"5\" class=\"no-findings\">No findings</td></tr>';\n }\n return findings\n .map(\n (f) =>\n ` <tr data-severity=\"${escapeHtml(f.severity)}\">\n <td class=\"severity-${escapeHtml(f.severity)}\">${escapeHtml(f.severity)}</td>\n <td>${escapeHtml(f.checkId)}</td>\n <td>${escapeHtml(f.file)}:${f.startLine}</td>\n <td>${escapeHtml(f.rule)}</td>\n <td>${escapeHtml(f.message)}</td>\n </tr>`,\n )\n .join(\"\\n\");\n}\n\n/** Inline CSS for the self-contained HTML report. */\nconst CSS = String.raw`\n:root { color-scheme: dark; }\n* { box-sizing: border-box; }\nbody {\n margin: 0;\n padding: 0;\n background: #0d1117;\n color: #c9d1d9;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif;\n line-height: 1.6;\n}\n.summary-header {\n padding: 1rem 2rem;\n background: #161b22;\n border-bottom: 1px solid #30363d;\n font-size: 0.875rem;\n display: flex;\n gap: 1rem;\n flex-wrap: wrap;\n align-items: center;\n}\n.summary-count { font-size: 1.5rem; font-weight: 700; color: #f0f6fc; }\n.summary-tool { color: #8b949e; font-weight: 600; }\nsection { padding: 1rem 2rem; border-bottom: 1px solid #21262d; }\nsection h2 { font-size: 1.25rem; color: #f0f6fc; margin: 0 0 1rem 0; }\n.findings-controls { display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; align-items: center; }\n.filter-buttons { display: flex; gap: 0.5rem; flex-wrap: wrap; }\n.filter-btn {\n padding: 0.25rem 0.75rem;\n border: 1px solid #30363d;\n border-radius: 6px;\n background: #21262d;\n color: #c9d1d9;\n cursor: pointer;\n font-size: 0.8rem;\n}\n.filter-btn.active { background: #1f6feb; border-color: #1f6feb; color: #fff; }\n.filter-btn:hover { border-color: #8b949e; }\n.search-input {\n padding: 0.25rem 0.5rem;\n border: 1px solid #30363d;\n border-radius: 6px;\n background: #0d1117;\n color: #c9d1d9;\n font-size: 0.8rem;\n flex: 1;\n min-width: 200px;\n}\ntable { width: 100%; border-collapse: collapse; font-size: 0.85rem; }\nth, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid #21262d; }\nth { color: #8b949e; font-weight: 600; cursor: pointer; user-select: none; }\nth.sortable:hover { color: #f0f6fc; }\nth.sort-asc::after { content: \" \\2191\"; }\nth.sort-desc::after { content: \" \\2193\"; }\n.severity-critical { color: #f85149; font-weight: 600; }\n.severity-high { color: #f85149; }\n.severity-medium { color: #d29922; }\n.severity-low { color: #8b949e; }\n.severity-info { color: #58a6ff; }\n.no-findings { text-align: center; color: #8b949e; padding: 2rem; }\n@media (max-width: 768px) {\n section { padding: 1rem; }\n .findings-controls { flex-direction: column; }\n .search-input { min-width: 100%; }\n table { font-size: 0.75rem; }\n th, td { padding: 0.3rem 0.5rem; }\n}\n`;\n\n/** Inline vanilla JS for filter, sort, and search. No external dependencies. */\nconst JS = String.raw`\n(function() {\n var findingsData = window.__FINDINGS_DATA__ || [];\n var currentFilter = \"all\";\n var currentSearch = \"\";\n var sortColumn = null;\n var sortDir = 1;\n\n function esc(text) {\n var d = document.createElement(\"div\");\n d.textContent = text == null ? \"\" : String(text);\n return d.innerHTML;\n }\n\n function renderTable() {\n var tbody = document.querySelector(\"#findings-table tbody\");\n if (!tbody) return;\n var filtered = findingsData.filter(function(f) {\n if (currentFilter !== \"all\" && f.severity !== currentFilter) return false;\n if (currentSearch) {\n var s = currentSearch.toLowerCase();\n return (f.severity + \" \" + f.checkId + \" \" + f.file + \" \" + f.rule + \" \" + f.message).toLowerCase().indexOf(s) !== -1;\n }\n return true;\n });\n if (sortColumn) {\n filtered.sort(function(a, b) {\n var va = a[sortColumn], vb = b[sortColumn];\n if (sortColumn === \"severity\") {\n var rank = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };\n return ((rank[vb] || 0) - (rank[va] || 0)) * sortDir;\n }\n if (typeof va === \"number\" && typeof vb === \"number\") return (va - vb) * sortDir;\n return String(va).localeCompare(String(vb)) * sortDir;\n });\n }\n if (filtered.length === 0) {\n tbody.innerHTML = '<tr><td colspan=\"5\" class=\"no-findings\">No findings</td></tr>';\n return;\n }\n tbody.innerHTML = filtered.map(function(f) {\n return '<tr data-severity=\"' + esc(f.severity) + '\">' +\n '<td class=\"severity-' + esc(f.severity) + '\">' + esc(f.severity) + '</td>' +\n '<td>' + esc(f.checkId) + '</td>' +\n '<td>' + esc(f.file) + (f.startLine ? ':' + f.startLine : '') + '</td>' +\n '<td>' + esc(f.rule) + '</td>' +\n '<td>' + esc(f.message) + '</td>' +\n '</tr>';\n }).join(\"\");\n }\n\n document.querySelectorAll(\".filter-btn\").forEach(function(btn) {\n btn.addEventListener(\"click\", function() {\n document.querySelectorAll(\".filter-btn\").forEach(function(b) { b.classList.remove(\"active\"); });\n btn.classList.add(\"active\");\n currentFilter = btn.getAttribute(\"data-severity\");\n renderTable();\n });\n });\n\n var searchInput = document.getElementById(\"findings-search\");\n if (searchInput) {\n searchInput.addEventListener(\"input\", function() {\n currentSearch = searchInput.value;\n renderTable();\n });\n }\n\n document.querySelectorAll(\"#findings-table th.sortable\").forEach(function(th) {\n th.addEventListener(\"click\", function() {\n var col = th.getAttribute(\"data-sort\");\n if (sortColumn === col) {\n sortDir = -sortDir;\n } else {\n sortColumn = col;\n sortDir = 1;\n }\n document.querySelectorAll(\"#findings-table th\").forEach(function(t) {\n t.classList.remove(\"sort-asc\", \"sort-desc\");\n });\n th.classList.add(sortDir === 1 ? \"sort-asc\" : \"sort-desc\");\n renderTable();\n });\n });\n})();\n`;\n\n/**\n * Generate a self-contained HTML report from findings. Pure — no I/O.\n * All CSS and JS are inlined. No external dependencies, no CDN.\n *\n * Browser-safe: no Node.js imports. Safe to call from browser code.\n */\nexport function generateSarifHtml(findings: readonly Finding[]): string {\n const rowsHtml = renderFindingsRows(findings);\n const summaryHtml = renderSummary(findings);\n const findingsData = escapeScriptData(\n JSON.stringify(\n findings.map((f) => ({\n severity: f.severity,\n checkId: f.checkId,\n file: f.file,\n rule: f.rule,\n message: f.message,\n startLine: f.startLine,\n })),\n ),\n );\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>SARIF Report — ${escapeHtml(toolName(findings))}</title>\n <style>${CSS}</style>\n</head>\n<body>\n ${summaryHtml}\n\n <section id=\"findings\">\n <h2>Findings</h2>\n <div class=\"findings-controls\">\n <div class=\"filter-buttons\">\n <button class=\"filter-btn active\" data-severity=\"all\">All</button>\n <button class=\"filter-btn\" data-severity=\"critical\">Critical</button>\n <button class=\"filter-btn\" data-severity=\"high\">High</button>\n <button class=\"filter-btn\" data-severity=\"medium\">Medium</button>\n <button class=\"filter-btn\" data-severity=\"low\">Low</button>\n <button class=\"filter-btn\" data-severity=\"info\">Info</button>\n </div>\n <input type=\"text\" id=\"findings-search\" placeholder=\"Search findings...\" class=\"search-input\">\n </div>\n <table id=\"findings-table\">\n <thead>\n <tr>\n <th data-sort=\"severity\" class=\"sortable\">Severity</th>\n <th data-sort=\"checkId\" class=\"sortable\">Check</th>\n <th data-sort=\"file\" class=\"sortable\">File</th>\n <th data-sort=\"rule\" class=\"sortable\">Rule</th>\n <th data-sort=\"message\" class=\"sortable\">Message</th>\n </tr>\n </thead>\n <tbody>\n ${rowsHtml}\n </tbody>\n </table>\n </section>\n\n <script>\n var __FINDINGS_DATA__ = ${findingsData};\n </script>\n <script>${JS}</script>\n</body>\n</html>`;\n}\n"],"mappings":";;AAMA,SAAS,WAAW,MAAsB;CACxC,OAAO,KACJ,WAAW,KAAK,OAAO,CAAC,CACxB,WAAW,KAAK,MAAM,CAAC,CACvB,WAAW,KAAK,MAAM,CAAC,CACvB,WAAW,MAAK,QAAQ,CAAC,CACzB,WAAW,KAAK,OAAO;AAC5B;;;AAIA,SAAS,iBAAiB,MAAsB;CAC9C,OAAO,KAAK,WAAW,KAAK,SAAS;AACvC;;AAGA,SAAS,SAAS,UAAsC;CACtD,OAAO,SAAS,EAAE,EAAE,OAAO,QAAQ;AACrC;;AAGA,SAAS,kBAAkB,UAAsD;CAC/E,MAAM,SAAiC;EACrC,UAAU;EACV,MAAM;EACN,QAAQ;EACR,KAAK;EACL,MAAM;CACR;CACA,KAAK,MAAM,KAAK,UACd,IAAI,EAAE,YAAY,QAChB,OAAO,EAAE,aAAc;CAG3B,OAAO;AACT;;AAGA,SAAS,cAAc,UAAsC;CAC3D,MAAM,YAAY,kBAAkB,QAAQ;CAC5C,MAAM,QAAQ,SAAS;CACvB,MAAM,OAAO,WAAW,SAAS,QAAQ,CAAC;CAU1C,OAAO,+BAA+B;EARpC,+BAA+B,MAAM;EACrC,8BAA8B,KAAK;EACnC,mCAAmC,UAAU,SAAS;EACtD,+BAA+B,UAAU,KAAK;EAC9C,iCAAiC,UAAU,OAAO;EAClD,8BAA8B,UAAU,IAAI;EAC5C,+BAA+B,UAAU,KAAK;CAEN,CAAC,CAAC,KAAK,KAAK,EAAE;AAC1D;;AAGA,SAAS,mBAAmB,UAAsC;CAChE,IAAI,SAAS,WAAW,GACtB,OAAO;CAET,OAAO,SACJ,KACE,MACC,8BAA8B,WAAW,EAAE,QAAQ,EAAE;gCAC7B,WAAW,EAAE,QAAQ,EAAE,IAAI,WAAW,EAAE,QAAQ,EAAE;gBAClE,WAAW,EAAE,OAAO,EAAE;gBACtB,WAAW,EAAE,IAAI,EAAE,GAAG,EAAE,UAAU;gBAClC,WAAW,EAAE,IAAI,EAAE;gBACnB,WAAW,EAAE,OAAO,EAAE;cAElC,CAAC,CACA,KAAK,IAAI;AACd;;AAGA,MAAM,MAAM,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsEtB,MAAM,KAAK,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FrB,SAAgB,kBAAkB,UAAsC;CACtE,MAAM,WAAW,mBAAmB,QAAQ;CAC5C,MAAM,cAAc,cAAc,QAAQ;CAC1C,MAAM,eAAe,iBACnB,KAAK,UACH,SAAS,KAAK,OAAO;EACnB,UAAU,EAAE;EACZ,SAAS,EAAE;EACX,MAAM,EAAE;EACR,MAAM,EAAE;EACR,SAAS,EAAE;EACX,WAAW,EAAE;CACf,EAAE,CACJ,CACF;CAEA,OAAO;;;;;0BAKiB,WAAW,SAAS,QAAQ,CAAC,EAAE;WAC9C,IAAI;;;IAGX,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;UA0BN,SAAS;;;;;;8BAMW,aAAa;;YAE/B,GAAG;;;AAGf"}
|
|
1
|
+
{"version":3,"file":"html-generator.mjs","names":[],"sources":["../src/html-generator.ts"],"sourcesContent":["// @sverka/sarif-viewer-web — pure HTML generation (browser-safe).\n// No Node.js imports. Safe to import from browser code.\n\nimport type { Finding } from \"@sverka/verification\";\n\n/** Escape HTML special characters to prevent XSS. Pure. */\nfunction escapeHtml(text: string): string {\n return text\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n\n/** Escape JSON data for safe embedding in <script> tags.\n * Prevents </script> breakout XSS by replacing < with \\u003c. */\nfunction escapeScriptData(json: string): string {\n return json.replaceAll(\"<\", \"\\\\u003c\");\n}\n\n/** Extract tool name from the first finding's source, or \"unknown\". */\nfunction toolName(findings: readonly Finding[]): string {\n return findings[0]?.source.tool ?? \"unknown\";\n}\n\n/** Count findings by severity for the summary header. Pure. */\nfunction severityBreakdown(\n findings: readonly Finding[],\n): Record<string, number> {\n const counts: Record<string, number> = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n };\n for (const f of findings) {\n if (f.severity in counts) {\n counts[f.severity]! += 1;\n }\n }\n return counts;\n}\n\n/** Render the summary header with total count, severity breakdown, and tool. */\nfunction renderSummary(findings: readonly Finding[]): string {\n const breakdown = severityBreakdown(findings);\n const total = findings.length;\n const tool = escapeHtml(toolName(findings));\n const parts = [\n `<span class=\"summary-count\">${total}</span> findings`,\n `<span class=\"summary-tool\">${tool}</span>`,\n `<span class=\"severity-critical\">${breakdown.critical} critical</span>`,\n `<span class=\"severity-high\">${breakdown.high} high</span>`,\n `<span class=\"severity-medium\">${breakdown.medium} medium</span>`,\n `<span class=\"severity-low\">${breakdown.low} low</span>`,\n `<span class=\"severity-info\">${breakdown.info} info</span>`,\n ];\n return `<div class=\"summary-header\">${parts.join(\" · \")}</div>`;\n}\n\n/** Render the findings table body rows. Pure. */\nfunction renderFindingsRows(findings: readonly Finding[]): string {\n if (findings.length === 0) {\n return '<tr><td colspan=\"5\" class=\"no-findings\">No findings</td></tr>';\n }\n return findings\n .map(\n (f) =>\n ` <tr data-severity=\"${escapeHtml(f.severity)}\">\n <td class=\"severity-${escapeHtml(f.severity)}\">${escapeHtml(f.severity)}</td>\n <td>${escapeHtml(f.checkId)}</td>\n <td>${escapeHtml(f.file)}:${f.startLine}</td>\n <td>${escapeHtml(f.rule)}</td>\n <td>${escapeHtml(f.message)}</td>\n </tr>`,\n )\n .join(\"\\n\");\n}\n\n/** Inline CSS for the self-contained HTML report. */\nconst CSS = String.raw`\n:root { color-scheme: dark; }\n* { box-sizing: border-box; }\nbody {\n margin: 0;\n padding: 0;\n background: #0d1117;\n color: #c9d1d9;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif;\n line-height: 1.6;\n}\n.summary-header {\n padding: 1rem 2rem;\n background: #161b22;\n border-bottom: 1px solid #30363d;\n font-size: 0.875rem;\n display: flex;\n gap: 1rem;\n flex-wrap: wrap;\n align-items: center;\n}\n.summary-count { font-size: 1.5rem; font-weight: 700; color: #f0f6fc; }\n.summary-tool { color: #8b949e; font-weight: 600; }\nsection { padding: 1rem 2rem; border-bottom: 1px solid #21262d; }\nsection h2 { font-size: 1.25rem; color: #f0f6fc; margin: 0 0 1rem 0; }\n.findings-controls { display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; align-items: center; }\n.filter-buttons { display: flex; gap: 0.5rem; flex-wrap: wrap; }\n.filter-btn {\n padding: 0.25rem 0.75rem;\n border: 1px solid #30363d;\n border-radius: 6px;\n background: #21262d;\n color: #c9d1d9;\n cursor: pointer;\n font-size: 0.8rem;\n}\n.filter-btn.active { background: #1f6feb; border-color: #1f6feb; color: #fff; }\n.filter-btn:hover { border-color: #8b949e; }\n.search-input {\n padding: 0.25rem 0.5rem;\n border: 1px solid #30363d;\n border-radius: 6px;\n background: #0d1117;\n color: #c9d1d9;\n font-size: 0.8rem;\n flex: 1;\n min-width: 200px;\n}\ntable { width: 100%; border-collapse: collapse; font-size: 0.85rem; }\nth, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid #21262d; }\nth { color: #8b949e; font-weight: 600; cursor: pointer; user-select: none; }\nth.sortable:hover { color: #f0f6fc; }\nth.sort-asc::after { content: \" \\2191\"; }\nth.sort-desc::after { content: \" \\2193\"; }\n.severity-critical { color: #f85149; font-weight: 600; }\n.severity-high { color: #f85149; }\n.severity-medium { color: #d29922; }\n.severity-low { color: #8b949e; }\n.severity-info { color: #58a6ff; }\n.no-findings { text-align: center; color: #8b949e; padding: 2rem; }\n@media (max-width: 768px) {\n section { padding: 1rem; }\n .findings-controls { flex-direction: column; }\n .search-input { min-width: 100%; }\n table { font-size: 0.75rem; }\n th, td { padding: 0.3rem 0.5rem; }\n}\n`;\n\n/** Inline vanilla JS for filter, sort, and search. No external dependencies. */\nconst JS = String.raw`\n(function() {\n var findingsData = window.__FINDINGS_DATA__ || [];\n var currentFilter = \"all\";\n var currentSearch = \"\";\n var sortColumn = null;\n var sortDir = 1;\n\n function esc(text) {\n var d = document.createElement(\"div\");\n d.textContent = text == null ? \"\" : String(text);\n return d.innerHTML;\n }\n\n function renderTable() {\n var tbody = document.querySelector(\"#findings-table tbody\");\n if (!tbody) return;\n var filtered = findingsData.filter(function(f) {\n if (currentFilter !== \"all\" && f.severity !== currentFilter) return false;\n if (currentSearch) {\n var s = currentSearch.toLowerCase();\n return (f.severity + \" \" + f.checkId + \" \" + f.file + \" \" + f.rule + \" \" + f.message).toLowerCase().indexOf(s) !== -1;\n }\n return true;\n });\n if (sortColumn) {\n filtered.sort(function(a, b) {\n var va = a[sortColumn], vb = b[sortColumn];\n if (sortColumn === \"severity\") {\n var rank = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };\n return ((rank[vb] || 0) - (rank[va] || 0)) * sortDir;\n }\n if (typeof va === \"number\" && typeof vb === \"number\") return (va - vb) * sortDir;\n return String(va).localeCompare(String(vb)) * sortDir;\n });\n }\n if (filtered.length === 0) {\n tbody.innerHTML = '<tr><td colspan=\"5\" class=\"no-findings\">No findings</td></tr>';\n return;\n }\n tbody.innerHTML = filtered.map(function(f) {\n return '<tr data-severity=\"' + esc(f.severity) + '\">' +\n '<td class=\"severity-' + esc(f.severity) + '\">' + esc(f.severity) + '</td>' +\n '<td>' + esc(f.checkId) + '</td>' +\n '<td>' + esc(f.file) + (f.startLine ? ':' + f.startLine : '') + '</td>' +\n '<td>' + esc(f.rule) + '</td>' +\n '<td>' + esc(f.message) + '</td>' +\n '</tr>';\n }).join(\"\");\n }\n\n document.querySelectorAll(\".filter-btn\").forEach(function(btn) {\n btn.addEventListener(\"click\", function() {\n document.querySelectorAll(\".filter-btn\").forEach(function(b) { b.classList.remove(\"active\"); });\n btn.classList.add(\"active\");\n currentFilter = btn.getAttribute(\"data-severity\");\n renderTable();\n });\n });\n\n var searchInput = document.getElementById(\"findings-search\");\n if (searchInput) {\n searchInput.addEventListener(\"input\", function() {\n currentSearch = searchInput.value;\n renderTable();\n });\n }\n\n document.querySelectorAll(\"#findings-table th.sortable\").forEach(function(th) {\n th.addEventListener(\"click\", function() {\n var col = th.getAttribute(\"data-sort\");\n if (sortColumn === col) {\n sortDir = -sortDir;\n } else {\n sortColumn = col;\n sortDir = 1;\n }\n document.querySelectorAll(\"#findings-table th\").forEach(function(t) {\n t.classList.remove(\"sort-asc\", \"sort-desc\");\n });\n th.classList.add(sortDir === 1 ? \"sort-asc\" : \"sort-desc\");\n renderTable();\n });\n });\n})();\n`;\n\n/**\n * Generate a self-contained HTML report from findings. Pure — no I/O.\n * All CSS and JS are inlined. No external dependencies, no CDN.\n *\n * Browser-safe: no Node.js imports. Safe to call from browser code.\n */\nexport function generateSarifHtml(findings: readonly Finding[]): string {\n const rowsHtml = renderFindingsRows(findings);\n const summaryHtml = renderSummary(findings);\n const findingsData = escapeScriptData(\n JSON.stringify(\n findings.map((f) => ({\n severity: f.severity,\n checkId: f.checkId,\n file: f.file,\n rule: f.rule,\n message: f.message,\n startLine: f.startLine,\n })),\n ),\n );\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>SARIF Report — ${escapeHtml(toolName(findings))}</title>\n <style>${CSS}</style>\n</head>\n<body>\n ${summaryHtml}\n\n <section id=\"findings\">\n <h2>Findings</h2>\n <div class=\"findings-controls\">\n <div class=\"filter-buttons\">\n <button class=\"filter-btn active\" data-severity=\"all\">All</button>\n <button class=\"filter-btn\" data-severity=\"critical\">Critical</button>\n <button class=\"filter-btn\" data-severity=\"high\">High</button>\n <button class=\"filter-btn\" data-severity=\"medium\">Medium</button>\n <button class=\"filter-btn\" data-severity=\"low\">Low</button>\n <button class=\"filter-btn\" data-severity=\"info\">Info</button>\n </div>\n <input type=\"text\" id=\"findings-search\" placeholder=\"Search findings...\" class=\"search-input\">\n </div>\n <table id=\"findings-table\">\n <thead>\n <tr>\n <th data-sort=\"severity\" class=\"sortable\">Severity</th>\n <th data-sort=\"checkId\" class=\"sortable\">Check</th>\n <th data-sort=\"file\" class=\"sortable\">File</th>\n <th data-sort=\"rule\" class=\"sortable\">Rule</th>\n <th data-sort=\"message\" class=\"sortable\">Message</th>\n </tr>\n </thead>\n <tbody>\n ${rowsHtml}\n </tbody>\n </table>\n </section>\n\n <script>\n var __FINDINGS_DATA__ = ${findingsData};\n </script>\n <script>${JS}</script>\n</body>\n</html>`;\n}\n"],"mappings":";;AAMA,SAAS,WAAW,MAAsB;CACxC,OAAO,KACJ,WAAW,KAAK,OAAO,CAAC,CACxB,WAAW,KAAK,MAAM,CAAC,CACvB,WAAW,KAAK,MAAM,CAAC,CACvB,WAAW,MAAK,QAAQ,CAAC,CACzB,WAAW,KAAK,OAAO;AAC5B;;;AAIA,SAAS,iBAAiB,MAAsB;CAC9C,OAAO,KAAK,WAAW,KAAK,SAAS;AACvC;;AAGA,SAAS,SAAS,UAAsC;CACtD,OAAO,SAAS,EAAE,EAAE,OAAO,QAAQ;AACrC;;AAGA,SAAS,kBACP,UACwB;CACxB,MAAM,SAAiC;EACrC,UAAU;EACV,MAAM;EACN,QAAQ;EACR,KAAK;EACL,MAAM;CACR;CACA,KAAK,MAAM,KAAK,UACd,IAAI,EAAE,YAAY,QAChB,OAAO,EAAE,aAAc;CAG3B,OAAO;AACT;;AAGA,SAAS,cAAc,UAAsC;CAC3D,MAAM,YAAY,kBAAkB,QAAQ;CAC5C,MAAM,QAAQ,SAAS;CACvB,MAAM,OAAO,WAAW,SAAS,QAAQ,CAAC;CAU1C,OAAO,+BAA+B;EARpC,+BAA+B,MAAM;EACrC,8BAA8B,KAAK;EACnC,mCAAmC,UAAU,SAAS;EACtD,+BAA+B,UAAU,KAAK;EAC9C,iCAAiC,UAAU,OAAO;EAClD,8BAA8B,UAAU,IAAI;EAC5C,+BAA+B,UAAU,KAAK;CAEN,CAAC,CAAC,KAAK,KAAK,EAAE;AAC1D;;AAGA,SAAS,mBAAmB,UAAsC;CAChE,IAAI,SAAS,WAAW,GACtB,OAAO;CAET,OAAO,SACJ,KACE,MACC,8BAA8B,WAAW,EAAE,QAAQ,EAAE;gCAC7B,WAAW,EAAE,QAAQ,EAAE,IAAI,WAAW,EAAE,QAAQ,EAAE;gBAClE,WAAW,EAAE,OAAO,EAAE;gBACtB,WAAW,EAAE,IAAI,EAAE,GAAG,EAAE,UAAU;gBAClC,WAAW,EAAE,IAAI,EAAE;gBACnB,WAAW,EAAE,OAAO,EAAE;cAElC,CAAC,CACA,KAAK,IAAI;AACd;;AAGA,MAAM,MAAM,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsEtB,MAAM,KAAK,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FrB,SAAgB,kBAAkB,UAAsC;CACtE,MAAM,WAAW,mBAAmB,QAAQ;CAC5C,MAAM,cAAc,cAAc,QAAQ;CAC1C,MAAM,eAAe,iBACnB,KAAK,UACH,SAAS,KAAK,OAAO;EACnB,UAAU,EAAE;EACZ,SAAS,EAAE;EACX,MAAM,EAAE;EACR,MAAM,EAAE;EACR,SAAS,EAAE;EACX,WAAW,EAAE;CACf,EAAE,CACJ,CACF;CAEA,OAAO;;;;;0BAKiB,WAAW,SAAS,QAAQ,CAAC,EAAE;WAC9C,IAAI;;;IAGX,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;UA0BN,SAAS;;;;;;8BAMW,aAAa;;YAE/B,GAAG;;;AAGf"}
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/viewer.ts","../src/input.ts"],"mappings":";;;;;UAMiB;;WAEN;;WAEA;;WAEA,oBAAoB;;;WAGpB,UAAU;;WAEV;;;;;;;;;;wBCAK,eAAe,SAAS;;;;;;;;;;;;;;wBCDxB,
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/viewer.ts","../src/input.ts"],"mappings":";;;;;UAMiB;;WAEN;;WAEA;;WAEA,oBAAoB;;;WAGpB,UAAU;;WAEV;;;;;;;;;;wBCAK,eAAe,SAAS;;;;;;;;;;;;;;wBCDxB,gBACd,SAAS,KAAK,iCACb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viewer-CohogBDA.mjs","names":[],"sources":["../src/input.ts","../src/viewer.ts"],"sourcesContent":["// @sverka/sarif-viewer-web — input resolution. Spec 47.\n\nimport { resolveSarifInput, type Finding } from \"@sverka/verification\";\nimport type { SarifWebOptions } from \"./types.js\";\n\n/**\n * Resolve `SarifWebOptions` (minus `outputPath`) into a `Finding[]`. Exactly\n * one of `sarif`, `sarifPath`, `findings` must be provided.\n *\n * Delegates to `@sverka/verification`'s `resolveSarifInput` for the shared\n * resolution logic (file reading, JSON parsing, normalization).\n *\n * @throws {Error} when zero or more than one input is provided.\n * @throws {NormalizationError} when SARIF is invalid (propagated).\n * @throws {Error} when the file cannot be read or parsed.\n */\nexport function resolveFindings(options: Omit<SarifWebOptions, \"outputPath\"
|
|
1
|
+
{"version":3,"file":"viewer-CohogBDA.mjs","names":[],"sources":["../src/input.ts","../src/viewer.ts"],"sourcesContent":["// @sverka/sarif-viewer-web — input resolution. Spec 47.\n\nimport { resolveSarifInput, type Finding } from \"@sverka/verification\";\nimport type { SarifWebOptions } from \"./types.js\";\n\n/**\n * Resolve `SarifWebOptions` (minus `outputPath`) into a `Finding[]`. Exactly\n * one of `sarif`, `sarifPath`, `findings` must be provided.\n *\n * Delegates to `@sverka/verification`'s `resolveSarifInput` for the shared\n * resolution logic (file reading, JSON parsing, normalization).\n *\n * @throws {Error} when zero or more than one input is provided.\n * @throws {NormalizationError} when SARIF is invalid (propagated).\n * @throws {Error} when the file cannot be read or parsed.\n */\nexport function resolveFindings(\n options: Omit<SarifWebOptions, \"outputPath\">,\n): Finding[] {\n return resolveSarifInput(options);\n}\n","// @sverka/sarif-viewer-web — file I/O wrapper. Spec 47.\n\nimport { writeFileSync, mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { generateSarifHtml } from \"./html-generator.js\";\nimport { resolveFindings } from \"./input.js\";\nimport type { SarifWebOptions } from \"./types.js\";\n\n// Re-export the pure HTML generator for backward compatibility.\nexport { generateSarifHtml } from \"./html-generator.js\";\n\n/**\n * Resolve `options` into findings, generate HTML, and write to\n * `options.outputPath`. Creates parent directories if needed.\n *\n * @throws {Error} when the file cannot be written.\n */\nexport function renderSarifWeb(options: SarifWebOptions): void {\n const findings = resolveFindings(options);\n const html = generateSarifHtml(findings);\n mkdirSync(dirname(options.outputPath), { recursive: true });\n writeFileSync(options.outputPath, html, \"utf-8\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,SAAgB,gBACd,SACW;CACX,OAAO,kBAAkB,OAAO;AAClC;;;;;;;;;ACHA,SAAgB,eAAe,SAAgC;CAC7D,MAAM,WAAW,gBAAgB,OAAO;CACxC,MAAM,OAAO,kBAAkB,QAAQ;CACvC,UAAU,QAAQ,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;CAC1D,cAAc,QAAQ,YAAY,MAAM,OAAO;AACjD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sverka/sarif-viewer-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Sverka SARIF viewer Web — standalone HTML report generator for SARIF files",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"typecheck": "tsc --noEmit"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@sverka/verification": "^0.1.
|
|
43
|
+
"@sverka/verification": "^0.1.7"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"tsdown": "^0.23.0",
|