diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..40b878d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules/
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..778c60b
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,26 @@
+{
+ "name": "firewallissues",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "firewallissues",
+ "devDependencies": {
+ "marked": "^16.3.0"
+ }
+ },
+ "node_modules/marked": {
+ "version": "16.4.2",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
+ "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f4eb2c5
--- /dev/null
+++ b/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "firewallissues",
+ "private": true,
+ "scripts": {
+ "vendor:marked": "mkdir -p web/vendor && cp node_modules/marked/lib/marked.esm.js web/vendor/marked.esm.js"
+ },
+ "devDependencies": {
+ "marked": "^16.3.0"
+ }
+}
\ No newline at end of file
diff --git a/web/js/issues.js b/web/js/issues.js
index ac619b4..fe35922 100644
--- a/web/js/issues.js
+++ b/web/js/issues.js
@@ -1,3 +1,11 @@
+import { stripMarkdownFrontmatter } from './markdown.js';
+import { marked, Renderer } from '../vendor/marked.esm.js';
+
+const summaryMarkdownRenderer = new Renderer();
+summaryMarkdownRenderer.html = token => {
+ return escapeHtml(typeof token === 'string' ? token : (token.raw || token.text || ''));
+};
+
export function clearIssues() {
document.getElementById('addressed-issues').innerHTML = '';
document.getElementById('known-issues').innerHTML = '';
@@ -134,7 +142,7 @@ function fetchIssueFile(productKey, fileName, issueType) {
}
function parseMarkdownIssues(markdownText) {
- const lines = String(markdownText || '').split(/\r?\n/);
+ const lines = String(stripMarkdownFrontmatter(markdownText) || '').split(/\r?\n/);
const issues = [];
let currentIssue = null;
let inCaveatBlock = false;
@@ -188,7 +196,7 @@ function parseMarkdownIssues(markdownText) {
return;
}
- currentIssue.descriptionLines.push(trimmed);
+ currentIssue.descriptionLines.push(line.trimEnd());
});
finalizeMarkdownIssue(currentIssue, issues);
@@ -200,11 +208,12 @@ function finalizeMarkdownIssue(issue, issues) {
return;
}
- const description = issue.descriptionLines
- .map(line => line.trimEnd())
- .join('\n')
- .replace(/\n{3,}/g, '\n\n')
- .trim();
+ const description = Array.isArray(issue.descriptionLines)
+ ? issue.descriptionLines
+ .join('\n')
+ .replace(/\n{3,}/g, '\n\n')
+ .trim()
+ : '';
let summary = description;
if (issue.caveat) {
@@ -556,79 +565,25 @@ function markdownSummaryToHtml(summaryText) {
}
function renderMarkdownBodyToHtml(markdownText) {
- const lines = String(markdownText || '').split(/\r?\n/);
- const htmlParts = [];
- let paragraphLines = [];
-
- function flushParagraph() {
- if (paragraphLines.length === 0) {
- return;
- }
- const text = paragraphLines.join(' ').replace(/\s+/g, ' ').trim();
- if (text) {
- htmlParts.push(`
${escapeHtml(text)}
`);
- }
- paragraphLines = [];
+ const input = String(markdownText || '').trim();
+ if (!input) {
+ return '';
}
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i];
- const trimmed = line.trim();
-
- if (!trimmed) {
- flushParagraph();
- continue;
- }
-
- const next = i + 1 < lines.length ? lines[i + 1].trim() : '';
- if (isTableRow(trimmed) && isTableSeparator(next)) {
- flushParagraph();
-
- const headerCells = parseMarkdownTableRow(trimmed);
- const rows = [];
- i += 2;
- while (i < lines.length && isTableRow(lines[i].trim())) {
- rows.push(parseMarkdownTableRow(lines[i].trim()));
- i++;
- }
- i -= 1;
-
- htmlParts.push(renderHtmlTable(headerCells, rows));
- continue;
- }
-
- paragraphLines.push(trimmed);
- }
-
- flushParagraph();
- return htmlParts.join('');
+ const html = marked.parse(input, {
+ gfm: true,
+ async: false,
+ renderer: summaryMarkdownRenderer
+ });
+ return addIssueTableClasses(typeof html === 'string' ? html : '');
}
-function isTableRow(line) {
- return /\|/.test(line);
-}
-
-function isTableSeparator(line) {
- return /^\|?\s*:?-{3,}:?(\s*\|\s*:?-{3,}:?)*\s*\|?$/.test(line);
-}
-
-function parseMarkdownTableRow(line) {
- const raw = line.trim().replace(/^\|/, '').replace(/\|$/, '');
- return raw.split('|').map(cell => cell.trim());
-}
-
-function renderHtmlTable(headerCells, rows) {
- const colCount = headerCells.length;
- const thead = `${headerCells.map(cell => `| ${escapeHtml(cell)} | `).join('')}
`;
- const tbodyRows = rows.map(row => {
- const normalized = [...row];
- while (normalized.length < colCount) {
- normalized.push('');
- }
- return `${normalized.slice(0, colCount).map(cell => `| ${escapeHtml(cell)} | `).join('')}
`;
- }).join('');
-
- return ``;
+function addIssueTableClasses(html) {
+ const doc = new DOMParser().parseFromString(String(html || ''), 'text/html');
+ doc.querySelectorAll('table').forEach(table => {
+ table.classList.add('issues-table');
+ });
+ return doc.body.innerHTML;
}
function escapeHtml(value) {
diff --git a/web/js/markdown.js b/web/js/markdown.js
new file mode 100644
index 0000000..e231983
--- /dev/null
+++ b/web/js/markdown.js
@@ -0,0 +1,65 @@
+export function buildIssueMarkdownDocument(payload) {
+ const lines = [];
+ lines.push('---');
+ lines.push(`type: ${payload.type}`);
+ lines.push(`product: ${payload.product}`);
+ lines.push(`version: ${payload.version}`);
+ lines.push('---');
+ lines.push('');
+
+ if (!payload.issues || payload.issues.length === 0) {
+ lines.push('_No issues_');
+ lines.push('');
+ return lines.join('\n');
+ }
+
+ payload.issues.forEach(issue => {
+ lines.push(`## ${issue.id}`);
+ lines.push('');
+
+ if (issue.caveat) {
+ lines.push('```caveat');
+ lines.push(issue.caveat);
+ lines.push('```');
+ lines.push('');
+ }
+
+ getIssueDescriptionParagraphs(issue.description).forEach(paragraph => {
+ lines.push(paragraph);
+ lines.push('');
+ });
+
+ if (Array.isArray(issue.platforms) && issue.platforms.length > 0) {
+ lines.push(`Platforms: ${issue.platforms.join(', ')}`);
+ lines.push('');
+ }
+ });
+
+ return lines.join('\n');
+}
+
+export function stripMarkdownFrontmatter(markdownText) {
+ const input = String(markdownText || '').replace(/^\uFEFF/, '');
+ const lines = input.split(/\r?\n/);
+
+ if (lines[0] !== '---') {
+ return input;
+ }
+
+ for (let i = 1; i < lines.length; i++) {
+ if (lines[i] === '---') {
+ return lines.slice(i + 1).join('\n').replace(/^\n+/, '');
+ }
+ }
+
+ return input;
+}
+
+function getIssueDescriptionParagraphs(description) {
+ const paragraphs = String(description || '')
+ .split(/\n{2,}/)
+ .map(text => text.trim())
+ .filter(Boolean);
+
+ return paragraphs.length > 0 ? paragraphs : [''];
+}
\ No newline at end of file
diff --git a/web/process.js b/web/js/process.js
similarity index 83%
rename from web/process.js
rename to web/js/process.js
index b7be985..1a1826a 100644
--- a/web/process.js
+++ b/web/js/process.js
@@ -1,3 +1,5 @@
+import { buildIssueMarkdownDocument } from './markdown.js';
+
document.addEventListener('DOMContentLoaded', () => {
loadProducts();
setupEventListeners();
@@ -34,6 +36,7 @@ function setupEventListeners() {
document.getElementById('productSelect').addEventListener('change', generateJSON);
document.getElementById('issueTypeSelect').addEventListener('change', generateJSON);
document.getElementById('versionInput').addEventListener('input', generateJSON);
+ document.getElementById('copyMarkdownBtn').addEventListener('click', copyToClipboard);
document.getElementById('productSelect').addEventListener('change', persistFormState);
document.getElementById('issueTypeSelect').addEventListener('change', persistFormState);
@@ -70,68 +73,18 @@ function generateJSON() {
caveat: issue.caveat || ''
}));
- const jsonData = {
+ const markdownText = buildIssueMarkdownDocument({
type: issueType,
product,
version,
issues
- };
+ });
- const markdownText = buildMarkdownOutput(jsonData);
document.getElementById('markdownOutput').value = markdownText;
updateDownloadLink(markdownText, version);
setParseStatus(`Parsed ${issues.length} issues from input.`);
}
-function buildMarkdownOutput(payload) {
- const lines = [];
- lines.push('---');
- lines.push(`type: ${payload.type}`);
- lines.push(`product: ${payload.product}`);
- lines.push(`version: ${payload.version}`);
- lines.push('---');
- lines.push('');
-
- if (!payload.issues || payload.issues.length === 0) {
- lines.push('_No issues_');
- lines.push('');
- return lines.join('\n');
- }
-
- payload.issues.forEach(issue => {
- lines.push(`## ${issue.id}`);
- lines.push('');
-
- if (issue.caveat) {
- lines.push('```caveat');
- lines.push(issue.caveat);
- lines.push('```');
- lines.push('');
- }
-
- const paragraphs = String(issue.description || '')
- .split(/\n{2,}/)
- .map(text => text.trim())
- .filter(Boolean);
-
- if (paragraphs.length === 0) {
- lines.push('');
- } else {
- paragraphs.forEach(paragraph => {
- lines.push(paragraph);
- lines.push('');
- });
- }
-
- if (Array.isArray(issue.platforms) && issue.platforms.length > 0) {
- lines.push(`Platforms: ${issue.platforms.join(', ')}`);
- lines.push('');
- }
- });
-
- return lines.join('\n');
-}
-
function parseIssuesInput(inputText) {
const fromHtml = parseIssuesFromHtmlTable(inputText);
if (fromHtml.length > 0) {
@@ -292,14 +245,6 @@ function sanitizeTableCellText(text) {
return normalizeWhitespace(text).replace(/\|/g, '\\|');
}
-function extractCellTextLegacy(cell) {
- const withLineBreaks = (cell.innerHTML || '')
- .replace(/
/gi, '\n')
- .replace(/<[^>]+>/g, ' ');
-
- return normalizeWhitespace(withLineBreaks).replace(/\s*\n\s*/g, '\n').trim();
-}
-
function parseIssuesFromLinePairs(rawText) {
const lines = rawText
.split(/\r?\n/)
@@ -363,17 +308,6 @@ function copyToClipboard(event) {
});
}
-function clearForm() {
- document.getElementById('productSelect').value = '';
- document.getElementById('issueTypeSelect').value = '';
- document.getElementById('versionInput').value = '';
- document.getElementById('issuesInput').value = '';
- document.getElementById('markdownOutput').value = '';
- resetDownloadLink();
- setParseStatus('');
- clearPersistedFormState();
-}
-
function persistFormState() {
const formState = {
product: document.getElementById('productSelect').value || '',
@@ -445,14 +379,6 @@ function applyPersistedProductSelection() {
}
}
-function clearPersistedFormState() {
- try {
- localStorage.removeItem(PROCESS_FORM_STATE_KEY);
- } catch (error) {
- console.warn('Could not clear process form state:', error);
- }
-}
-
function updateDownloadLink(markdownText, version) {
const link = document.getElementById('downloadMarkdownLink');
resetDownloadLink();
@@ -482,7 +408,3 @@ function resetDownloadLink() {
link.style.pointerEvents = 'none';
link.style.opacity = '0.6';
}
-
-window.generateJSON = generateJSON;
-window.copyToClipboard = copyToClipboard;
-window.clearForm = clearForm;
diff --git a/web/process.html b/web/process.html
index ad5d2b5..a82f022 100644
--- a/web/process.html
+++ b/web/process.html
@@ -125,12 +125,12 @@
Markdown Preview
-
+
Download Markdown File
-
+