diff --git a/web/js/issue-id.js b/web/js/issue-id.js new file mode 100644 index 0000000..80b912b --- /dev/null +++ b/web/js/issue-id.js @@ -0,0 +1,84 @@ +export const BLANK_ISSUE_ID = 'BLANK-000000'; + +const ISSUE_ID_CORE_PATTERN = '[A-Z][A-Z0-9]{1,15}-\\d{3,8}'; + +export const ISSUE_ID_PATTERN = new RegExp(`(${ISSUE_ID_CORE_PATTERN})`, 'i'); +export const ISSUE_ID_LIST_PREFIX_PATTERN = new RegExp( + `^\\s*((?:${ISSUE_ID_CORE_PATTERN})(?:\\s*(?:,|and|&)\\s*(?:${ISSUE_ID_CORE_PATTERN}))*)`, + 'i' +); +export const ISSUE_ID_LINE_PATTERN = new RegExp(`^(${ISSUE_ID_CORE_PATTERN})\\s*[:\\-]?\\s*(.*)$`, 'i'); + +export function normalizeWhitespace(value) { + return String(value || '').replace(/\s+/g, ' ').trim(); +} + +export function normalizeIssueId(value) { + return normalizeWhitespace(value).toUpperCase(); +} + +export function isBlankIssueMarker(value) { + return /^[\u2014\u2013\-]+$/.test(String(value || '').trim()); +} + +export function extractIssueIds(value) { + const ids = []; + const seen = new Set(); + const matches = String(value || '').matchAll(new RegExp(ISSUE_ID_PATTERN.source, 'ig')); + + for (const match of matches) { + const normalizedId = normalizeIssueId(match[1] || ''); + if (!normalizedId || seen.has(normalizedId)) { + continue; + } + + seen.add(normalizedId); + ids.push(normalizedId); + } + + return ids; +} + +export function parseIssueIdCell(value) { + const text = normalizeWhitespace(value); + const issueIdListMatch = text.match(ISSUE_ID_LIST_PREFIX_PATTERN); + const blankId = !issueIdListMatch && isBlankIssueMarker(text); + + if (!issueIdListMatch && !blankId) { + return null; + } + + if (blankId) { + return { + issueIds: [BLANK_ISSUE_ID], + metadataText: '', + isBlankId: true + }; + } + + const issueIds = extractIssueIds(String(issueIdListMatch[1] || '')); + const metadataText = normalizeWhitespace(text.slice(issueIdListMatch[0].length)).replace(/^[-:;,.\s]+/, ''); + + return { + issueIds, + metadataText, + isBlankId: false + }; +} + +export function parseIssueLine(value) { + const text = String(value || '').trim(); + const match = text.match(ISSUE_ID_LINE_PATTERN); + + if (!match) { + return { + id: '', + summary: text + }; + } + + return { + id: normalizeIssueId(match[1] || ''), + summary: (match[2] || '').trim() + }; +} \ No newline at end of file diff --git a/web/js/issues.js b/web/js/issues.js index 3a557eb..c8e8a49 100644 --- a/web/js/issues.js +++ b/web/js/issues.js @@ -1,4 +1,10 @@ import { stripMarkdownFrontmatter } from './markdown.js'; +import { + BLANK_ISSUE_ID, + normalizeIssueId, + normalizeWhitespace, + parseIssueLine as parseIssueLineValue +} from './issue-id.js'; import { marked, Renderer } from '../vendor/marked.esm.js'; const summaryMarkdownRenderer = new Renderer(); @@ -12,7 +18,6 @@ export function clearIssues() { } let socialRefsByIssueId = new Map(); -const ISSUE_ID_LINE_PATTERN = /^([A-Z][A-Z0-9]{1,15}-\d{3,8})\s*[:\-]?\s*(.*)$/i; const socialRefsReady = loadSocialRefs(); const issueFileDataCache = new Map(); const issueFilePromiseCache = new Map(); @@ -436,36 +441,20 @@ function dedupeIssues(issues, versionKey) { } function getIssueDedupeKey(id, summary, resolved, caveat) { - const normalizedId = String(id || '').trim().toUpperCase(); + const normalizedId = normalizeIssueId(id); - if (normalizedId !== 'BLANK-000000') { + if (normalizedId !== BLANK_ISSUE_ID) { return normalizedId; } - const normalizedSummary = normalizeWhitespace(String(summary || '')); - const normalizedResolved = normalizeWhitespace(String(resolved || '')); - const normalizedCaveat = normalizeWhitespace(String(caveat || '')); + const normalizedSummary = normalizeWhitespace(summary); + const normalizedResolved = normalizeWhitespace(resolved); + const normalizedCaveat = normalizeWhitespace(caveat); return `${normalizedId}|${normalizedSummary}|${normalizedResolved}|${normalizedCaveat}`; } -function normalizeWhitespace(text) { - return String(text || '').replace(/\s+/g, ' ').trim(); -} - function parseIssueLine(text) { - const value = String(text || '').trim(); - const match = value.match(ISSUE_ID_LINE_PATTERN); - if (match) { - return { - id: match[1].toUpperCase(), - summary: (match[2] || '').trim() - }; - } - - return { - id: '', - summary: value - }; + return parseIssueLineValue(text); } function getSourceVersionFromFileName(fileName) { @@ -506,7 +495,7 @@ function renderSummaryCell(cell, issue) { } function getSocialRefsForIssue(issueId) { - const normalizedIssueId = String(issueId || '').trim().toUpperCase(); + const normalizedIssueId = normalizeIssueId(issueId); if (!normalizedIssueId) { return []; } diff --git a/web/js/process.js b/web/js/process.js index 9b11920..30cbbe4 100644 --- a/web/js/process.js +++ b/web/js/process.js @@ -1,4 +1,8 @@ import { buildIssueMarkdownDocument } from './markdown.js'; +import { + normalizeWhitespace, + parseIssueIdCell +} from './issue-id.js'; if (typeof document !== 'undefined') { document.addEventListener('DOMContentLoaded', () => { @@ -14,8 +18,6 @@ if (typeof document !== 'undefined') { let activeDownloadUrl = null; const PROCESS_FORM_STATE_KEY = 'bugmedley.process.formState.v1'; -const ISSUE_ID_PATTERN = /([A-Z][A-Z0-9]{1,15}-\d{3,8})/i; -const ISSUE_ID_LIST_PREFIX_PATTERN = /^\s*((?:[A-Z][A-Z0-9]{1,15}-\d{3,8})(?:\s*(?:,|and|&)\s*(?:[A-Z][A-Z0-9]{1,15}-\d{3,8}))*)/i; const BLOCK_CONTAINER_TAGS = new Set(['ARTICLE', 'ASIDE', 'BLOCKQUOTE', 'DIV', 'P', 'PRE', 'SECTION']); const TEXT_NODE = 3; const ELEMENT_NODE = 1; @@ -135,26 +137,18 @@ export function parseIssuesFromHtmlTable(htmlText, options = {}) { return; } - const issueIdListMatch = leftCellText.match(ISSUE_ID_LIST_PREFIX_PATTERN); - const isBlankId = !issueIdListMatch && /^[\u2014\u2013\-]+$/.test(leftCellText.trim()); - - if (!issueIdListMatch && !isBlankId) { + const issueIdData = parseIssueIdCell(leftCellText); + if (!issueIdData) { return; } - const issueIds = isBlankId - ? ['BLANK-000000'] - : Array.from(String(issueIdListMatch[1] || '').matchAll(new RegExp(ISSUE_ID_PATTERN.source, 'ig'))) - .map(match => String(match[1] || '').toUpperCase()) - .filter(Boolean); + const issueIds = issueIdData.issueIds; if (issueIds.length === 0) { return; } - const metadataText = isBlankId - ? '' - : normalizeWhitespace(leftCellText.slice(issueIdListMatch[0].length)).replace(/^[-:;,.\s]+/, ''); + const metadataText = issueIdData.metadataText; const resolved = isResolvedMetadataText(metadataText) ? metadataText : ''; const description = rightCellText; @@ -509,10 +503,6 @@ function sanitizeTableCellText(text) { return normalizeWhitespace(text).replace(/\|/g, '\\|'); } -function normalizeWhitespace(value) { - return value.replace(/\s+/g, ' ').trim(); -} - function setParseStatus(message, isError = false) { const status = document.getElementById('parseStatus'); status.textContent = message;