. 분석 기반 클래스

구문 분석을 하는 주체는 분석기이며 클래스로 구현할 것이다. 문서 포맷별로 구문 분석의 방법이 질적으로 달라지므로 포맷당 하나의 분석기를 만들며 ApiEdit는 적용할 문법에 따라 분석기 객체를 실행중에 바꿔 가며 사용한다. 이렇게 되려면 모든 분석기를 대표하는 부모 클래스가 있어야 한다. ApiEdit는 부모 클래스의 포인터 하나만 가지고 있으면 모든 분석기를 제어할 수 있다. 알다시피 부모 클래스형의 포인터는 모든 파생 클래스의 객체를 가리킬 수 있기 때문이다.

문서의 포맷에 따라 구문 분석을 하는 방법이 아무리 질적으로 다르다 해도 공통적으로 공유할 수 있는 코드가 많이 있다. 분석 정보를 관리하는 방법이나 ApiEdit와 통신하는 방법 등은 문법과 상관없이 모든 분석기에 적용된다. 그래서 이런 공통되는 코드는 부모 클래스에 한 번만 작성해놓고 파생 클래스들은 이 코드를 상속받아서 사용한다. 공통 코드이므로 각 분석기마다 따로 작성할 필요가 없다.

공통 코드와 ApiEdit의 분석기 제어 편의를 위해 분석 기반 클래스를 작성하기로 한다. Parse.h에 다음과 같이 CParse 클래스를 작성하도록 하자.

 

class CApiEdit;

class CParse

{

protected:

     int ParseSize;

     SParseStyle arStyle[32];

 

public:

     ParseInfo *pInfo;

     CParse();

     ~CParse();

     void InitInfo(BOOL bAlloc);

     void DeleteParseInfo(int nLine);

     void MakeParseInfo(int nLine, int &nUnit, int pos, int style);

     BOOL IsStringExist(TCHAR *list,TCHAR *str,int len,BOOL bCase);

     BOOL IsNumber(CApiEdit &ae,int s, int e);

     void ParseLines(CApiEdit &ae,int nLine);

     void GetStyleColor(int style, COLORREF &fore, COLORREF &back);

     void SetStyleColor(int style, COLORREF fore, COLORREF back);

     void GetStyleName(int style, TCHAR *szName);

 

     virtual void ParseLine(CApiEdit &ae,int nLine)=0;

     virtual TCHAR *GetInfo(int iIndex)=0;

};

 

분석기는 문서 내용을 읽어야 하며 따라서 ApiEdit buf, off 등의 핵심 멤버변수에 액세스해야 한다. 그래서 CParse의 멤버함수는 분석 대상이 되는 CApiEdit 객체의 레퍼런스를 인수로 취하는데 이 원형을 선언하려면 CParse를 선언하기 전에 CApiEdit가 클래스형이라는 것을 미리 알려줘야 한다. 첫 줄의 class CApiEdit; 전방 선언문이 바로 이런 일을 한다. CApiEdit 클래스는 CParse 객체를 멤버로 가지고 CParse 클래스는 CApiEdit 객체를 인수로 취하므로 상호 참조를 하고 있는 셈이다. 이럴 때는 헤더 파일의 순서를 정할 수 없으므로 먼저 인클루드되는 쪽이 반대쪽 클래스에 대한 전방 선언을 가져야 한다.

멤버변수는 세 개 선언되어 있는데 pInfo가 바로 분석 정보이다. 한 줄이 몇 개의 유닛으로 구성될 것인지 상한선이 없는 것과 마찬가지로 하나의 문서가 몇 개의 줄로 구성될 것인가도 상한선이 없다. 그래서 줄 분석 정보 배열도 동적으로 관리되어야 한다. pInfo는 분석 정보 배열이며 문서의 줄 수만큼 동적으로 할당된다. 분석기 외부에서 이 정보를 수시로 참조해야 하므로 pInfo public 액세스 지정을 가진다.

ParseSize는 분석 정보 배열의 할당된 크기값이며 pInfo 배열 관리에 사용된다. ParseSize는 외부에서 직접 액세스하지 않으므로 protected 액세스 속성으로 선언되었다. arStyle 배열은 이 분석기가 정의하는 스타일 정보이며 생성자에서 초기화된다. 배열의 크기가 32이며 마지막 요소는 끝 표시에 사용되므로 분석기당 최대 31개의 스타일을 정의할 수 있다.

멤버함수는 생성자와 파괴자를 포함하여 모두 13개가 선언되어 있는데 각 함수에 대한 간략한 설명은 다음과 같다.

 

함수

설명

생성자

분석 정보 배열을 초기 할당한다.

파괴자

분석 정보 배열을 해제한다.

InitInfo

분석 정보 배열을 해제하고 재할당하여 초기화한다.

DeleteParseInfo

nLine 이하 모든 줄의 분석 정보를 삭제하여 무효화한다.

MakeParseInfo

분석 정보를 작성한다.

GetInfo

ID값을 조사해준다. 인라인으로 작성되어 있다.

ParseLines

nLine줄까지 분석하여 분석 정보 배열에 결과를 저장한다.

ParseLine

nLine줄을 분석하여 분석 정보를 작성한다.

GetStyleColor

스타일에 따른 색상값을 조사한다.

SetStyleColor

스타일에 따른 색상값을 초기화한다.

GetStyleName

스타일의 이름을 구한다.

IsStringExist

문자열이 존재하는지 검사한다.

IsNumber

숫자인지 검사한다.

 

이 함수들은 CParse 클래스가 정의하고 파생 클래스는 이 함수를 사용하기만 하며 모든 분석기가 공유하는 코드이다. 분석 정보 배열을 초기화하거나 생성, 삭제하는 동작은 문법과는 무관하기 때문에 부모 클래스에 한 번만 작성해놓으면 된다. 문법이 달라진다고 해서 배열을 작성하는 방법 자체가 달라지지는 않으며 다만 작성되는 내용이 달라질 뿐이다.

GetInfo 함수와 ParseLine 함수는 분석기별로 고유하다. ParseLine 함수는 실제로 구문을 분석하는 함수인데 이 함수가 어떤 식으로 문서를 해석하는가에 따라 분석결과가 달라지게 된다. GetInfo 함수는 분석기의 ID나 구분자, 주석 등에 대한 정보를 제공한다. 이 두 함수는 부모 클래스가 구현하지 않으므로 순수 가상함수로 선언되어 있으며 따라서 파생 클래스는 이 함수들을 반드시 재정의해야 한다.

CParse 클래스는 순수 가상함수를 가지고 있으므로 객체를 만들 수 없는 추상 클래스로 선언되어 있다. 이 클래스의 구현 코드는 Parse.cpp에 다음과 같이 작성한다.

 

CParse::CParse()

{

     pInfo=NULL;

     InitInfo(TRUE);

}

 

CParse::~CParse()

{

     InitInfo(FALSE);

}

 

void CParse::InitInfo(BOOL bAlloc)

{

     int i;

 

     if (pInfo) {

          for (i=0;i<ParseSize;i++) {

              if (pInfo[i].pUnit) {

                   free(pInfo[i].pUnit);

              }

          }

          free(pInfo);

     }

 

     if (bAlloc) {

          ParseSize=100;

          pInfo=(ParseInfo *)malloc(sizeof(ParseInfo)*ParseSize);

          for (i=0;i<ParseSize;i++) {

              pInfo[i].UnitSize=5;

              pInfo[i].pUnit=(ParseUnit *)malloc(sizeof(ParseUnit)*pInfo[i].UnitSize);

              memset(pInfo[i].pUnit,-1,sizeof(ParseUnit)*pInfo[i].UnitSize);

              pInfo[i].Context=0;

          }

     }

}

 

void CParse::MakeParseInfo(int nLine, int &nUnit, int pos, int style)

{

     if (nUnit > 0) {

          if (pInfo[nLine].pUnit[nUnit-1].pos == pos) {

              nUnit--;

          } else

          if (pInfo[nLine].pUnit[nUnit-1].style == style) {

              return;

          }

     }

 

     if (pInfo[nLine].UnitSize == nUnit) {

          pInfo[nLine].UnitSize++;

          pInfo[nLine].pUnit=(ParseUnit *)realloc(pInfo[nLine].pUnit,

              sizeof(ParseUnit)*pInfo[nLine].UnitSize);

     }

 

     pInfo[nLine].pUnit[nUnit].pos=pos;

     pInfo[nLine].pUnit[nUnit].style=style;

     nUnit++;

}

 

void CParse::DeleteParseInfo(int nLine)

{

     int l;

 

     for (l=nLine;;l++) {

          if (l>=ParseSize || pInfo[l].pUnit[0].pos == -1)

              break;

 

          memset(pInfo[l].pUnit,-1,sizeof(ParseUnit)*pInfo[l].UnitSize);

     }

}

 

void CParse::ParseLines(CApiEdit &ae,int nLine)

{

     int l,i;

     int OldSize;

 

     if (nLine >= ParseSize) {

          OldSize=ParseSize;

          ParseSize=nLine+100;

          pInfo=(ParseInfo *)realloc(pInfo,sizeof(ParseInfo)*ParseSize);

          for (i=OldSize;i<ParseSize;i++) {

              pInfo[i].UnitSize=5;

              pInfo[i].pUnit=(ParseUnit *)malloc(sizeof(ParseUnit)*pInfo[i].UnitSize);

              memset(pInfo[i].pUnit,-1,sizeof(ParseUnit)*pInfo[i].UnitSize);

              pInfo[i].Context=0;

          }

     }

 

     if (pInfo[nLine].pUnit[0].pos != -1)

          return;

 

     for (l=nLine;l>=0;l--) {

          if (pInfo[l].pUnit[0].pos != -1)

              break;

     }

     l++;

 

     for (i=l;i<=nLine;i++) {

          ParseLine(ae,i);

     }

}

 

BOOL CParse::IsStringExist(TCHAR *list,TCHAR *str,int len,BOOL bCase)

{

     TCHAR *tbuf;

     BOOL ret;

 

     tbuf=(TCHAR *)malloc(len+3);

     tbuf[0]=‘ ‘;

     lstrcpyn(tbuf+1,str,len+1);

     tbuf[len+1]=‘ ‘;

     tbuf[len+2]=0;

     if (bCase==FALSE) {

          CharLower(tbuf);

     }

     ret=(strstr(list,tbuf)!=NULL);

     free(tbuf);

     return ret;

}

 

BOOL CParse::IsNumber(CApiEdit &ae,int s, int e)

{

     int i;

     TCHAR ch;

 

     if (ae.buf[s]==‘0’ && (ae.buf[s+1]==‘x’ || ae.buf[s+1]==‘X’)) {

          for (i=s+2;i<=e;i++) {

              ch=ae.buf[i];

              if (!((ch >= ‘0’ && ch <= ‘9’) ||

                     (ch >= ‘a’ && ch <= ‘f’) ||

                     (ch >= ‘A’ && ch <= ‘F’)))

                   return FALSE;

          }

     } else {

          for (i=s;i<=e;i++) {

              ch=ae.buf[i];

              if (!(ch >= ‘0’ && ch <= ‘9’))

                   return FALSE;

          }

     }

 

     return TRUE;

}

 

void CParse::GetStyleColor(int style, COLORREF &fore, COLORREF &back)

{

     fore=arStyle[style].fore;

     back=arStyle[style].back;

}

 

void CParse::SetStyleColor(int style, COLORREF fore, COLORREF back)

{

     arStyle[style].fore=fore;

     arStyle[style].back=back;

}

 

void CParse::GetStyleName(int style, TCHAR *szName)

{

     lstrcpy(szName,arStyle[style].name);

}

 

순수 가상함수와 인라인 함수를 제외한 여섯 개의 함수 코드가 작성되어 있다. 이 함수들은 모든 분석기들이 공통적으로 사용하는 공용 함수들이다. 코드를 분석해보자.

분석 정보 배열 초기화

분석기는 분석 정보 배열의 포인터인 pInfo만 가지고 있으므로 분석결과를 저장하기 위해 이 배열을 먼저 할당하고 초기화해야 한다. 이 작업은 InitInfo 멤버함수가 주로 하게 되며 생성자와 파괴자 그리고 ApiEdit InitDoc에서 이 함수를 호출한다. 코드를 보고 분석해보자.

배열을 할당하기 전에 먼저 이미 할당되어 있는 상태이면 할당된 메모리를 해제해야 한다. 문서가 바뀔 때 이전 문서의 분석 정보를 지워야 하기 때문이다. pInfo NULL이 아니면 즉 할당되어 있는 상태이면 pInfo는 물론이고 pInfo의 멤버인 pUnit도 같이 삭제한다.

pInfo를 깨끗하게 비운 후 새로 할당하되 단 bAlloc인수가 TRUE일 때만 할당한다. 이 인수가 FALSE이면 그냥 pInfo를 삭제하기만 한다. 분석기를 완전히 제거하고자 할 때 bAlloc FALSE가 된다. 초기 할당량은 총 100줄의 분석 정보를 저장할 수 있도록 하고 각 줄당 5개의 유닛을 저장할 수 있도록 하였다. pInfo 배열은 크기 100으로, pUnit 배열은 크기 5로 초기화된다.

두 배열의 초기 할당량은 어디까지나 초기값에 불과하다. 줄 수나 유닛수는 실행중에 필요한 만큼 재할당되므로 처음부터 크게 할당할 필요가 없으며 적당한 크기만큼만 할당하면 된다. 줄 수를 100으로 잡은 것은 특별한 의미는 없으며 그냥 단순한 임의의 값이다. 유닛의 초기 크기 5는 상당히 심사숙고 끝에 결정한 것인데 이 값이 너무 크면 메모리가 낭비될 수 있으며 너무 작으면 재할당이 빈번해져서 느려질 수 있다. 대부분의 문서에서 스타일이 5번 바뀔 정도면 비교적 충분한 것 같아 5로 결정했다. 만약 메모리를 좀 더 쓰더라도 속도를 높이고 싶다면 이 값을 좀 더 크게 잡아 주면 된다.

메모리 할당 후 유닛의 pos, style 멤버는 모두 -1로 초기화하였다. 위치값인 pos -1을 가지는 것은 아직 분석되지 않은 정보라는 뜻으로 정의한다. Context 멤버는 모두 0으로 초기화하여 특별한 컨텍스트를 가지지 않도록 하였다. InitInfo 함수에 의해 pInfo 배열은 다음 그림처럼 초기화된다.

개념적으로는 2차원 배열과 거의 비슷한 모양이다. 수직으로 100줄의 길이를 가지며 한 줄에 다섯 개의 스타일을 저장할 수 있는 용량이다. 문서의 문법이 복잡해지면 이 배열이 수평으로 확장될 것이며 문서의 길이가 길어지면 수직으로 확장될 것이다.

InitInfo 함수는 생성자와 파괴자에서 각각 호출되는데 그 의미는 다르다. 생성자는 pInfo NULL로 초기화한 후 InitInfo(TRUE)를 호출하므로 메모리를 해제하지는 않고 초기 할당만 한다. 반면 파괴자는 InitInfo(FALSE)를 호출하므로 메모리를 해제하기만 하고 재할당은 하지 않는다. InitInfo 함수는 bAlloc 인수의 값에 따라 해제와 초기화를 동시에 수행하는 함수이다.

분석 정보 작성

함수이름을 보면 알겠지만 분석 정보를 작성하는 일은 MakeParseInfo 함수가 한다. 이 함수는 4개의 인수를 전달받는데 nLine이 줄번호이고 nUnit은 유닛의 번호이다. pInfo 2차원 배열이라고 할 때 nLine은 배열의 y 좌표값이고 nUnit은 배열의 x 좌표에 해당한다. 분석 정보를 작성한 후에 유닛 번호는 한 칸 뒤로 이동해야 하므로 nUnit은 레퍼런스로 전달받는다.

나머지 두 개의 인수 pos, style은 실제 분석 정보에 해당하는 오프셋, 스타일값이다. MakeParseInfo 함수가 하는 가장 중요한 일은 이 두 값을 pInfo.pUnit에 기록하는 것이며 다음 두 줄이 핵심 코드이다.

 

void CParse::MakeParseInfo(int nLine, int &nUnit, int pos, int style)

{

     ....

     pInfo[nLine].pUnit[nUnit].pos=pos;

     pInfo[nLine].pUnit[nUnit].style=style;

}

 

인수로 전달된 값을 pUnit 배열에 기록한다. 이 두 줄의 앞에 있는 나머지 코드는 메모리관리와 중복 정보 방지 코드이다. pInfo 배열은 동적으로 할당되기 때문에 nLine 줄과 nUnit 유닛이 항상 존재한다는 보장이 없다. nLine 줄이 없으면 재할당해야 하는데 이 작업은 MakeParseInfo 함수를 호출하는 호출측에서 하므로 nLine은 항상 유효하다.

그러나 nUnit은 있을 수도 있고 없을 수도 있다. pUnit은 최초 5의 크기로 할당되므로 pUnit 배열은 pUnit[4]까지만 유효하다. 이 상태에서 nUnit 5가 되면 pUnit 배열을 재할당하여 늘려야 한다. MakeParseInfo 함수는 분석 정보를 pInfo에 기록하기 전에 nUint을 기록할 공간이 있는지 먼저 조사하고 없으면 pUnit의 크기를 하나 더 늘려 재할당한다. 배열의 크기가 작기 때문에 여유분은 주지 않았다.

함수의 선두에 있는 다음 코드는 약간의 설명이 필요하다. 이 코드는 중복된 정보를 작성하지 않도록 하는 두 가지 조건 점검을 하고 있다.

 

     if (nUnit > 0) {

          if (pInfo[nLine].pUnit[nUnit-1].pos == pos) {

              nUnit--;

          } else

          if (pInfo[nLine].pUnit[nUnit-1].style == style) {

              return;

          }

     }

 

첫 번째 조건은 현재 작성하는 유닛과 이전 유닛의 위치가 같으면 이전 유닛을 삭제하는 역할을 한다. n번째 유닛과 n-1번째 유닛의 위치가 같다는 말을 n-1번째 유닛의 길이가 0이라는 뜻이며 이 유닛은 존재할 필요가 없으므로 삭제되어야 한다. 분석기는 줄의 분석을 시작하기 전에 항상 보통 문자열 유닛을 pUnit[0]에 배치하여 분석 정보를 초기화한 후 키워드나 주석 등을 찾는다. 그러다 보니 다음과 같은 분석 정보가 만들어질 수 있다.

 

pUnit[0] : 오프셋 0부터 보통 문자열

pUnit[1] : 오프셋 0부터 키워드

pUnit[2] : 오프셋 5부터 보통 문자열

pUnit[3] : 오프셋 5부터 주석

 

보다시피 pUnit[0] pUnit[2]는 길이가 0이므로 불필요하게 공간만 낭비하고 있는 것이다. 물론 있어도 결국 화면출력은 되지 않기 때문에 무해하지만 출력속도에도 불리하고 메모리를 좀먹는 이런 유닛을 그대로 내버려 둘 필요가 없다. 이때 nUnit 1감소시켜 새로 작성되는 유닛이 이전 유닛을 덮어쓰도록 하였다.

두 번째 조건은 현재 작성하는 유닛과 이전 유닛이 같은 스타일이면 두 유닛을 통합한다. 다음과 같은 경우이다.

 

pUnit[0] : 오프셋 0부터 보통 문자열

pUnit[1] : 오프셋 1부터 키워드

pUnit[2] : 오프셋 5부터 키워드

pUnit[3] : 오프셋 30부터 주석

 

pUnit[1] pUnit[2]는 스타일이 같으므로 통합할 수 있으며 사실상 pUnit[2]는 전혀 불필요한 존재이다. 그래서 이런 경우는 아예 분석 정보를 작성하지 않는다. pUnit[1]만 있어도 오프셋1부터 다음 유닛의 위치인 오프셋 30까지가 키워드라는 것을 알 수 있다. 물론 pUnit[2]를 그냥 내버려 두어도 두 번 나누어 출력될 뿐이지 이상하게 동작하는 것은 아니다.

두 조건 모두 최소한 하나의 유닛은 작성되어 있는 상태에서만 점검할 수 있으므로 if (nUnit > 0) 조건 블록에 싸여 있다. 이 함수에서 중복 정보를 방지하는 서비스를 하고 있기 때문에 분석기는 무엇인가 발견되는 족족 이 함수를 불러 주기만 하면 된다. 이 두 조건 점검은 누군가가 반드시 해야 하는데 CParse 부모 클래스가 책임지고 하므로 파생 분석기의 분석 코드가 간단해진다.

분석 정보 삭제

분석 정보를 삭제한다는 것은 분석된 결과를 무효화한다는 뜻이다. 문서가 조금이라도 편집되면 편집된 위치 이후의 내용에 대해서는 다시 구문 분석을 해야 한다. 그렇다고 해서 편집된 즉시 구문 분석을 다시 할 필요는 없으며 분석 정보를 무효화시켜 놓기만 하면 된다. ApiEdit는 화면에 출력하기 전에 무효한 분석 정보를 유효하게 만든 후, 즉 재분석한 후 출력하도록 되어 있다. 분석 정보를 삭제하는 것은 곧 이후의 내용에 대해서는 구문 분석을 다시 하라는 명령을 pInfo에 남겨 놓는 것이다.

분석 정보 삭제는 DeleteParseInfo 함수에서 한다. nLine 인수로 줄번호를 받는데 이 줄 이후의 모든 분석 정보를 삭제하여 무효화한다. 통상 편집이 일어난 줄번호가 nLine으로 전달되며 그 줄 이후를 다시 분석하도록 한다.

분석 정보를 삭제하는 것은 아주 간단하다. nLine부터 시작해서 pInfo의 끝까지 또는 분석 정보가 이미 삭제되어 있는 줄까지 루프를 돌며 pUnit pos를 모두 -1로 바꿔 놓으면 된다. pos -1값을 가지는 것은 아직 분석되지 않았다는 뜻을 가지도록 정의되어 있다. pos -1로 바꾸며 할당되어 있는 메모리까지 해제할 필요는 없다. 왜냐하면 무효화된 줄을 재분석하면 결국 그 메모리가 또 필요하기 때문이다.

문서 분석 및 분석 정보 배열 재할당

ParseLines 함수는 외부에서 볼 때 문서를 분석하는 주체이며 인수로 전달된 nLine 줄까지 분석을 한다. 그러나 직접 문서 분석을 하는 것은 아니며 분석을 위한 준비만 하고 각 줄별로 분석 함수를 호출할 뿐이다. 하지만 이 함수에 의해 분석 정보가 작성되므로 ApiEdit는 문서 분석이 필요할 때 이 함수를 호출하기만 하면 nLine까지 완전히 분석된 상태가 된다. 즉 이 함수는 문서 분석의 시작점이면서 외부와 통신하는 가장 중요한 인터페이스 함수이다.

ParseLines 함수가 하는 가장 중요한 일은 분석 정보 배열을 관리하는 것이다. pInfo 배열의 초기 크기 100은 대부분의 문서에서 부족한 크기이다. 그러나 필요할 때 배열크기를 늘릴 수 있기 때문에 크기가 작다고 해서 문제가 되지는 않는다. 분석기는 문서를 분석하면서 항상 현재 할당된 배열크기인 ParseSize가 작지 않은지 점검하고 작으면 필요한 만큼 크기를 늘린다. 예를 들어 pInfo 100줄까지 할당되어 있는 상태에서 105번째 줄의 분석 정보를 작성한다면 이 크기를 105 120정도의 충분한 크기로 늘릴 수 있다.

배열의 공간이 언제 부족할지 알 수 없으므로 이 작업은 분석 정보를 작성할 때마다 매번 해야 하는데 모든 분석 루틴에서 이 작업을 하기는 무척 번거롭다. 아마 정보를 작성하는 코드보다 메모리를 관리하는 코드가 더 많아질 것이다. 그래서 분석을 시작하는 시점인 이 함수의 선두에서만 배열을 관리한다. 모든 분석 관련 함수가 호출되기 전에 이 함수가 먼저 호출되므로 여기서 배열을 충분하게 할당해놓기만 하면 나머지 코드는 안심하고 pInfo 배열을 사용할 수 있다.

만약 ParseSize nLine보다 작거나 같다면 pInfo 배열은 nLine이상의 크기를 가지도록 재할당한다. 작을 때뿐만 아니라 같을 때도 재할당을 해야 함을 유의하도록 하자. 왜냐하면 nLine Zero Base의 배열 첨자인데 비해 ParseSize One Base의 배열크기이기 때문이다. nLine 100일 때 즉 100번 줄의 분석 정보를 기록하고 있다고 해보자. 이때 ParseSize 100이라면 pInfo 99까지만 할당되어 있는 상태이므로 이때도 배열을 재할당하여 pInfo[100]이 존재하도록 해야 한다.

배열 크기를 늘릴 때 nLine만큼 정확하게 크기를 늘리는 것이 아니라 미리 100줄만큼의 여유분을 주어 충분한 크기로 늘려 주도록 하여 재할당 횟수를 최소화하도록 하였다. 이런 기법은 앞에서도 많이 선보인 적이 있으므로 이제 아주 익숙할 것이다. ParseSize nLine+100으로 변경하고 이 크기만큼 pInfo 배열을 재할당하였으며 새로 추가된 줄에 대해 pUnit도 새로 할당하였다. ParseLines 함수는 분석 루틴에서 필요로 하는 nLine 줄이 반드시 pInfo 배열에 존재하도록 보장하는 역할을 한다.

분석을 시작하기 전에 이미 분석되어 있는 줄인지를 먼저 점검해보고 분석되어 있으면 그냥 리턴한다. nLine 줄의 첫 번째 유닛의 pos -1이 아니라면 이 줄까지 분석이 되어 있는 것으로 판단할 수 있다. 호스트는 무조건 이 함수를 호출하도록 되어 있으므로 반드시 분석 완료 여부를 점검해보아야 한다. 변화가 없는 문서를 다시 그리기만 한다면 분석 정보는 유효하므로 재분석할 필요가 없다.

배열 관리와 분석 완료 점검 후 분석 대상 줄을 계산하는데 분석할 끝 줄은 인수로 전달된 nLine이며 분석의 시작줄은 따로 찾아야 한다. nLine에서 시작하여 위로 올라가면서 분석되지 않은 첫 줄을 찾는다. 이 줄은 문서가 변경된 오프셋의 시작 문단줄인데 화면상의 첫 줄일 수도 있고 화면상의 중간쯤일 수도 있고 문서의 처음인 0번째 줄일 수도 있다. 구문 분석은 항상 이전 줄의 컨텍스트를 참조해야 하므로 nLine보다 앞에 있는 줄은 모두 같이 분석되어야 한다.

그래서 설사 분석 시작줄이 화면상에 보이지 않는다 하더라도 그 줄부터 분석을 해 와야 한다. for 루프에서 nLine부터 시작하여 첫 번째 유닛이 -1이 아닌 첫 줄을 찾고 그 다음 줄을 취하면 이 줄이 바로 분석되지 않은 첫 번째 줄이다. 분석할 첫 번째 줄 l이 결정되면 l~nLine까지 루프를 돌며 ParseLine 함수를 호출하여 각 줄을 개별적으로 분석한다. 이 함수가 진짜 분석을 하는 주체이며 루프를 모두 돌았을 때는 nLine까지 분석이 완료되어 있을 것이다.

문자열 점검

IsStringExist 함수는 분석기의 구문 분석을 보조하는 역할을 한다. 각 문법들은 키워드, 함수, 태그, 연산자 등의 고유한 구성 요소들을 가지는데 이 요소들은 문법에 미리 정의된 문자열 중 하나여야 한다. 예를 들어 HTML 태그는 table, body, img 등이 있는데 지금 분석하고 있는 부분이 이 중 하나인지 조사해야 한다. 어떤 문자열이 문자열 목록 중 하나에 속하는지 아닌지를 점검하는 것이 이 함수의 주된 목적이다.

list는 문자열 목록을 가지는데 각 문자열은 공백으로 분리되어 있어야 한다. str은 검사할 문자열이며 len은 문자열의 길이이다. bCase는 대소문자를 구분해서 검사할 것인가 아닌가를 지정한다. C 언어는 대소문자를 구분하며 HTML이나 베이직 같은 문법은 대소문자를 구분하지 않는다. 분석기는 검사하고자 하는 문자열 목록을 하나의 배열에 작성해놓고 지금 분석하고 있는 위치의 문자열이 이 목록에 있는지 검사하기 위해 이 함수를 호출한다. 예를 들어 과일의 이름을 목록으로 정의한다고 하자.

 

TCHAR *list=" apple orange banana strawberry grape ";

 

분석하고 있는 문자열이 이 중 하나인지 조사하려면 IsStringExist(list, str, 6, FALSE)라고 호출하면 된다. strorange라고 했을 때 이 함수가 어떻게 문자열을 찾는지 보자. 전달된 검색 문자열의 길이보다 3바이트 더 긴 버퍼를 할당하고 문자열의 양쪽에 공백, 그리고 끝에 널 종료문자열을 배치한다. 검색 대상 문자열은 다음과 같아진다.

양쪽에 공백을 두는 이유는 다른 단어의 일부를 찾지 않도록 하기 위해서이다. 만약 이렇게 하지 않으면 berry는 목록에 없지만 strawberry의 일부에 속해 있으므로 이 단어가 목록에 있다고 오판을 하게 될 것이다. 대소문자를 구분하지 않는다면 검색 문자열을 모두 소문자로 바꾼 후 비교한다.

이렇게 준비를 한 후 목록에서 strstr 함수로 검색 대상 문자열이 있는지 없는지만 조사해보면 된다. 이 함수가 이런 식으로 문자열 점검을 하기 때문에 분석기들은 모든 문자열들을 반드시 공백으로 분리해야 한다. 심지어 첫 번째 단어와 마지막 단어도 앞 뒤로 공백을 각각 넣어야 할 필요가 있다. 만약 apple ~의 첫 공백을 없애고 apple~로 기록해놓으면 apple은 이 목록에 없는 것으로 취급되어 버린다.

이 함수는 결국 strstr 함수로 목록내의 문자열 존재 여부를 조사하며 이 함수가 제대로 목록을 검색할 수 있도록 하기 위해 필요한 철저한 준비를 하고 있다. 이 방법 외에 모든 문자열을 알파벳순으로 정렬한 상태로 문자열 배열을 만든 후 이분 검색을 하는 방법도 있는데 목록이 커질수록 이분 검색의 속도개선 효과가 뚜렷하게 나타날 것이다. 하지만 strstr 함수는 고도로 최적화되어 있으며 기계어 수준에서 문자열 검색을 하기 때문에 적당한 길이의 목록에서는 오히려 이분 검색보다 훨씬 더 빠르고 굳이 목록을 정렬할 필요가 없다는 장점이 있어 이 방법을 채택하였다.

숫자 점검

IsNumber 함수는 오프셋 s e사이의 문자열이 숫자인지 아닌지를 조사한다. 문자열을 구성하는 모든 문자가 0~9사이에 있다면 이 문자열은 숫자이다. , 아라비아 숫자뿐만 아니라 16진수 형식의 문자열도 있기 때문에 시작 부분이 0x 0X이면 알파벳 a~f, A~F도 숫자의 일부분으로 인정하고 있다. 실수인 경우와 부호가 있는 숫자의 경우 - 기호와 소수점은 구두점이기 때문에 숫자 부분과 기호 부분이 각각 따로 검사된다.

어떤 문자들이 숫자를 구성하는가는 문법에 따라 달라진다. C 언어는 16진수를 0x로 표현하지만 베이직은 &H로 표현하며 16진 표현을 쓰지 못하는 문법도 많이 있다. 그래서 이 함수는 여러 문법에 공통되기는 하지만 모든 문법에 절대적으로 적용되지는 않는다. 이 함수의 분석결과와 맞지 않는 분석기들은 이 함수를 재정의하여 사용한다.

스타일의 색상

스타일의 색상 정보를 가지는 arStyle 배열은 각 분석기의 생성자에서 초기화한다. CParse GetStyleColor 함수는 이 배열의 style 첨자에서 글자색과 배경색을 조사해 리턴하기만 한다. 분석기는 pInfo에 분석 정보를 작성하고 ApiEdit는 이 분석 정보에서 스타일값을 구한 후 GetStyleColor로부터 스타일의 색상값을 조사해 그 색상대로 출력함으로써 구문 분석결과가 비로소 사용자의 눈에 보이게 된다.

SetStyleColor는 스타일의 색상을 인수로 전달된 fore, back으로 변경한다. 분석기 스스로 스타일 색상을 정의하고 있지만 외부에서 이 함수를 호출함으로써 색상을 바꿀 수 있다. 호스트의 설정 변경(Customizing)을 지원하기 위한 함수이다. GetStyleName 함수도 마찬가지 목적으로 제공되며 이 두 함수는 분석기가 직접 사용하지는 않는다.

 

이상으로 CParse의 핵심 멤버함수들을 분석해보았다. 이 함수들의 주된 작업거리는 pInfo 배열을 관리하는 것이다. CParse의 파생 클래스인 실제 분석기들은 이 함수들을 호출하여 고유한 방법으로 문서를 분석하며 그 결과를 pInfo에 작성할 것이고 ApiEdit는 화면출력시 pInfo의 분석결과를 참조한다.