. 자료구조

앞의 전략에서 정의했듯이 구문을 분석하는 시점과 결과를 적용하는 시점이 분리되어 있기 때문에 분석결과를 저장할 수 있는 자료구조를 만들어야 한다. 분석 루틴은 구문을 분석한 후 그 결과를 저장해두고 적용할 때는 이 결과를 바탕으로 화면에 출력하게 된다. 자료구조를 정의하기 전에 문법강조와 관련된 코드를 작성할 Parse.h, Parse.cpp 모듈을 프로젝트에 추가하도록 하자. 프로젝트/새 항목 추가 메뉴를 선택하여 다음 대화상자를 띄우고 Parse.h, Parse.cpp라는 이름으로 C++ 파일과 헤더 파일을 각각 만든다.

이 두 파일에 문법강조를 위한 구조체와 클래스를 선언하고 코드를 작성할 것이다. 이 파일도 미리 컴파일된 헤더를 사용하도록 속성을 조정하도록 하자. 솔루션 탐색기의 Parse.cpp에서 팝업메뉴를 열고 속성 항목을 선택하여 다음 그림처럼 옵션을 변경하면 된다.

이렇게 되면 Parse.cpp도 미리 컴파일된 헤더를 사용하게 된다. 비주얼 C++ 6.0사용자들은 다음 대화상자에서 미리 컴파일된 헤더를 사용하도록 변경한다. 대화상자 모양만 다르지 옵션의 효과는 동일하다.

프로젝트의 모든 모듈들이 Parse.h에 선언된 분석기 객체를 참조할 수 있도록 stdafx.h Parse.h를 인클루드한다. SOption구조체가 분석 정보를 포함하기 때문에 Util.h보다 먼저 포함되어야 한다.

 

....

#include <wininet.h>

#include "Parse.h"

#include "Util.h"

#include "resource.h"

#include "ApiEdit.h"

 

stdafx.h에만 헤더를 포함하면 다른 모듈들은 이 헤더 파일을 더 이상 포함할 필요가 없다. 다만 다음을 위해서 ApiEdit.cpp에 비록 이 문장을 포함시켜 놓고 주석으로 처리해두는 것이 좋다. 당근 프로젝트는 미리 컴파일된 헤더를 사용하므로 ApiEdit.cpp Parse.h를 포함할 필요가 없지만 다음에 이 컨트롤을 다른 프로젝트에 사용할 때 포함되어 있지 않으면 약간의 혼란을 초래할 수도 있으므로 미리 혼란을 방지해두는 것이 좋을 것 같다.

구문 분석은 기본적으로 유닛(Unit) 단위로 수행되며 분석결과는 줄단위로 저장된다. 다음 두 구조체는 문법강조의 핵심 자료구조를 구성하며 분석결과를 이 구조체의 배열에 저장할 것이다. Parse.h에 이 구조체들을 선언한다.

 

#ifndef __PARSE_H

#define __PARSE_H

 

struct ParseUnit

{

     int pos;

     int style;

};

 

struct ParseInfo

{

     ParseUnit *pUnit;

     int UnitSize;

     DWORD Context;

};

 

struct SParseStyle

{

     SParseStyle();

     TCHAR name[32];

     COLORREF fore;

     COLORREF back;

};

 

#endif // __PARSE_H

 

ParseUnit 구조체는 한 유닛에 대한 분석 정보를 표현한다. 이 정의에서 유닛이라는 용어를 정확하게 정의하고 넘어 가도록 하자. 구문 분석은 보통 단어 단위로 이루어지며 각 단어는 고유한 문법적인 자격을 가진다. 단어란 공백 등의 구분자로 나누어진 문자의 집합이다. 예를 들어 struct tagDog { int foot; } 문장은 공백으로 구분되는 struct, tagDog, int 등의 단어마다 다른 품사를 가지게 된다.

하지만 for (i=0;i<sizeof(k);inc(i)) 등과 같이 공백으로 나누어지지 않고서도 품사가 바뀔 수 있기 때문에 구문 분석의 단위를 단어라고 정의할 수는 없다. 그래서 여기서는 구문 분석의 단위를 유닛으로 정의하기로 하자. 사실 유닛을 번역해 봐야 단위가 되는데 좀 더 분명한 설명을 위해 유닛이라는 용어를 도입하였다. 정리하자면 유닛이란 독립된 품사를 가질 수 있는 글자의 집합이며 구문 분석의 최소 단위이다.

다시 ParseUnit 구조체를 보도록 하자. pos는 유닛의 시작 오프셋이고 style은 이 유닛의 품사가 무엇인가에 대한 정보값이다. 유닛은 계속 연속적으로 이어지므로 끝 오프셋은 따로 저장할 필요가 없다. pos 123이고 style 5라면 오프셋 123번지부터 다음 유닛까지 5번 스타일로 해석되었다는 뜻이다. 다음 유닛의 pos 129라면 이 유닛의 길이는 자동적으로 6이 되므로 끝 오프셋은 굳이 저장할 필요가 없는 것이다.

style의 값은 분석기마다 다르게 정의한다. C분석기는 단어들을 키워드, 연산자, 상수, 주석 등으로 정의할 것이고 HTML 분석기는 태그, 속성, 스크립트 등으로 정의할 것이다. 문서들이 질적으로 다르기 때문에 스타일값도 미리 고정할 수 없으며 분석기가 자유롭게 값을 정의할 수 있도록 해야 한다. ParseUnit.style 멤버는 분석기가 정의한 스타일값을 기억할 뿐이며 이 스타일이 어떤 품사인지는 알 필요가 없다. 스타일의 정보는 분석기가 가지며 분석기가 해석한다.

ParseInfo 구조체는 한 줄에 대한 분석 정보를 표현한다. 한 줄은 여러 개의 유닛으로 구성되므로 줄에 대한 분석 정보는 유닛 분석 정보의 배열이 된다. 한 줄이 몇 개의 유닛으로 구성될 것인가는 미리 알 수 없으며 상한값이 없기 때문에 유닛의 배열은 동적으로 관리되어야 한다. 그래서 pUnit은 포인터로 선언되어 있으며 필요한 유닛의 개수만큼 동적으로 배열의 크기를 늘려나갈 것이다. UnitSize 멤버는 pUnit의 크기를 가지며 이 배열을 관리하기 위해 필요하다.

실제 문서가 어떤 식으로 분석되어 저장되는지 보도록 하자. 다음 문장은 C 문서의 변수 선언문인데 보다시피 세 개의 유닛으로 구성되어 있다.

이 줄이 분석되면 ParseInfo 구조체에 다음과 같이 기록된다.

세 개의 ParseUnit 구조체가 정의되어 있는데 첫 번째 유닛은 오프셋 0번부터 1번 스타일이라는 뜻이다. 마찬가지로 두 번째 유닛은 오프셋 4번부터 2번 스타일, 세 번째 유닛은 오프셋 9번부터 3번 스타일이라는 뜻이다. 분석 루틴에서 이런 식으로 ParseInfo를 작성해놓으면 적용 루틴은 이 정보를 읽어 int 1번 스타일이 정의하는 색상대로 출력하고 var; 2번 스타일로, //comment 3번 스타일대로 출력하게 된다.

ParseInfo의 마지막 멤버인 Context는 이전 줄의 분석결과인 컨텍스트값이다. 구문 분석은 원칙적으로 줄단위로 수행하므로 각 줄의 처음부터 분석 정보를 초기화한 후 분석하면 된다. 아주 특수한 경우 이전 줄의 분석결과에 따라 현재 줄의 분석 방법이 달라져야 하는데 C 언어의 블록 주석이 대표적인 예이다.

 

for (i=0;i<10,i++) { /* 이 루프는

     10번 실행된다. */

     j=j+i;        // j i를 합친다.

}

 

첫 번째 줄의 끝에서 블록 주석이 시작되었는데 이렇게 되면 두 번째 줄 이후는 */를 만나기 전에는 계속 주석이다. 키워드나 상수가 와도 무시하고 주석으로 해석해야 한다. 반면 세 번째 줄의 끝에 있는 주석은 한 줄 주석이므로 다음 줄에는 영향을 주지 않는다.

이런 식으로 다음 줄에 영향을 미치는 구문이 나왔으면 분석 정보에 기록해두어야 하며 이 정보를 Context 멤버에 저장한다. Context 3이면 이 줄이 블록 주석으로 끝났다는 뜻이며 다음 줄을 분석할 때 이 정보를 참조해야 한다. 두 번째 줄을 분석할 때는 첫 번째 줄의 컨텍스트값이 블록 주석이므로 키워드나 상수를 찾을 필요가 없으며 오로지 */만 찾으면 된다.

블록 주석과 같이 여러 줄이 같은 스타일을 가지는 경우가 많이 있는데 문자열 상수나 스크립트 블록 등도 좋은 예이다. 줄단위로 문서를 분석하되 분석결과를 연속적으로 적용하기 위해 컨텍스트 개념이 필요하다. Context 값을 어떻게 정의할 것인가와 어떻게 적용할 것인가는 문서 포맷에 따라 달라지므로 분석기가 자유롭게 정할 수 있다.

SParseStyle은 스타일의 글자색과 배경색에 대한 정보를 가지는 구조체이다. name주석, 키워드, 문자열 등과 같은 스타일 이름을 지정하며 fore, back은 이 스타일에 적용될 글자색과 배경색이다. Parse.cpp에는 이 구조체의 생성자를 정의한다.

 

//#include <windows.h>

//#include "Parse.h"

//#include "ApiEdit.h"

#include "stdafx.h"

 

SParseStyle::SParseStyle()

{

     name[0]=0;

     fore=(DWORD)-2;

     back=(DWORD)-1;

}

 

미리 컴파일된 헤더를 사용해야 하므로 선두에서 stdafx.h를 반드시 인클루드해야 한다. 스타일의 이름은 NULL, 전경색은 -2, 배경색은 -1로 초기화된다. -1은 컨트롤의 디폴트 색상이라는 뜻인데 구체적으로 ApiEdit cFore, cBack값이 적용된다. -2는 끝값이라는 뜻인데 이 구조체로 배열을 만들 경우를 위해 미리 fore -2를 대입하여 배열이 비어 있음을 나타내도록 하였다.

이상으로 구문 분석에 사용되는 세 개의 구조체에 대해 알아 보았는데 이 구조체를 이해하는 것은 이후의 코드를 이해하는데 중요하므로 잘 알아 두도록 하자.