"use strict"; const rangeParser = require(`parse-numeric-range`); /** * As code has already been prism-highlighted at this point, * a JSX opening comment: * {/* * would look like this: * {/* * And a HTML opening comment: * |\\*\\/\\}|\\*\\/)?`); const DIRECTIVE = createDirectiveRegExp(`(highlight|hide)`); const HIGHLIGHT_DIRECTIVE = createDirectiveRegExp(`highlight`); const END_DIRECTIVE = { highlight: /highlight-end/, hide: /hide-end/ }; const PLAIN_TEXT_WITH_LF_TEST = /[^<]*\n[^<]*<\/span>/g; const stripComment = line => /** * This regexp does the following: * 1. Match a comment start, along with the accompanying PrismJS opening comment span tag; * 2. Match one of the directives; * 3. Match a comment end, along with the accompanying PrismJS closing span tag. */ line.replace(new RegExp(`\\s*(${HIGHLIGHTED_JSX_COMMENT_START}|${PRISMJS_COMMENT_OPENING_SPAN_TAG}${COMMENT_START.source})\\s*${DIRECTIVE.source}\\s*(${HIGHLIGHTED_JSX_COMMENT_END}|${COMMENT_END.source}${PRISMJS_COMMENT_CLOSING_SPAN_TAG})`), ``); const highlightWrap = line => [``, line, ``].join(``); // const wrapAndStripComment = line => wrap(stripComment(line)) const parseLine = (line, code, index, actions) => { const [, feature, directive, directiveRange] = line.match(DIRECTIVE); const flagSource = { feature, index, directive: `${feature}-${directive}${directiveRange}` }; switch (directive) { case `next-line`: actions.flag(feature, index + 1, flagSource); actions.hide(index); break; case `start`: { // find the next `${feature}-end` directive, starting from next line const endIndex = code.findIndex((line, idx) => idx > index && END_DIRECTIVE[feature].test(line)); const end = endIndex === -1 ? code.length : endIndex; actions.hide(index); actions.hide(end); for (let i = index + 1; i < end; i++) { actions.flag(feature, i, flagSource); } break; } case `line`: actions.flag(feature, index, flagSource); actions.stripComment(index); break; case `range`: actions.hide(index); if (directiveRange) { const strippedDirectiveRange = directiveRange.slice(1, -1); const range = rangeParser.parse(strippedDirectiveRange); if (range.length > 0) { range.forEach(relativeIndex => { actions.flag(feature, index + relativeIndex, flagSource); }); break; } } console.warn(`Invalid match specified: "${line.trim()}"`); break; } }; module.exports = function highlightLineRange(code, highlights = []) { if (highlights.length > 0 || HIGHLIGHT_DIRECTIVE.test(code)) { // HACK split plain-text spans with line separators inside into multiple plain-text spans // separatered by line separator - this fixes line highlighting behaviour for jsx code = code.replace(PLAIN_TEXT_WITH_LF_TEST, match => match.replace(/\n/g, `\n`)); } const split = code.split(`\n`); const lines = split.map(code => { return { code, highlight: false, hide: false, flagSources: [] }; }); const actions = { flag: (feature, line, flagSource) => { if (line >= 0 && line < lines.length) { const lineMeta = lines[line]; lineMeta[feature] = true; lineMeta.flagSources.push(flagSource); } }, hide: line => actions.flag(`hide`, line), highlight: line => actions.flag(`highlight`, line), stripComment: line => { lines[line].code = stripComment(lines[line].code); } }; const transform = lines => lines.filter(({ hide, highlight, flagSources }, index) => { if (hide && highlight) { const formattedSources = flagSources.map(({ feature, index, directive }) => ` - Line ${index + 1}: ${feature} ("${directive}")`).join(`\n`); throw Error(`Line ${index + 1} has been marked as both hidden and highlighted.\n${formattedSources}`); } return !hide; }).map(line => { if (line.highlight) { line.code = highlightWrap(line.code); } return line; }); // If a highlight range is passed with the language declaration, e.g. // ``jsx{1, 3-4} // we only use that and do not try to parse highlight directives if (highlights.length > 0) { highlights.forEach(lineNumber => { actions.highlight(lineNumber - 1); }); return transform(lines); } for (let i = 0; i < split.length; i++) { const line = split[i]; if (DIRECTIVE.test(line)) { parseLine(line, split, i, actions); } } return transform(lines); };