CodeSpitz78 3/ (stack ๊ตฌ์กฐ) HTML parser โจโจ
๐๐๐
๐ฅ ์ฝ๋์คํผ์ธ ์์ ์ ์๊ฐํ๋ฉด์ ๋ณต์ตํ ๋ด์ฉ์ ์ ๋ฆฌํ์ต๋๋ค. ์ฐธ๊ณ : ๋ ๋๋ง ์์ง - ํ์ฑย
1. ๊ฐ์ ์ด๋ค ์ํฉ์ ๋ณด๊ณ ๊ตฌ์กฐ์ ์ด๊ณ ์ฌ๊ท์ ์ธ ํํ๋ก ํ์ ์ ํ ์ ์๋๋, ๋ฐ์ดํฐ ๋ถ์์ ํ ์
์๋๋..
BNF
<๊ธฐํธ> ::= <ํํ์>
- ๋ด๋ถ ๊ตฌ์ฑ์์๋ก๋ถํฐ ์์ฉ๊ตฌ์ฑ์์ ํ์ฅํ๋ ๊ฒ์ BNF ์ ์๋ฐฉ์
- ์ธ์ด์ ๊ตฌ์ฑ์์๋ฅผ ์ ์ํ๋ ์ฌ๋ฌ ๋ฌธ๋ฒ๋ค์ด ์๋ค (lex, yak)
html ๊ธฐ๋ณธ ํจํด
A = <tag>body</tag>
B = <tag />
C = text
body = (A|B|C)N
์์ ๊ทธ๋ฆผ์ ํ์ฑํ ์ ์๋ ํ์๋ฅผ ๋ง๋ค๊ณ ์ถ๋ค! ์ผ์ด์ค๊ฐ ์ฌ๊ท๋ฉด์ ๋ณตํฉ์ ์ธ ์ํฉ์ ์งค ์ ์๋ค๋ฉด ์ค๊ธ๊ฐ๋ฐ์
ํ์์ ๊ธฐ๋ณธ ๊ตฌ์กฐ
- ํจ์์ ๋ชฉํ, ์ธ์๊ฐ๊ณผ ๋ฆฌํด๊ฐ์ด ๋ฌด์์ธ์ง ์ ์ํ๋ค.
- ๋ฌด์จ ์ธ์๋ฅผ ๋ฐ์์ ๋ฌด์์ ๋ฆฌํดํ์ง?
๋ฌธ์์ด์ ์ฝ์ด์ ๊ตฌ์กฐ์ ์ผ๋ก ๊ฐ์ฒดํ ์์ผ ๋ฆฌํดํ๊ฒ ํ๊ณ ์ถ๋ค.
interface Result {
name?: string;
type: string; // text, node
children: Result[];
}
interface StackItem {
tag: Result;
}
const parser = (input: string): Result[] => {
input = input.trim();
// ์ด๊ธฐํ
const result = { name: 'ROOT', type: 'node', children: [] };
const stack = [{ tag: result }];
let curr,
i = 0,
j = input.length;
while ((curr = stack.pop())) {
while (i < j) {
const cursor = i; // ํท๊ฐ๋ฆฌ๊ธฐ๋๋ฌธ์ ์กฐํ์ฉ์ผ๋ก ๋ฐ๋ก
if (input[cursor] === '<') {
// '<'๋ก ์์ํ๋ฉด ํ๊ทธ: A,B์ ๊ฒฝ์ฐ
} else {
// ํ
์คํธ ํ์
: C์ ๊ฒฝ์ฐ
}
}
}
return result;
};์ค์ฒฉ๋ฃจํ
- ์ฒซ๋ฒ์งธ loop๋ ๋์ ๋ฃจํ
- ๋ฃจํ๋ฅผ ๊ฒฐ์ ํ๋ ์์ธ์ด ์์ ๋ฃจํ๋ฅผ ๋๋ค๊ฐ ๋ณํ ์๋ ์๋ค.
- ์ด๋ฐ ๋ฃจํ๊ฐ ๊ธฐ๋ณธ์ด๋ค. ์ต์ํด์ง์ ~
- ์กฐ๊ฑด์ด
false๊ฐ ๋ ๋๊น์ง loop
- ๋๋ฒ์งธ loop๋ ์ ํด์ ธ์๋ ๋ฃจํ
- ์ค์บ๋ ๋ฃจํ๋ผ๊ณ ํ๋ค.
- ๊ณํ๋์ง ์์ loop๋ ์ํํ๋ค๋ ์๊ฐ์ ๋ฒ๋ฆฌ์.
- ์คํ์์ค์ผ๋ก loop๋ฅผ ๋์์ผํ๋ค. ๋๋ฌธ์ ์คํ๊ตฌ์กฐ ๋ฃจํ ์๋์ ์๊ณ ๋ฆฌ์ฆ์ด ์๋ ์ํฉ์ด๋ค
2. ํ ์คํธ ๋ ธ๋ Cํ์ : ํ ์คํธ
2-1. ์์
๋ค์ ํ๊ทธ๊น์ง๋ฅผ ํ์
- name: ํ์ฌ ์ปค์ ~ ๋ค์ ํ๊ทธ๊น์ง์ ํ ์คํธ ์ถ์ถ
- type: โtextโ
- children์ ์๋ค.
interface Result {
name?: string,
type: string, // text, node
children: Result[]
}
interface StackItem {
tag: Result
}
const parser = (input: string): Result[] => {
input = input.trim();
const result = {name: 'ROOT', type: 'node', children: []};
const stack = [{tag: result}];
let curr, i = 0, j = input.length;
while(curr = stack.pop()){
while(i < j){
const cursor = i;
if(input[cursor] === '<'){
// '<'๋ก ์์ํ๋ฉด ํ๊ทธ: A,B์ ๊ฒฝ์ฐ
} else {
// ํ
์คํธ ํ์
: C์ ๊ฒฝ์ฐ
๐๐๐
const idx = input.indexOf('<', cursor);
// cursor์์น์์๋ถํฐ < ์์น: ๋ค์ ํ๊ทธ ์ ๊น์ง
curr.tag.children.push({
type: 'text',
text: input.substring(cursor, idx);
}); // children์ด ์๊ธฐ ๋๋ฌธ์ children์ ํจ์ค
i = idx; // ๋ค์ ์์์ง์ ์ ์ฎ๊น
๐๐๐
}
}
};
return result;
}2-2. ๋ถ๋ฆฌ
- ๋ก์ง์ด ๋ ๋ฆฝ์ ์ด๋ค.
- ๋ฐ๋ก ๋ถ๋ฆฌ ๊ฐ๋ฅ !
curr๋๋ฌธ์ ๊ฒฐํฉ๋๊ฐ ์ฌ๋ผ๊ฐ์ง๋ง ์ด์ฉ ์ ์๋ ๋ถ๋ถ
์ญํ ์ ์ธ์ํ์๋ง์!! ๋ถ๋ฆฌํ์. ๋์ค์ ๋ถ๋ฆฌํ ๋๋ ์ด๋ฏธ ์ค์ผ๋์ด์์ด์ ๋ถ๋ฆฌ์ํค๊ธฐ ์ด๋ ต๋ค.
const textNode = (input: string, cursor: number, curr: StackItem): number => {
const idx = input.indexOf('<', cursor);
curr.tag.children.push({
type: 'text',
text: input.substring(cursor, idx);
});
return idx;
}interface Result {
name?: string,
type: string, // text, node
children: Result[]
}
interface StackItem {
tag: Result
}
const parser = input => {
input = input.trim();
const result = {name: 'ROOT', type: 'node', children: []};
const stack = [{tag: result}];
let curr, i = 0, j = input.length;
while(curr = stack.pop()){
while(i < j){
const cursor = i; // ํท๊ฐ๋ฆฌ๊ธฐ๋๋ฌธ์ ์กฐํ์ฉ์ผ๋ก ๋ฐ๋ก
if(input[cursor] === '<'){
// '<'๋ก ์์ํ๋ฉด ํ๊ทธ: A,B์ ๊ฒฝ์ฐ
} else {
๐๐๐
i = textNode(input, cursor, curr)
๐๐๐
}
}
};
return result;
}
// ๋ชฉํ: textNode๋ฅผ ์์ฑ + result ๋ฐฐ์ด์ ์ถ๊ฐ => idx๊ฐ(ํ
์คํธ ๋
ธ๋์ ๋ง์ง๋ง ์์๊ฐ) ๋ฐํ
const textNode = (input: string, cursor: number, curr: StackItem): number => {
const idx = input.indexOf('<', cursor);
curr.tag.children.push({
type: 'text',
text: input.substring(cursor, idx);
});
return idx;
}์ฝ๋๋ฅผ ์งค ๋๋ ๋ฌด์กฐ๊ฑด ์ฌ์ด ๊ฒ ๋ถํฐ ์ฒ๋ฆฌํ๋ค.
- why? ์ฌ์ด๊ฒ์ ํน์ง์ :
- ์์กด์ฑ์ด ๋ฎ๋ค.
- ๋ ๋ฆฝ๋ ๊ธฐ๋ฅ์ผ ๊ฒฝ์ฐ๊ฐ ๋๋ค.
- ์์กด์ฑ์ด ๋ฎ์ ๋ชจ๋๋ถํฐ ๋์ ๋ชจ๋๋ก ์ฌ๋ผ๊ฐ์.
3. ํ๊ทธ ๋ ธ๋
<๊ฐ ๋ฐ๋ ํธ๋ฆฌ๊ฑฐ๋ก ์ฐ์ด๊ณ ์๋ค.
- ์ด๋ ๊ฒ weekํ๊ฒ ?
- ํ์๋ฅผ ๋ง๋ค ๋๋ ์ด๋ ๊ฒ seperator, token ํํ์ ํธ๋ฆฌ๊ฑฐ๊ฐ ๋ง๋ค์ด์ง๋ค.
ํธ๋ฆฌ๊ฑฐ๊ฐ ๋ฐ๋๋๋ ์ผ์ด์ค๊ฐ 3๊ฐ์ง ์ข ๋ฅ๊ฐ ์๋ค.
- ์์ํ๊ทธ
- ๋ซ๋ํ๊ทธ
- ์๋ฃํ๊ทธ
- ๊ณตํต์ ์์ํ๊ทธ๋ก ์์ํด์ ๋ซ๋ํ๊ทธ๋ก ๋๋๋ค.
๊ณตํต์ ์ ์ฐพ์์ ์ฝ๋๋ฅผ ์ค๋ณต์ํค๋ ๊ฒ์ ํผํด์ผํ๋ค. ๋์ ํ๋ จํด์ ๋จผ์ ๊ณตํต์์๋ฅผ ์ถ์ํ ํ ์ ์๋ ๋ฅ๋ ฅ์ ํค์์ผํ๋ค.
3.1 empty element <img />, open tag <div>
empty element๊ฐ ๋ ๊ฐ๋จํด ๋ณด์ด๋ฏ๋ก ๋จผ์ ์ง๊ธ
const parser = input => {
input = input.trim();
const result = {name: 'ROOT', type: 'node', children: []}; // ๋ฆฌํด๊ฐ : ์ด๋ฆ์ด ๋ญ๊ณ , ํ์
์ด ๋ญ๊ณ , children์ ๋ญ ๊ฐ๊ณ ์๋์ง.
const stack = [{tag: result}]; // DOM ๊ฐ์ฒด
let curr, i = 0, j = input.length;
while(curr = stack.pop()){
while(i < j){
const cursor = i; // ํท๊ฐ๋ฆฌ๊ธฐ๋๋ฌธ์ ์กฐํ์ฉ์ผ๋ก ๋ฐ๋ก
if(input[cursor] === '<'){
// '<'๋ก ์์ํ๋ฉด ํ๊ทธ: A,B์ ๊ฒฝ์ฐ
// ํ์ฌ cursor |<
const idx = input.indexOf('>', cursor);
// ํ์ฌ idx |>
i = idx + 1;
if(input[cursor + 1] === '/'){
// close ํ๊ทธ
} else {
๐๐๐
if(input[idx - 1] === '/'){
// empty ํ๊ทธ
} else {
// open ํ๊ทธ
}
}
๐๐๐
} else i = textNode(input, cursor, curr)
}
};
return result;
}
const textNode = (input, cursor, curr) => {...};cf__1 ์ฝ๋ ์ค๊ณ๋ฅผ ์ํ์ ์ข์ ์ฝ๋๋ฅผ ์ง๋ ๋น๋ฐ์ - ํ ์คํธ ์ฃผ๋ ๊ฐ๋ฐ์ ์๋ ๊ฒ์ด ์๋๋ผ, -
๋ฐ์ดํฐ๋ฅผ ์ดํดํ๊ณ ์ฌ๊ท์ ์ธ ๋ก์ง์ ์ฐพ์๋ด๊ฑฐ๋ - ์ถ์ํ๋ ๊ณตํต์ ์ ์ฐพ์๋ด๊ฑฐ๋ - ์ญํ ์ ์ดํดํ๊ฑฐ๋์ ์๋ค.
๋จธ๋ฆฟ์์ ๋งจํจ๋ชจ๋ธ์ด ๊ทธ๋ ค์ง๋ฉด ์ฝ๋๋ก ๋๊ฐ์ด ํํ๋์ด์ผ์ง ์ ์์ด๋ค.
- ๊ทธ๋์ผ ๋์ค์ ์ ์ง๋ณด์๊ฐ ๋๋ค.
- ๊ทธ๋ฌ๊ธฐ ๋๋ฌธ์ ์ฃผ์์ด ํ์์๋ค.
๋ฐ๋ฅธ ๋ฐ์ดํฐ ๋ชจ๋ธ๋ง์ด ๋ผ์์ผ๋ฉด
- ์ฝ๋๋ ๊ทธ๋ฅ ๋งคํ์ฉ์ด๋ค.
let name, isClose; // ๐ ๊ณตํต ์ค๋น์ฌํญ ์ธต
if (input[idx - 1] === '/') {
// ๐ ๊ณตํต ์ฒ๋ฆฌ์ฌํญ ์ธต ~
// empty element ํ๊ทธ
// name์ '<'์ '>'์ฌ์ด
name = input.substring(cursor + 1, idx - 1);
isClose = true;
} else {
// open ํ๊ทธ
name = input.substring(cursor + 1, idx);
isClose = false;
}
const tag = { name, type: 'node', children: [] }; // ๐ ํก์ํ๋? ์ธต
curr.tag.children.push(tag);์ผ์ด์ค๋ ๋ค ๊ฐ์ผ๋ก ๋ฐ๊ฟ ์ ์๋ค.
- ์ผ์ด์ค์ ์ฐจ์ด๋ฅผ ๊ฐ์ผ๋ก ํก์ํด์ ํ๋์ ์๊ณ ๋ฆฌ์ฆ์ผ๋ก ๋ง๋ฆ.
- ๋ฉ๋ชจ๋ฆฌ(name, isClose)์ ์ฐ์ฐ(์กฐ๊ฑด๋ฌธ)์ ๊ตํ๋๋ค.
- ์ฐ์ฐ์ ๋ฉ๋ชจ๋ฆฌ๋ก ๋ฐ๊ฟ => ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๊ฐ๋ฆฌํค๋ ํ๋์ ์ฐ์ฐ๋ง ๊ธฐ์ ํ๋ฉด ๋๋ค.
์์ ์ฝ๋ ํํ
- ๊ณตํต ์ค๋น์ฌํญ ์ธต
- ๊ณตํต ์ฒ๋ฆฌ์ฌํญ ์ธต
- ๋ค๋ฅธ์ ์ ๊ธฐ์ ํ๋ ๋ถ๋ถ์ ํก์ํ๋ ์ธต
cf__2 ํ์ดํธ๋ฆฌ์ค ํ์ดํธ๋ฆฌ์ค whitelist โ์์ โ์ด ์ฆ๋ช ๋ ๊ฒ๋ง์ ํ์ฉํ๋ ๊ฒ์ผ๋ก โ์ ์์ฑโ์ด
์ ์ฆ๋ ๊ฒ์ ์ฐจ๋จํ๋ ๋ธ๋๋ฆฌ์คํธ ๋ณด์๊ณผ ์๋ฐ๋๋ ๋ณด์ ๋ฐฉ์ ์ด๋ค. ํ์ดํธ๋ฆฌ์คํธ, ๋ธ๋๋ฆฌ์คํธ๋ผ๋ ์ฉ์ด ๋์ โpositiveโ์ โnagativeโ ๋ณด์ ๋ฐฉ๋ฒ์ผ๋ก ๋ถ๋ ค์ง๊ธฐ๋ ํฉ๋๋ค.
const idx = input.indexOf('>', cursor);
// ํ์ฌ idx |>
i = idx + 1;
if(input[cursor + 1] === '/'){
// close ํ๊ทธ
} else {
let name, isClose;
if(input[idx - 1] === '/'){
// empty element ํ๊ทธ
// name์ '<'์ '>'์ฌ์ด
name = input.substring(cursor + 1, idx - 1);
isClose = true;
} else {
// open ํ๊ทธ
name = input.substring(cursor + 1, idx);
isClose = false;
}
const tag = {name, type: 'node', children: []};
curr.tag.children.push(tag);
๐๐๐
if(!isClose){
stack.push({tag, back:curr});
// โจโจ ๋ฆฌํดํฌ์ธํธ๋ฅผ ์๋์ผ๋ก ์ ์ํ๋ ์ํฉ.
break; // while๋ฌธ์ break
}
๐๐๐
}๋ถ๋ฆฌ ~
const idx = input.indexOf('>', cursor);
// ํ์ฌ idx |>
i = idx + 1;
if(input[cursor + 1] === '/'){
// close ํ๊ทธ
} else {
if(elementNode(input, cursor, idx, curr, stack)) break;
}
...
const elementNode = (
input: string,
cursor: number,
idx: number,
curr: StackItem,
stack: StackItem[]): boolean => {
let name, isClose;
if(input[idx - 1] === '/'){
// empty element ํ๊ทธ
// name์ '<'์ '>'์ฌ์ด
name = input.substring(cursor + 1, idx - 1);
isClose = true;
} else {
// open ํ๊ทธ
name = input.substring(cursor + 1, idx);
isClose = false;
}
const tag = {name, type: 'node', children: []};
curr.tag.children.push(tag);
if(!isClose){
stack.push({tag, back:curr});
// ๋ฆฌํดํฌ์ธํธ๋ฅผ ์๋์ผ๋ก ์ ์ํ๋ ์ํฉ.
return true;
}
return false;
}
cf__3 ๊ฐ๋ ์ฑ์ด ๋์ ์ฝ๋? (๋ณ์๋ช ์ ์ด์๊ฒ ์ฐ๋, ์ปจ๋ฒค์ ์ ์งํค๋) ์ฝ๋๊ฐ ๋ฆฌ๋๋ธํ๋ค?
Readable - ์ ์ ํ ์ญํ ๋ชจ๋ธ๋ก ์์๋์ ๊ทธ๋ค๊ฐ์ ํต์ ๊ณผ ํ์ ๋ง ๋ณผ ์ ์๋ ์ฝ๋๊ฐ ๊ฐ๋ ์ฑ์ด ๋์ ์ฝ๋์ด๋ค.
3.2 close tag
stack.push({ tag, back: curr });...
const idx = input.indexOf('>', cursor);
// ํ์ฌ idx |>
i = idx + 1;
if(input[cursor + 1] === '/'){
๐๐๐
// close ํ๊ทธ
curr = curr.back;
๐๐๐
} else {
if(elementNode(input, cursor, idx, curr, stack)) break;
}
...
const elementNode = (input, cursor, idx, curr, stack) => {
let name, isClose;
if(input[idx - 1] === '/'){
// empty element ํ๊ทธ
// name์ '<'์ '>'์ฌ์ด
name = input.substring(cursor + 1, idx - 1);
isClose = true;
} else {
// open ํ๊ทธ
name = input.substring(cursor + 1, idx);
isClose = false;
}
const tag = {name, type: 'node', children: []};
curr.tag.children.push(tag);
if(!isClose){
stack.push({tag, back:curr});
// ๋ฆฌํดํฌ์ธํธ๋ฅผ ์๋์ผ๋ก ์ ์ํ๋ ์ํฉ.
return true;
}
return false;
}
cf__4 css์์ถ์ด๋ javascript์์ถ๋ณด๋ค html์์ถ์ด ๋ธ๋ผ์ฐ์ ์ ๋ถํ๋ฅผ ์ค์ด๋ ๋ฐฉ๋ฒ ์ธ๋ฐ์๋
๋ ธ๋์์ฑ์ ์ค์ธ๋ค.
const elementNode = (input, cursor, idx, curr, stack) => {
const isClose = input[idx - 1] === '/';
const tag = {
name: input.substring(cursor + 1, idx - (isClose ? 1 : 0)),
type: 'node',
children: [],
};
curr.tag.children.push(tag);
if (!isClose) {
stack.push({ tag, back: curr });
// โจโจ ๋ฆฌํดํฌ์ธํธ๋ฅผ ์๋์ผ๋ก ์ ์ํ๋ ์ํฉ.
return true;
}
return false;
};4. ์ต์ข
const parser = input => {
input = input.trim();
const result = { name: 'ROOT', type: 'node', children: [] };
const stack = [{ tag: result }]; // DOM ๊ฐ์ฒด
let curr,
i = 0,
j = input.length;
while ((curr = stack.pop())) {
while (i < j) {
const cursor = i; // ํท๊ฐ๋ฆฌ๊ธฐ๋๋ฌธ์ ์กฐํ์ฉ์ผ๋ก ๋ฐ๋ก
if (input[cursor] === '<') {
const idx = input.indexOf('>', cursor);
i = idx + 1;
if (input[cursor + 1] === '/') {
// ๋ซ๋ํ๊ทธ
curr = curr.back;
} else {
if (elementNode(input, cursor, idx, curr, stack)) break; // ๋๋ฒ์งธ ๋ฃจํ break
}
} else i = textNode(input, cursor, curr);
}
}
return result;
};
const textNode = (input, cursor, curr) => {
const idx = input.indexOf('<', cursor);
curr.tag.children.push({
type: 'text',
text: input.substring(cursor, idx),
});
return idx;
};
const elementNode = (input, cursor, idx, curr, stack) => {
const isClose = input[idx - 1] === '/';
const tag = {
name: input.substring(cursor + 1, idx - (isClose ? 1 : 0)),
type: 'node',
children: [],
};
curr.tag.children.push(tag);
if (!isClose) {
stack.push({ tag, back: curr });
// โจโจ ๋ฆฌํดํฌ์ธํธ๋ฅผ ์๋์ผ๋ก ์ ์ํ๋ ์ํฉ.
return true;
}
return false;
};