// @ts-check "use strict"; const { addErrorContext, emphasisMarkersInCodeSpans, forEachLine, includesSorted, isBlankLine } = require("../helpers"); const { lineMetadata } = require("./cache"); const emphasisRe = /(^|[^\\])(?:(\*\*?\*?)|(__?_?))/g; const asteriskListItemMarkerRe = /^(\s*)\*(\s+)/; const leftSpaceRe = /^\s+/; const rightSpaceRe = /\s+$/; module.exports = { "names": [ "MD037", "no-space-in-emphasis" ], "description": "Spaces inside emphasis markers", "tags": [ "whitespace", "emphasis" ], "function": function MD037(params, onError) { // eslint-disable-next-line init-declarations let effectiveEmphasisLength, emphasisIndex, emphasisKind, emphasisLength, pendingError = null; // eslint-disable-next-line jsdoc/require-jsdoc function resetRunTracking() { emphasisIndex = -1; emphasisLength = 0; emphasisKind = ""; effectiveEmphasisLength = 0; pendingError = null; } // eslint-disable-next-line jsdoc/require-jsdoc function handleRunEnd(line, lineIndex, contextLength, match, matchIndex) { // Close current run let content = line.substring(emphasisIndex, matchIndex); if (!emphasisLength) { content = content.trimStart(); } if (!match) { content = content.trimEnd(); } const leftSpace = leftSpaceRe.test(content); const rightSpace = rightSpaceRe.test(content); if (leftSpace || rightSpace) { // Report the violation const contextStart = emphasisIndex - emphasisLength; const contextEnd = matchIndex + contextLength; const context = line.substring(contextStart, contextEnd); const column = contextStart + 1; const length = contextEnd - contextStart; const leftMarker = line.substring(contextStart, emphasisIndex); const rightMarker = match ? (match[2] || match[3]) : ""; const fixedText = `${leftMarker}${content.trim()}${rightMarker}`; return [ onError, lineIndex + 1, context, leftSpace, rightSpace, [ column, length ], { "editColumn": column, "deleteCount": length, "insertText": fixedText } ]; } return null; } // Initialize const ignoreMarkersByLine = emphasisMarkersInCodeSpans(params); resetRunTracking(); forEachLine( lineMetadata(), (line, lineIndex, inCode, onFence, inTable, inItem, onBreak) => { const onItemStart = (inItem === 1); if (inCode || inTable || onBreak || onItemStart || isBlankLine(line)) { // Emphasis resets when leaving a block resetRunTracking(); } if (inCode || onBreak) { // Emphasis has no meaning here return; } if (onItemStart) { // Trim overlapping '*' list item marker line = line.replace(asteriskListItemMarkerRe, "$1 $2"); } let match = null; // Match all emphasis-looking runs in the line... while ((match = emphasisRe.exec(line))) { const ignoreMarkersForLine = ignoreMarkersByLine[lineIndex] || []; const matchIndex = match.index + match[1].length; if (includesSorted(ignoreMarkersForLine, matchIndex)) { // Ignore emphasis markers inside code spans continue; } const matchLength = match[0].length - match[1].length; const matchKind = (match[2] || match[3])[0]; if (emphasisIndex === -1) { // New run emphasisIndex = matchIndex + matchLength; emphasisLength = matchLength; emphasisKind = matchKind; effectiveEmphasisLength = matchLength; } else if ( (matchLength === effectiveEmphasisLength) && (matchKind === emphasisKind) ) { // Ending an existing run, report any pending error if (pendingError) { addErrorContext(...pendingError); pendingError = null; } const error = handleRunEnd( line, lineIndex, effectiveEmphasisLength, match, matchIndex); if (error) { addErrorContext(...error); } // Reset resetRunTracking(); } else if (matchLength === 3) { // Swap internal run length (1->2 or 2->1) effectiveEmphasisLength = matchLength - effectiveEmphasisLength; } else if (effectiveEmphasisLength === 3) { // Downgrade internal run (3->1 or 3->2) effectiveEmphasisLength -= matchLength; } else { // Upgrade to internal run (1->3 or 2->3) effectiveEmphasisLength += matchLength; } } if (emphasisIndex !== -1) { pendingError = pendingError || handleRunEnd(line, lineIndex, 0, null, line.length); // Adjust for pending run on new line emphasisIndex = 0; emphasisLength = 0; } } ); } };