Problem#
Given a string representing a code snippet, implement a tag validator to parse the code and return whether it is valid.
A code snippet is valid if all the following rules hold:
- The code must be wrapped in a valid closed tag. Otherwise, the code is invalid.
- A closed tag (not necessarily valid) has exactly the following format :
<TAG_NAME>TAG_CONTENT</TAG_NAME>
. Among them, <TAG_NAME>
is the start tag, and </TAG_NAME>
is the end tag. The TAG_NAME in start and end tags should be the same. A closed tag is valid if and only if the TAG_NAME and TAG_CONTENT are valid.
- A valid
TAG_NAME
only contain upper-case letters , and has length in range [1,9]. Otherwise, the TAG_NAME
is invalid.
- A valid
TAG_CONTENT
may contain other valid closed tags , cdata and any characters (see note1) EXCEPT unmatched <
, unmatched start and end tag, and unmatched or closed tags with invalid TAG_NAME. Otherwise, the TAG_CONTENT
is invalid.
- A start tag is unmatched if no end tag exists with the same TAG_NAME, and vice versa. However, you also need to consider the issue of unbalanced when tags are nested.
- A
<
is unmatched if you cannot find a subsequent >
. And when you find a <
or </
, all the subsequent characters until the next >
should be parsed as TAG_NAME (not necessarily valid).
- The cdata has the following format :
<![CDATA[CDATA_CONTENT]]>
. The range of CDATA_CONTENT
is defined as the characters between <![CDATA[
and the first subsequent ]]>
.
CDATA_CONTENT
may contain any characters. The function of cdata is to forbid the validator to parse CDATA_CONTENT
, so even it has some characters that can be parsed as tag (no matter valid or invalid), you should treat it as regular characters.
Examples#
Example 1#
1
2
3
4
5
6
7
|
Input: code = "<DIV>This is the first line <![CDATA[<div>]]></DIV>"
Output: true
Explanation:
The code is wrapped in a closed tag : <DIV> and </DIV>.
The TAG_NAME is valid, the TAG_CONTENT consists of some characters and cdata.
Although CDATA_CONTENT has an unmatched start tag with invalid TAG_NAME, it should be considered as plain text, not parsed as a tag.
So TAG_CONTENT is valid, and then the code is valid. Thus return true.
|
Example 2#
1
2
3
4
5
6
7
8
9
10
11
12
|
Input: code = "<DIV>>> ![cdata[]] <![CDATA[<div>]>]]>]]>>]</DIV>"
Output: true
Explanation:
We first separate the code into : start_tag|tag_content|end_tag.
start_tag -> **" <DIV>"**
end_tag -> **" </DIV>"**
tag_content could also be separated into : text1|cdata|text2.
text1 -> **" >> ![cdata[]] "**
cdata -> **" <![CDATA[<div>]>]]>"**, where the CDATA_CONTENT is **" <div>]>"**
text2 -> **"]]>>]"**
The reason why start_tag is NOT **" <DIV>>>"** is because of the rule 6.
The reason why cdata is NOT **" <![CDATA[<div>]>]]>]]>"** is because of the rule 7.
|
Example 3#
1
2
3
|
Input: code = "<A> <B> </A> </B>"
Output: false
Explanation: Unbalanced. If "<A>" is closed, then "<B>" must be unmatched, and vice versa.
|
Constraints#
1 <= code.length <= 500
code
consists of English letters, digits, '<'
, '>'
, '/'
, '!'
, '['
, ']'
, '.'
, and ' '
.
Solution#
Approach#
We use a stack to track open tags and parse the string character by character. For each tag, we check validity and match with the stack. CDATA sections are skipped as plain text. The code is valid if the stack is empty at the end and all rules are satisfied.
Code#
Java#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
import java.util.*;
class Solution {
public boolean isValid(String code) {
Deque<String> stack = new ArrayDeque<>();
int i = 0, n = code.length();
while (i < n) {
if (i > 0 && stack.isEmpty()) return false;
if (code.startsWith("<![CDATA[", i)) {
int j = code.indexOf("]]>", i);
if (j < 0) return false;
i = j + 3;
} else if (code.startsWith("</", i)) {
int j = code.indexOf('>', i);
if (j < 0) return false;
String tag = code.substring(i + 2, j);
if (stack.isEmpty() || !isValidTag(tag) || !stack.peek().equals(tag)) return false;
stack.pop();
i = j + 1;
} else if (code.startsWith("<", i)) {
int j = code.indexOf('>', i);
if (j < 0) return false;
String tag = code.substring(i + 1, j);
if (!isValidTag(tag)) return false;
stack.push(tag);
i = j + 1;
} else {
i++;
}
}
return stack.isEmpty();
}
private boolean isValidTag(String tag) {
if (tag.length() < 1 || tag.length() > 9) return false;
for (char c : tag.toCharArray()) if (c < 'A' || c > 'Z') return false;
return true;
}
}
|
Python#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
class Solution:
def isValid(self, code: str) -> bool:
stack = []
i, n = 0, len(code)
while i < n:
if i > 0 and not stack:
return False
if code.startswith('<![CDATA[', i):
j = code.find(']]>', i)
if j == -1:
return False
i = j + 3
elif code.startswith('</', i):
j = code.find('>', i)
if j == -1:
return False
tag = code[i+2:j]
if not stack or not (1 <= len(tag) <= 9 and tag.isupper()) or stack[-1] != tag:
return False
stack.pop()
i = j + 1
elif code.startswith('<', i):
j = code.find('>', i)
if j == -1:
return False
tag = code[i+1:j]
if not (1 <= len(tag) <= 9 and tag.isupper()):
return False
stack.append(tag)
i = j + 1
else:
i += 1
return not stack
|
C++#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
#include <stack>
#include <string>
using namespace std;
class Solution {
public:
bool isValid(string code) {
stack<string> st;
int i = 0, n = code.size();
while (i < n) {
if (i > 0 && st.empty()) return false;
if (code.substr(i, 9) == "<![CDATA[") {
int j = code.find("]]>", i);
if (j == string::npos) return false;
i = j + 3;
} else if (code.substr(i, 2) == "</") {
int j = code.find('>', i);
if (j == string::npos) return false;
string tag = code.substr(i + 2, j - (i + 2));
if (st.empty() || !isValidTag(tag) || st.top() != tag) return false;
st.pop();
i = j + 1;
} else if (code[i] == '<') {
int j = code.find('>', i);
if (j == string::npos) return false;
string tag = code.substr(i + 1, j - (i + 1));
if (!isValidTag(tag)) return false;
st.push(tag);
i = j + 1;
} else {
i++;
}
}
return st.empty();
}
bool isValidTag(const string& tag) {
if (tag.size() < 1 || tag.size() > 9) return false;
for (char c : tag) if (c < 'A' || c > 'Z') return false;
return true;
}
};
|
Rust#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
impl Solution {
pub fn is_valid(code: String) -> bool {
let mut stack = Vec::new();
let code = code.as_bytes();
let mut i = 0;
while i < code.len() {
if i > 0 && stack.is_empty() { return false; }
if code[i..].starts_with(b"<![CDATA[") {
if let Some(j) = code[i..].windows(3).position(|w| w == b"]]>" ) {
i += j + 3;
} else { return false; }
} else if code[i..].starts_with(b"</") {
if let Some(j) = code[i..].iter().position(|&c| c == b'>') {
let tag = &code[i+2..i+j];
if stack.is_empty() || tag.len() < 1 || tag.len() > 9 || !tag.iter().all(|&c| c >= b'A' && c <= b'Z') || stack.last().unwrap() != &tag {
return false;
}
stack.pop();
i += j + 1;
} else { return false; }
} else if code[i] == b'<' {
if let Some(j) = code[i..].iter().position(|&c| c == b'>') {
let tag = &code[i+1..i+j];
if tag.len() < 1 || tag.len() > 9 || !tag.iter().all(|&c| c >= b'A' && c <= b'Z') {
return false;
}
stack.push(tag);
i += j + 1;
} else { return false; }
} else {
i += 1;
}
}
stack.is_empty()
}
}
|
TypeScript#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
function isValid(code: string): boolean {
const stack: string[] = [];
let i = 0;
while (i < code.length) {
if (i > 0 && stack.length === 0) return false;
if (code.startsWith('<![CDATA[', i)) {
const j = code.indexOf(']]>', i);
if (j === -1) return false;
i = j + 3;
} else if (code.startsWith('</', i)) {
const j = code.indexOf('>', i);
if (j === -1) return false;
const tag = code.slice(i + 2, j);
if (stack.length === 0 || !/^[A-Z]{1,9}$/.test(tag) || stack[stack.length - 1] !== tag) return false;
stack.pop();
i = j + 1;
} else if (code[i] === '<') {
const j = code.indexOf('>', i);
if (j === -1) return false;
const tag = code.slice(i + 1, j);
if (!/^[A-Z]{1,9}$/.test(tag)) return false;
stack.push(tag);
i = j + 1;
} else {
i++;
}
}
return stack.length === 0;
}
|
Explanation#
We use a stack to track open tags. For each tag, we check validity and match with the stack. CDATA sections are skipped as plain text. The code is valid if the stack is empty at the end and all rules are satisfied.
Complexity#
- ⏰ Time complexity:
O(n)
where n is the length of code
.
- 🧺 Space complexity:
O(n)
for the stack in the worst case.