Use marked module for some Markdown processing

This commit is contained in:
2026-03-16 10:54:27 -05:00
parent d92e82cea0
commit 5041229ea1
8 changed files with 213 additions and 161 deletions
+1
View File
@@ -0,0 +1 @@
node_modules/
+26
View File
@@ -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"
}
}
}
}
+10
View File
@@ -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"
}
}
+29 -74
View File
@@ -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() { export function clearIssues() {
document.getElementById('addressed-issues').innerHTML = ''; document.getElementById('addressed-issues').innerHTML = '';
document.getElementById('known-issues').innerHTML = ''; document.getElementById('known-issues').innerHTML = '';
@@ -134,7 +142,7 @@ function fetchIssueFile(productKey, fileName, issueType) {
} }
function parseMarkdownIssues(markdownText) { function parseMarkdownIssues(markdownText) {
const lines = String(markdownText || '').split(/\r?\n/); const lines = String(stripMarkdownFrontmatter(markdownText) || '').split(/\r?\n/);
const issues = []; const issues = [];
let currentIssue = null; let currentIssue = null;
let inCaveatBlock = false; let inCaveatBlock = false;
@@ -188,7 +196,7 @@ function parseMarkdownIssues(markdownText) {
return; return;
} }
currentIssue.descriptionLines.push(trimmed); currentIssue.descriptionLines.push(line.trimEnd());
}); });
finalizeMarkdownIssue(currentIssue, issues); finalizeMarkdownIssue(currentIssue, issues);
@@ -200,11 +208,12 @@ function finalizeMarkdownIssue(issue, issues) {
return; return;
} }
const description = issue.descriptionLines const description = Array.isArray(issue.descriptionLines)
.map(line => line.trimEnd()) ? issue.descriptionLines
.join('\n') .join('\n')
.replace(/\n{3,}/g, '\n\n') .replace(/\n{3,}/g, '\n\n')
.trim(); .trim()
: '';
let summary = description; let summary = description;
if (issue.caveat) { if (issue.caveat) {
@@ -556,79 +565,25 @@ function markdownSummaryToHtml(summaryText) {
} }
function renderMarkdownBodyToHtml(markdownText) { function renderMarkdownBodyToHtml(markdownText) {
const lines = String(markdownText || '').split(/\r?\n/); const input = String(markdownText || '').trim();
const htmlParts = []; if (!input) {
let paragraphLines = []; return '';
function flushParagraph() {
if (paragraphLines.length === 0) {
return;
}
const text = paragraphLines.join(' ').replace(/\s+/g, ' ').trim();
if (text) {
htmlParts.push(`<p>${escapeHtml(text)}</p>`);
}
paragraphLines = [];
} }
for (let i = 0; i < lines.length; i++) { const html = marked.parse(input, {
const line = lines[i]; gfm: true,
const trimmed = line.trim(); async: false,
renderer: summaryMarkdownRenderer
if (!trimmed) { });
flushParagraph(); return addIssueTableClasses(typeof html === 'string' ? html : '');
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('');
} }
function isTableRow(line) { function addIssueTableClasses(html) {
return /\|/.test(line); const doc = new DOMParser().parseFromString(String(html || ''), 'text/html');
} doc.querySelectorAll('table').forEach(table => {
table.classList.add('issues-table');
function isTableSeparator(line) { });
return /^\|?\s*:?-{3,}:?(\s*\|\s*:?-{3,}:?)*\s*\|?$/.test(line); return doc.body.innerHTML;
}
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 = `<thead><tr>${headerCells.map(cell => `<th>${escapeHtml(cell)}</th>`).join('')}</tr></thead>`;
const tbodyRows = rows.map(row => {
const normalized = [...row];
while (normalized.length < colCount) {
normalized.push('');
}
return `<tr>${normalized.slice(0, colCount).map(cell => `<td>${escapeHtml(cell)}</td>`).join('')}</tr>`;
}).join('');
return `<table class="issues-table">${thead}<tbody>${tbodyRows}</tbody></table>`;
} }
function escapeHtml(value) { function escapeHtml(value) {
+65
View File
@@ -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 : [''];
}
+5 -83
View File
@@ -1,3 +1,5 @@
import { buildIssueMarkdownDocument } from './markdown.js';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
loadProducts(); loadProducts();
setupEventListeners(); setupEventListeners();
@@ -34,6 +36,7 @@ function setupEventListeners() {
document.getElementById('productSelect').addEventListener('change', generateJSON); document.getElementById('productSelect').addEventListener('change', generateJSON);
document.getElementById('issueTypeSelect').addEventListener('change', generateJSON); document.getElementById('issueTypeSelect').addEventListener('change', generateJSON);
document.getElementById('versionInput').addEventListener('input', generateJSON); document.getElementById('versionInput').addEventListener('input', generateJSON);
document.getElementById('copyMarkdownBtn').addEventListener('click', copyToClipboard);
document.getElementById('productSelect').addEventListener('change', persistFormState); document.getElementById('productSelect').addEventListener('change', persistFormState);
document.getElementById('issueTypeSelect').addEventListener('change', persistFormState); document.getElementById('issueTypeSelect').addEventListener('change', persistFormState);
@@ -70,68 +73,18 @@ function generateJSON() {
caveat: issue.caveat || '' caveat: issue.caveat || ''
})); }));
const jsonData = { const markdownText = buildIssueMarkdownDocument({
type: issueType, type: issueType,
product, product,
version, version,
issues issues
}; });
const markdownText = buildMarkdownOutput(jsonData);
document.getElementById('markdownOutput').value = markdownText; document.getElementById('markdownOutput').value = markdownText;
updateDownloadLink(markdownText, version); updateDownloadLink(markdownText, version);
setParseStatus(`Parsed ${issues.length} issues from input.`); 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) { function parseIssuesInput(inputText) {
const fromHtml = parseIssuesFromHtmlTable(inputText); const fromHtml = parseIssuesFromHtmlTable(inputText);
if (fromHtml.length > 0) { if (fromHtml.length > 0) {
@@ -292,14 +245,6 @@ function sanitizeTableCellText(text) {
return normalizeWhitespace(text).replace(/\|/g, '\\|'); return normalizeWhitespace(text).replace(/\|/g, '\\|');
} }
function extractCellTextLegacy(cell) {
const withLineBreaks = (cell.innerHTML || '')
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<[^>]+>/g, ' ');
return normalizeWhitespace(withLineBreaks).replace(/\s*\n\s*/g, '\n').trim();
}
function parseIssuesFromLinePairs(rawText) { function parseIssuesFromLinePairs(rawText) {
const lines = rawText const lines = rawText
.split(/\r?\n/) .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() { function persistFormState() {
const formState = { const formState = {
product: document.getElementById('productSelect').value || '', 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) { function updateDownloadLink(markdownText, version) {
const link = document.getElementById('downloadMarkdownLink'); const link = document.getElementById('downloadMarkdownLink');
resetDownloadLink(); resetDownloadLink();
@@ -482,7 +408,3 @@ function resetDownloadLink() {
link.style.pointerEvents = 'none'; link.style.pointerEvents = 'none';
link.style.opacity = '0.6'; link.style.opacity = '0.6';
} }
window.generateJSON = generateJSON;
window.copyToClipboard = copyToClipboard;
window.clearForm = clearForm;
+2 -2
View File
@@ -125,12 +125,12 @@
<h2>Markdown Preview</h2> <h2>Markdown Preview</h2>
<label for="markdownOutput">Copy the Markdown below:</label> <label for="markdownOutput">Copy the Markdown below:</label>
<textarea id="markdownOutput" readonly></textarea> <textarea id="markdownOutput" readonly></textarea>
<button onclick="copyToClipboard(event)" style="margin-top: 10px; width: 100%;">Copy Markdown</button> <button id="copyMarkdownBtn" style="margin-top: 10px; width: 100%;">Copy Markdown</button>
<a id="downloadMarkdownLink" href="#" style="display: block; margin-top: 10px; text-align: center; color: #2c7be5; text-decoration: none; pointer-events: none; opacity: 0.6;">Download Markdown File</a> <a id="downloadMarkdownLink" href="#" style="display: block; margin-top: 10px; text-align: center; color: #2c7be5; text-decoration: none; pointer-events: none; opacity: 0.6;">Download Markdown File</a>
</section> </section>
</div> </div>
</main> </main>
<script src="process.js"></script> <script type="module" src="js/process.js"></script>
</body> </body>
</html> </html>
+73
View File
File diff suppressed because one or more lines are too long