본문 바로가기

C#/근웹 연대기

c#으로 근본 없는 웹서버 개발기 22 : cshtml 파싱 - 공통클래스

⚠WARNING
ASP.NET에 대한 포스팅이 아닙니다
나가실 문은 오른쪽 하단입니다

 

1. 군소리

1-1 영역 구별

이전 글에서 예고했던 파싱 하는 부분을 한방에 만들려고 했지만 분량이 생각보다 많다.

단순히 구분을 지어도 C#, HTML, CSS, JS 그리고 RAZOR 문법이 혼재하고 있다.

그래서 개개의 문법이 적용되는 구간을 정확하게 파악해야 하는데,

각각의 언어?마다 구간의 시작과 끝을 맺는 문자열이 다르다. 

여기서 또,

문자열 상수와 문자열 보간 구간은 razor구문이 먹히지 않으니 따로 처리를 해줘야 한다.

까다로운점은 문자열 안에 또 다른 문자열이 있는 경우가 생길 수 있다는 점이다

예를 들면.

C# :  $"ab\"cd{$"abcd{"abcd"}"}" 처럼

전지적 시점에서 보면 하나의 문자열 상수일 수도 있지만 단순히 따옴표의 개수로 구간을 정할 수 없다. (따옴표가 홀수)

그래서 구간이 끝나는 부분을 알아내기 위해서는 결국에는 문자열 보간에 대한 처리까지 꼼꼼하게 해줘야 한다.

 

P.S. CSS는 어쨰서  //, 라인 코맨트가 안될까.. 

P.S.2. vs2022은(는) 어째서 CSS영역에서 코맨트 단축키를 누르면 라인코맨트가 적용되어 경고메세지를 만드는가..

1-2 이상한 문법규칙

다음의 예시를 보자.

@section sec1{
    <div>}</div>
}

div태그 내에 있는 }는 어떤 블록 구문의 닫힘의 역할을 하는 것이 아니라 순수하게 문자 하나로 생각되는게 인지상정인데, 이상하게도 razor에서는 섹션 영역의 닫힘 문자로 간주하고 있다. 

그래서 이러한 의문점을 가지고 다음의 코드를 실행해보면

@{
    <div>}</div>
}

일관성이 없도록? 이 코드는 정상적인 느낌으로 작동한다. 🙃(혼돈의 CHAOS)

그렇다

razor 에서는 섹션 영역을 이용하려면 중괄호의 짝 맞춤에 주의해야 한다.

그렇다

.. 사실 저런 사소한 부분은 별것 아닐 수 있다.

razor를 사용하는 입장에서는 저런 부분을 알랑가 모르겠지만, 그것을 만들어보는 입장에서는 여간.. 신경 쓰인다.

 

2 공통클래스

추상 클래스로서 각각의 문법 규칙 구간마다 존재하는 노드들의 부모클래스이다.

각각의 노드들에 대해서는 다음 포스팅에서 소개할 예정이다.

파싱하는 부분에 대한 코드가 절반이상 완료가 되어 다음의 코드는 차후에 큰 변화가 없을 예정이다.

namespace dotweb;

enum NTYPE { CLIENT, CSHARP, EXPRESS, STRING, LAYOUT, FUNCTION, SECTION, USING }
abstract partial class ParseNode
{
    public ParseNode parent; //부모 노드
    public List<object> child; //문자 또는 자식노드가 올수 있음.
    private List<string> tokens; //토큰나이징된 문자열
    private List<string> pre; //이전 토근 값
    protected int i;  //현재 인덱스
    protected int pi; //과거 인덱스
    protected string tk; //현재 토큰 값
    protected static TTYPE GetType(string str) => TemplateEngine.GetTokenType(str);
    public abstract NTYPE Type { get; }

    public ParseNode(ParseNode parent) : this(parent.tokens , parent)
    { //자식노드를 위한 생성자
        this.parent = parent ?? throw new ArgumentNullException(nameof(parent));
       
    }
    public ParseNode(List<string> tokens, ParseNode parent=null)
    {  //루트노드를 위한 생성자
        this.parent = parent;
        Init(tokens);
        Load();
    }
    protected abstract void Load(); //구현해야할 메인 로직

    protected void Init(List<string> tokens)
    { //초기화
        this.tokens = tokens ?? throw new ArgumentNullException(nameof(tokens));
        if(GetType(tokens.LastOrDefault()) != TTYPE.WHITE_SPACE) tokens.Add("\r\n");
        this.child = new List<object>();
        this.pre = new List<string>();
        this.i = parent?.i ?? -1;
        this.pi = parent?.pi ?? 0;
        this.tk = parent?.tk;
       
    }

    protected string Next(bool noPre = false)
    {  //공백을 스킵하고 다음 토큰 반환
        this.i++;
        string token = SkipSpace();
        if (noPre == false) this.pre.Insert(0, this.tk);
        this.tk = token;
        return token;
    }

    protected string NextRaw(bool noPre = true)
    {   //다음 토큰 반환
        this.i++;
        var token = (tokens.Count <= i) ? null : tokens[i];
        if (noPre == false) this.pre.Insert(0, this.tk);
        this.tk = token;
        return token;
    }
    protected string SkipSpace()
    { //공백 문자 스킵
        for (; i < tokens.Count; i++)
        {
            if (GetType(tokens[i]) != TTYPE.WHITE_SPACE) return tokens[i];
        }
        return null;
    }

    protected string Offset(int x)
    {  //상대적 위치 단일 문자열
        if(x+i < 0 || x+i >= tokens.Count) return null;
        return tokens[x+i];
    }

    protected string Offset(int x, int y)
    { // 상대적 위치 범위 문자열
        if (x > y) return null;
        if (x + i < 0 || x + i >= tokens.Count) return null;
        if (y + i < 0 || y + i >= tokens.Count) return null;
        return String.Join(null, tokens.GetRange(x + i, y - x + 1));
    }

    protected string OffsetS(int x)
    { //공백을 제외한 상대적 위치
        if(x==0) return Offset(0);
        int pos = i;
        int step = x>0?1:-1;
        while(x != 0)
        {
            pos += step;
            if (pos < 0 || tokens.Count <= pos) return null;
            if (!String.IsNullOrWhiteSpace(tokens[pos])) x -= step;
        }
        return tokens[pos];
    }

    protected string AddStr() => AddStr(pi, i);

    protected string AddStr(int s_idx , int e_idx)
    {  //문자열을 자식 노드로 저장
        pi = e_idx;
        if (e_idx > tokens.Count) e_idx = tokens.Count;
        if (e_idx <= s_idx ) return null;
        var str = String.Join(null, tokens.GetRange(s_idx, e_idx - s_idx));
        if (str.Length > 0) this.child.Add(str);

        return str;
    }

    protected void Invoke(Type type)
    {  //자식 노드로 분기
        var childNode = Activator.CreateInstance(type,this) as ParseNode;
        this.child.Add(childNode);
        (i, pi) = (childNode.i, childNode.pi);
        if (Offset(0) == null) Err();
    }

    protected void Err(String msg = null)
    {  //에러메세지.. 대충.. 뜨로우..
        int stE = this.i - 10;
        if (stE < 0) stE = 0;
        int edE = this.i + 10;
        if (edE >= tokens.Count) edE = tokens.Count - 1;
        throw new ArgumentException("Invaild syntax: "+ msg + "\n"
            + String.Join("", tokens.GetRange(stE, edE-stE)));
    }
}

파티얼 클래스이므로 나머지도 다음 포스팅에서...