174 lines
5.5 KiB
JavaScript
174 lines
5.5 KiB
JavaScript
"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:
|
|
* <span class="token punctuation">{</span><span class="token comment">/*
|
|
* And a HTML opening comment:
|
|
* <!--
|
|
* would look like this:
|
|
* <!--
|
|
*/
|
|
|
|
|
|
const HIGHLIGHTED_JSX_COMMENT_START = `<span class="token punctuation">\\{<\\/span><span class="token comment">\\/\\*`;
|
|
const HIGHLIGHTED_JSX_COMMENT_END = `\\*\\/<\\/span><span class="token punctuation">\\}</span>`;
|
|
const HIGHLIGHTED_HTML_COMMENT_START = `<!--`;
|
|
const PRISMJS_COMMENT_OPENING_SPAN_TAG = `(<span\\sclass="token\\scomment">)?`;
|
|
const PRISMJS_COMMENT_CLOSING_SPAN_TAG = `(<\\/span>)?`;
|
|
const COMMENT_START = new RegExp(`(#|\\/\\/|\\{\\/\\*|\\/\\*+|${HIGHLIGHTED_HTML_COMMENT_START})`);
|
|
|
|
const createDirectiveRegExp = featureSelector => new RegExp(`${featureSelector}-(next-line|line|start|end|range)({([^}]+)})?`);
|
|
|
|
const COMMENT_END = new RegExp(`(-->|\\*\\/\\}|\\*\\/)?`);
|
|
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 = /<span class="token plain-text">[^<]*\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 => [`<span class="gatsby-highlight-code-line">`, line, `</span>`].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, `</span>\n<span class="token plain-text">`));
|
|
}
|
|
|
|
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);
|
|
}; |