Use marked module for some Markdown processing
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
Generated
+26
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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() : '';
|
function addIssueTableClasses(html) {
|
||||||
if (isTableRow(trimmed) && isTableSeparator(next)) {
|
const doc = new DOMParser().parseFromString(String(html || ''), 'text/html');
|
||||||
flushParagraph();
|
doc.querySelectorAll('table').forEach(table => {
|
||||||
|
table.classList.add('issues-table');
|
||||||
const headerCells = parseMarkdownTableRow(trimmed);
|
});
|
||||||
const rows = [];
|
return doc.body.innerHTML;
|
||||||
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) {
|
|
||||||
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 = `<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) {
|
||||||
|
|||||||
@@ -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 : [''];
|
||||||
|
}
|
||||||
@@ -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
@@ -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>
|
||||||
Vendored
+73
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user