Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 74 additions & 20 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,79 @@
const CONDITIONAL_COMMENT_REGEX = /(<!(--)?\[if\s[()\w\s|&!]+\]>(?:<!--+>)?)((?:.|\n)*?)((?:<!--)?<!\[endif\]\2>)/gi;
// Token regexes to support nested conditional comments
const OPEN_REGEX = /<!(--)?\[if\s[()\w\s|&!]+\]>(?:<!--+>)?/gi;
// Capture whether the close carries trailing dashes ("--") before '>' to enforce symmetry
const CLOSE_REGEX = /(?:<!--)?<!\[endif\](--)?>/gi;

module.exports = function findConditionalComments(str) {
let comments = [];

let result;
while ((result = CONDITIONAL_COMMENT_REGEX.exec(str)) !== null) {
const [match, open, commentDashes, content, close] = result;

const bubble = open.endsWith("-->");

comments.push({
isComment: open.startsWith("<!--"),
open,
close,
bubble,
downlevel: bubble || commentDashes !== "--" ? "revealed" : "hidden",
range: [
CONDITIONAL_COMMENT_REGEX.lastIndex - match.length,
CONDITIONAL_COMMENT_REGEX.lastIndex,
],
});
const comments = [];

// Use a stack to handle nested conditional comments
const stack = [];
let position = 0;

while (position < str.length) {
// Find next open and next close from current position
OPEN_REGEX.lastIndex = position;
CLOSE_REGEX.lastIndex = position;

const openMatch = OPEN_REGEX.exec(str);
const closeMatch = CLOSE_REGEX.exec(str);

// If neither token is found, we're done
if (!openMatch && !closeMatch) {
break;
}

// Choose the earliest token occurrence
const nextOpenIndex = openMatch ? openMatch.index : Infinity;
const nextCloseIndex = closeMatch ? closeMatch.index : Infinity;

if (nextOpenIndex < nextCloseIndex) {
// Process opening token
const openText = openMatch[0];
const openStart = openMatch.index;
const openEnd = OPEN_REGEX.lastIndex;

const isComment = openText.startsWith("<!--");
const bubble = openText.endsWith("-->");

stack.push({
open: openText,
start: openStart,
end: openEnd,
isComment,
bubble,
});

position = openEnd;
continue;
}

// Process closing token
const closeText = closeMatch[0];
const closeStart = closeMatch.index;
const closeEnd = CLOSE_REGEX.lastIndex;

if (stack.length > 0) {
const openState = stack[stack.length - 1];
const closeHasDashes = Boolean(closeMatch[1]);
const openRequiresDashes = openState.isComment; // HTML comment style requires '-->'

if (closeHasDashes === openRequiresDashes) {
// Valid matching close for the most recent open
stack.pop();
comments.push({
isComment: openState.isComment,
open: openState.open,
close: closeText,
bubble: openState.bubble,
downlevel:
openState.bubble || !openState.isComment ? "revealed" : "hidden",
range: [openState.start, closeEnd],
});
}
}

position = closeEnd;
}

return comments;
Expand Down
51 changes: 51 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,54 @@ test("correctly marks comments as downlevel-revealed or downlevel-hidden", () =>
`)[0].downlevel
).toEqual("revealed");
});

test("supports nested conditional comments", () => {
const html = `
<!--[if mso]>
Outer start
<!--[if gte mso 15]>Inner<![endif]-->
Outer end
<![endif]-->
`;

const result = findConditionalComments(html);

expect(result.length).toBe(2);

// Identify outer and inner by range size
const sorted = [...result].sort(
(a, b) => a.range[1] - a.range[0] - (b.range[1] - b.range[0])
);
const inner = sorted[0];
const outer = sorted[1];

expect(inner.open.startsWith("<!--[if gte mso 15]>")).toBe(true);
expect(inner.close).toMatch(/<!\[endif\]-->/);
expect(inner.isComment).toBe(true);
expect(inner.downlevel).toBe("hidden");

expect(outer.open.startsWith("<!--[if mso]>")).toBe(true);
expect(outer.close).toMatch(/<!\[endif\]-->/);
expect(outer.isComment).toBe(true);
expect(outer.downlevel).toBe("hidden");

// Inner range should be fully inside outer range
expect(inner.range[0]).toBeGreaterThan(outer.range[0]);
expect(inner.range[1]).toBeLessThan(outer.range[1]);
});

test("rejects mismatched closing dashes for HTML-comment style open", () => {
const html = `
<!--[if mso]>Bad<![endif]>
`;
const result = findConditionalComments(html);
expect(result.length).toBe(0);
});

test("rejects mismatched closing dashes for downlevel-revealed open (non-comment)", () => {
const html = `
<![if mso]>Bad<![endif]-->
`;
const result = findConditionalComments(html);
expect(result.length).toBe(0);
});
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.