5차 B-1: XAML 태그·속성·값을 갈라 칠한다
실제 소스에 XAML 이 400개 42,477줄인데 여태 주석과 따옴표만 알아봐서 사실상 평문이었다. 마크업 전용 토크나이저를 따로 뒀다. 기존 토크나이저에 못 얹은 까닭은 규칙이 다르기 때문이다. 그쪽은 낱말을 읽어 예약어 표와 맞춰 보는데, 마크업은 같은 Content 가 꺾쇠 안에서는 속성 이름이고 밖에서는 그냥 글자다. 낱말이 무엇이냐가 아니라 어디에 있느냐로 갈린다. 줄 넘김 상태를 bool 하나에서 깃발 묶음(CarryOver)으로 넓혔다. XAML 은 여는 태그가 여러 줄에 걸치는 것이 보통이라 - 시험에 쓴 실제 파일은 첫 태그 하나가 14줄이다 - "지금 꺾쇠 안이다"를 안 넘기면 이어지는 줄의 속성이 그냥 글자로 보인다. 부르는 자리 다섯을 모두 바꿨다. 색은 새로 만들지 않고 이미 있는 갈래에 얹었다. 태그는 예약어 색, 속성 이름은 메서드 색, 값은 문자열 색, 앞가지는 타입 색이다. 태그 밖의 본문은 일부러 안 칠한다. 붙이는 중에 파일 비교 화면이 통째로 비어서 뜨는 일이 있었다. 의존 속성 타입을 bool 에서 CarryOver 로 바꾸면서 기본값 false 를 같이 안 고쳤다. 빌드도 통과하고 로그에도 안 남는다. 같은 주의를 코드 옆에 적어 뒀다. 실제 저장소 XAML 사본(638줄)으로 화면 확인. C# 색칠은 그대로. MarkupTest 65건. 전체 688건 통과(하네스 20벌). 경고 0 · 오류 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
238910634c
commit
26446d5e6b
@ -153,3 +153,99 @@ if (HasChanges && !threeWay && string.Equals(localText, _originalLocalText, Stri
|
|||||||
|
|
||||||
검사를 `GitOpTest` 36건으로 고쳤다. 전체 **623건 통과**(하네스 19벌) · 빌드 경고 0 · 오류 0.
|
검사를 `GitOpTest` 36건으로 고쳤다. 전체 **623건 통과**(하네스 19벌) · 빌드 경고 0 · 오류 0.
|
||||||
**실제 저장소는 건드리지 않았다** — 데모 저장소(`mergehub-demo`) 사본으로만 했다.
|
**실제 저장소는 건드리지 않았다** — 데모 저장소(`mergehub-demo`) 사본으로만 했다.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## B-1. XAML 색칠이 사실상 없다 (2026-09-11, 완료)
|
||||||
|
|
||||||
|
### 무엇이 문제였나
|
||||||
|
|
||||||
|
실제 소스에 XAML 이 400개 · 42,477줄이다. C# 다음으로 많다.
|
||||||
|
그런데 지금까지 XAML 은 `LanguageProfile.Markup` 으로 열렸고, 그것이 아는 것은 **주석과 따옴표뿐**이었다.
|
||||||
|
태그 이름 · 속성 이름 · 속성 값이 전부 같은 색이라 사실상 평문이었다.
|
||||||
|
|
||||||
|
### 왜 기존 토크나이저에 못 얹었나
|
||||||
|
|
||||||
|
`CodeTokenizer` 는 **낱말을 읽어 예약어 표와 맞춰 보는** 방식이다. 마크업은 규칙이 다르다.
|
||||||
|
같은 `Content` 가 <b>꺾쇠 안에서는 속성 이름이고 밖에서는 그냥 글자</b>다.
|
||||||
|
낱말이 무엇이냐가 아니라 **어디에 있느냐**로 갈린다. 그래서 마크업만 따로 뒀다(`MarkupTokenizer`).
|
||||||
|
|
||||||
|
### 줄을 넘어가는 상태를 넓혀야 했다
|
||||||
|
|
||||||
|
토크나이저는 한 줄씩 따로 훑는다. 앞 줄에서 넘어오는 것은 여태 `bool inBlockComment` 하나였다.
|
||||||
|
그런데 XAML 은 **여는 태그가 여러 줄에 걸치는 것이 예외가 아니라 보통**이다.
|
||||||
|
시험에 쓴 실제 파일(`UCManualRCCOperation.xaml`)은 **첫 태그 하나가 14줄**이다.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Button Grid.Row="1"
|
||||||
|
Content="비교"
|
||||||
|
Click="OnCompare" />
|
||||||
|
```
|
||||||
|
|
||||||
|
둘째·셋째 줄은 `<` 로 시작하지 않는다. "지금 꺾쇠 안이다" 를 넘겨주지 않으면
|
||||||
|
**속성 이름이 그냥 글자로 보인다** - XAML 은 이런 줄이 대부분이라 색칠이 없는 것과 같다.
|
||||||
|
|
||||||
|
그래서 `bool` 을 깃발 묶음(`CarryOver`)으로 넓혔다. `None` · `BlockComment` · `Tag` 셋이다.
|
||||||
|
부르는 자리 다섯(검사기 · 폴더 비교 · 화면 VM · 화면 컨트롤 · 행 모델)을 모두 바꿨다.
|
||||||
|
|
||||||
|
### 무엇을 무슨 색으로 보는가
|
||||||
|
|
||||||
|
| 무엇 | 색 | 까닭 |
|
||||||
|
|---|---|---|
|
||||||
|
| 태그 이름 | 예약어 (파랑) | 구조를 이루는 것이라 가장 먼저 눈에 들어와야 한다 |
|
||||||
|
| 속성 이름 | 메서드 (황토/연노랑) | 태그와 값 사이에서 따로 보여야 한다 |
|
||||||
|
| 속성 값 | 문자열 (빨강/주황) | 다른 언어의 문자열과 같은 색 - 같은 것이다 |
|
||||||
|
| 앞가지(`x:` · `local:`) | 타입 예약어 (청록) | XAML 에서 "어느 네임스페이스냐"는 정보다. 이름에 묻으면 `x:Name` 과 `Name` 이 같아 보인다 |
|
||||||
|
| `< > / :` | 구두점 (회색) | 뒤로 물러나야 한다 |
|
||||||
|
| `=` | 연산자 (회색) | 위와 같다 |
|
||||||
|
| `<?xml ?>` · `<!DOCTYPE>` · `<![CDATA[` | 지시문 (회색) | 파일마다 한두 줄이고 고칠 일이 없다. 안을 더 쪼개지 않는다 |
|
||||||
|
| 태그 밖의 본문 | **칠하지 않는다** | 물려받은 색 그대로 두어야 본문이 눈에 띈다 |
|
||||||
|
|
||||||
|
색을 새로 만들지 않고 **이미 있는 갈래에 얹었다.** 팔레트를 늘리면 밝은/어두운 두 벌을 다 손봐야 하고,
|
||||||
|
같은 화면에서 C# 과 XAML 이 서로 다른 색 체계를 쓰면 오히려 읽기 어렵다.
|
||||||
|
|
||||||
|
### 못 하는 것 (일부러)
|
||||||
|
|
||||||
|
속성 값이 줄을 넘어가면(닫는 따옴표가 다음 줄에 있으면) 그 줄까지만 문자열로 보고
|
||||||
|
다음 줄은 속성 자리로 돌아간다. XAML 에서 드물고, 틀려도 **색만 어긋난다.**
|
||||||
|
깃발을 하나 더 늘릴 만한 값이 아니라고 봤다.
|
||||||
|
|
||||||
|
### 하루를 날릴 뻔한 것 - 빈 화면
|
||||||
|
|
||||||
|
붙이고 나니 **파일 비교 화면이 통째로 비어서 떴다.** 단추 하나 없이 하얀 판이었다.
|
||||||
|
빌드는 경고 0 · 오류 0 이고 로그에도 아무것도 안 남았다.
|
||||||
|
|
||||||
|
까닭은 한 줄이었다. 의존 속성의 **기본값 타입이 속성 타입과 달랐다.**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
DependencyProperty.Register(nameof(InBlockComment), typeof(CarryOver), typeof(CodeTextBlock),
|
||||||
|
new FrameworkPropertyMetadata(false, OnVisualInputChanged)); // ← false 는 bool 이다
|
||||||
|
```
|
||||||
|
|
||||||
|
타입을 `bool` 에서 `CarryOver` 로 바꾸면서 **기본값을 같이 안 고쳤다.**
|
||||||
|
이러면 이 컨트롤을 처음 쓰는 순간 정적 초기화에서 터지고, 화면 전체가 안 뜬다.
|
||||||
|
빌드는 통과한다 - `object` 를 받는 자리라 컴파일러가 잡을 수 없다.
|
||||||
|
|
||||||
|
찾은 방법은 **고치기 전 상태로 되돌려 보는 것**이었다(`git stash`). 되돌리니 화면이 나왔고,
|
||||||
|
그래서 내 변경이 원인인 줄 알았다. 그 다음에 파일을 바이트로 들여다보다 `false` 를 발견했다.
|
||||||
|
같은 주의를 코드 옆에 적어 두었다.
|
||||||
|
|
||||||
|
### 확인한 것
|
||||||
|
|
||||||
|
**실제 저장소의 XAML 사본**(`UCManualRCCOperation.xaml` · 638줄)을 열어 눈으로 봤다.
|
||||||
|
|
||||||
|
| 확인 | 결과 |
|
||||||
|
|---|---|
|
||||||
|
| 14줄짜리 여는 태그의 **이어지는 줄** 속성(`d:DesignHeight` · `x:Name`) | 속성 색으로 나온다 |
|
||||||
|
| 태그 이름(`Setter` · `ControlTemplate` · `Border`) | 파랑 |
|
||||||
|
| 앞가지(`view:` · `x:` · `d:`) | 청록 |
|
||||||
|
| 속성 값(`"White"` · `"#1A22..."`) | 빨강 |
|
||||||
|
| 주석 줄(`<!--<Setter .../>-->`) | 초록 한 덩어리 |
|
||||||
|
| C# 파일 색칠 | 달라지지 않았다 (문서 주석 초록 · `public` 굵은 자홍 · 타입 청록) |
|
||||||
|
|
||||||
|
**검사를 새로 만들었다**(`MarkupTest` 65건). 여러 줄 태그 · 주석 이어짐 · 앞가지 · CDATA 와 함께,
|
||||||
|
**C# · C++ · JSON 색칠이 안 망가졌는지**도 같이 본다. 토큰이 겹치거나 거꾸로 가지 않는지도 본다.
|
||||||
|
|
||||||
|
전체 **688건 통과**(하네스 20벌) · 빌드 경고 0 · 오류 0.
|
||||||
|
**실제 저장소는 읽기만 했다** - 사본을 떠서 시험했다.
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
| 날짜 | 항목 | 커밋 |
|
| 날짜 | 항목 | 커밋 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-09-11 | **5차 B-1** — XAML 태그·속성·값을 갈라 칠함. 여러 줄에 걸친 여는 태그도 이어진다. 줄 넘김 상태를 깃발 묶음으로 넓혔다 | 아래 |
|
||||||
| 2026-09-11 | **5차 A-2** — Git 작업을 시작하는 자리에서 묻게 함. 가려서 묻는 것은 접고 전부 묻는다. 덤으로 4차 A-1 이 새던 자리 셋을 막음 | 아래 |
|
| 2026-09-11 | **5차 A-2** — Git 작업을 시작하는 자리에서 묻게 함. 가려서 묻는 것은 접고 전부 묻는다. 덤으로 4차 A-1 이 새던 자리 셋을 막음 | 아래 |
|
||||||
| 2026-09-11 | **5차 A-1** — 실제 저장소 2,091개로 인코딩을 확인(깨짐 0). 판별 코드가 두 군데 복사돼 있던 것을 한 곳으로 모으고, CP949 를 한 이름으로 부르게 함 | 아래 |
|
| 2026-09-11 | **5차 A-1** — 실제 저장소 2,091개로 인코딩을 확인(깨짐 0). 판별 코드가 두 군데 복사돼 있던 것을 한 곳으로 모으고, CP949 를 한 이름으로 부르게 함 | 아래 |
|
||||||
| 2026-09-10 | **4차 C-5** — 한 번 정하는 것들을 한자리에 모아 접고, 차이 이동을 개수 옆으로 | 아래 |
|
| 2026-09-10 | **4차 C-5** — 한 번 정하는 것들을 한자리에 모아 접고, 차이 이동을 개수 옆으로 | 아래 |
|
||||||
|
|||||||
@ -70,7 +70,7 @@
|
|||||||
|
|
||||||
| 상태 | 항목 |
|
| 상태 | 항목 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 대기 | B-1 XAML 색칠이 사실상 없다 (400개 · 42,477줄) |
|
| **완료** | B-1 XAML 색칠이 사실상 없다 (400개 · 42,477줄) |
|
||||||
| 대기 | B-2 폴더 비교가 실제 저장소에서 12초 걸린다 |
|
| 대기 | B-2 폴더 비교가 실제 저장소에서 12초 걸린다 |
|
||||||
|
|
||||||
### C. 실전 투입
|
### C. 실전 투입
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
@ -259,8 +259,8 @@ namespace MergeHub.Models
|
|||||||
private bool _isMergeNote;
|
private bool _isMergeNote;
|
||||||
|
|
||||||
/// <summary>앞 줄에서 블록 주석이 열린 채 넘어왔는지 (BASE / LOCAL 각각).</summary>
|
/// <summary>앞 줄에서 블록 주석이 열린 채 넘어왔는지 (BASE / LOCAL 각각).</summary>
|
||||||
[ObservableProperty] private bool _baseInComment;
|
[ObservableProperty] private Services.CarryOver _baseInComment;
|
||||||
[ObservableProperty] private bool _localInComment;
|
[ObservableProperty] private Services.CarryOver _localInComment;
|
||||||
|
|
||||||
/// <summary>BASE 쪽에 이 줄이 있는지. 정렬 결과라 바뀌지 않는다.</summary>
|
/// <summary>BASE 쪽에 이 줄이 있는지. 정렬 결과라 바뀌지 않는다.</summary>
|
||||||
public bool HasBase { get; }
|
public bool HasBase { get; }
|
||||||
@ -298,7 +298,7 @@ namespace MergeHub.Models
|
|||||||
/// <summary>REMOTE 쪽 한 줄 안에서 바뀐 구간.</summary>
|
/// <summary>REMOTE 쪽 한 줄 안에서 바뀐 구간.</summary>
|
||||||
[ObservableProperty] private IReadOnlyList<WordDiffSegment>? _remoteWords;
|
[ObservableProperty] private IReadOnlyList<WordDiffSegment>? _remoteWords;
|
||||||
|
|
||||||
[ObservableProperty] private bool _remoteInComment;
|
[ObservableProperty] private Services.CarryOver _remoteInComment;
|
||||||
|
|
||||||
// ---- 3-Way 에서의 LOCAL 원본 ----
|
// ---- 3-Way 에서의 LOCAL 원본 ----
|
||||||
//
|
//
|
||||||
|
|||||||
34
src/MergeHub/Services/CarryOver.cs
Normal file
34
src/MergeHub/Services/CarryOver.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MergeHub.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 한 줄을 칠하고 난 뒤 <b>다음 줄로 넘겨야 하는 상태.</b>
|
||||||
|
///
|
||||||
|
/// 토크나이저는 한 줄씩 따로 훑는다. 그런데 어떤 것은 줄을 넘어간다 -
|
||||||
|
/// <c>/* ... */</c> 는 여러 줄에 걸치고, XAML 의 여는 태그도 흔히 여러 줄에 걸친다.
|
||||||
|
///
|
||||||
|
/// <code>
|
||||||
|
/// <Button Grid.Row="1"
|
||||||
|
/// Content="비교"
|
||||||
|
/// Click="OnCompare" />
|
||||||
|
/// </code>
|
||||||
|
///
|
||||||
|
/// 둘째·셋째 줄은 <c><</c> 로 시작하지 않는다. "지금 꺾쇠 안이다" 를 넘겨주지 않으면
|
||||||
|
/// <b>속성 이름이 그냥 글자로 보인다</b> - XAML 은 이런 줄이 대부분이라 색칠이 사실상 없는 것과 같다.
|
||||||
|
///
|
||||||
|
/// 예전에는 <c>bool inBlockComment</c> 하나였다. 넘길 것이 하나 더 생겨서 깃발 묶음으로 넓혔다.
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum CarryOver
|
||||||
|
{
|
||||||
|
/// <summary>넘길 것이 없다. 줄이 스스로 완결됐다.</summary>
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
/// <summary><c>/* ... */</c> 또는 <c><!-- ... --></c> 가 열린 채로 줄이 끝났다.</summary>
|
||||||
|
BlockComment = 1,
|
||||||
|
|
||||||
|
/// <summary>여는 태그의 꺾쇠가 아직 닫히지 않았다. 마크업에서만 쓴다.</summary>
|
||||||
|
Tag = 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using MergeHub.Views.Controls;
|
using MergeHub.Views.Controls;
|
||||||
@ -54,13 +54,13 @@ namespace MergeHub.Services
|
|||||||
{
|
{
|
||||||
var problems = new List<CodeProblem>();
|
var problems = new List<CodeProblem>();
|
||||||
|
|
||||||
var inBlockComment = false;
|
var carry = CarryOver.None;
|
||||||
int round = 0, curly = 0, square = 0;
|
int round = 0, curly = 0, square = 0;
|
||||||
|
|
||||||
for (var i = 0; i < lines.Count; i++)
|
for (var i = 0; i < lines.Count; i++)
|
||||||
{
|
{
|
||||||
var line = lines[i];
|
var line = lines[i];
|
||||||
var tokens = CodeTokenizer.Tokenize(line, profile, inBlockComment, out inBlockComment);
|
var tokens = CodeTokenizer.Tokenize(line, profile, carry, out carry);
|
||||||
|
|
||||||
// 각 글자가 어떤 종류 안에 있는지 표시해 둔다(토큰이 안 잡힌 자리는 코드로 본다).
|
// 각 글자가 어떤 종류 안에 있는지 표시해 둔다(토큰이 안 잡힌 자리는 코드로 본다).
|
||||||
var free = new bool[line.Length];
|
var free = new bool[line.Length];
|
||||||
@ -90,7 +90,7 @@ namespace MergeHub.Services
|
|||||||
CountBrackets(line, free, tokens, ref round, ref curly, ref square);
|
CountBrackets(line, free, tokens, ref round, ref curly, ref square);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inBlockComment)
|
if ((carry & CarryOver.BlockComment) != 0)
|
||||||
problems.Add(new CodeProblem(Math.Max(0, lines.Count - 1), "블록 주석 /* 가 닫히지 않았습니다."));
|
problems.Add(new CodeProblem(Math.Max(0, lines.Count - 1), "블록 주석 /* 가 닫히지 않았습니다."));
|
||||||
|
|
||||||
AddBalanceProblem(problems, lines.Count, round, '(', ')');
|
AddBalanceProblem(problems, lines.Count, round, '(', ')');
|
||||||
|
|||||||
@ -9,6 +9,8 @@ namespace MergeHub.Services
|
|||||||
///
|
///
|
||||||
/// C# 은 전용 토크나이저로 넘긴다 - 축자·보간 문자열과 접근제한자 구분이 표로 담기지 않고,
|
/// C# 은 전용 토크나이저로 넘긴다 - 축자·보간 문자열과 접근제한자 구분이 표로 담기지 않고,
|
||||||
/// 이미 테스트로 굳어 있어 굳이 다시 만들 이유가 없다.
|
/// 이미 테스트로 굳어 있어 굳이 다시 만들 이유가 없다.
|
||||||
|
/// 마크업(XML · XAML · HTML)도 따로 보낸다 - <b>꺾쇠 안이냐 밖이냐</b>로 갈리는 규칙이라
|
||||||
|
/// 여기의 단어 단위 훑기에 얹을 수가 없다. <see cref="MarkupTokenizer"/> 를 보라.
|
||||||
/// 평문·Markdown 처럼 코드가 아닌 형식은 토큰을 만들지 않는다(그냥 검은 글씨로 나온다).
|
/// 평문·Markdown 처럼 코드가 아닌 형식은 토큰을 만들지 않는다(그냥 검은 글씨로 나온다).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class CodeTokenizer
|
public static class CodeTokenizer
|
||||||
@ -19,12 +21,22 @@ namespace MergeHub.Services
|
|||||||
private static readonly IReadOnlyList<CodeToken> Empty = Array.Empty<CodeToken>();
|
private static readonly IReadOnlyList<CodeToken> Empty = Array.Empty<CodeToken>();
|
||||||
|
|
||||||
public static IReadOnlyList<CodeToken> Tokenize(string line, LanguageProfile profile,
|
public static IReadOnlyList<CodeToken> Tokenize(string line, LanguageProfile profile,
|
||||||
bool inBlockComment, out bool stillInBlockComment)
|
CarryOver carry, out CarryOver next)
|
||||||
{
|
{
|
||||||
if (profile.UseCSharpTokenizer)
|
if (profile.UseCSharpTokenizer)
|
||||||
return CSharpTokenizer.Tokenize(line, inBlockComment, out stillInBlockComment);
|
{
|
||||||
|
var wasInComment = (carry & CarryOver.BlockComment) != 0;
|
||||||
|
var result = CSharpTokenizer.Tokenize(line, wasInComment, out var stillInComment);
|
||||||
|
next = stillInComment ? CarryOver.BlockComment : CarryOver.None;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
stillInBlockComment = inBlockComment;
|
if (profile.UseMarkupTokenizer)
|
||||||
|
return MarkupTokenizer.Tokenize(line, profile, carry, out next);
|
||||||
|
|
||||||
|
// 아래로는 꺾쇠 상태를 쓰지 않는다. 블록 주석만 넘긴다.
|
||||||
|
var inBlockComment = (carry & CarryOver.BlockComment) != 0;
|
||||||
|
next = carry & CarryOver.BlockComment;
|
||||||
|
|
||||||
if (!profile.IsCode || string.IsNullOrEmpty(line)) return Empty;
|
if (!profile.IsCode || string.IsNullOrEmpty(line)) return Empty;
|
||||||
|
|
||||||
@ -43,7 +55,7 @@ namespace MergeHub.Services
|
|||||||
|
|
||||||
i = close + profile.BlockEnd!.Length;
|
i = close + profile.BlockEnd!.Length;
|
||||||
tokens.Add(new CodeToken(0, i, CodeTokenKind.Comment));
|
tokens.Add(new CodeToken(0, i, CodeTokenKind.Comment));
|
||||||
stillInBlockComment = false;
|
next = CarryOver.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 전처리 지시문 ----
|
// ---- 전처리 지시문 ----
|
||||||
@ -83,7 +95,7 @@ namespace MergeHub.Services
|
|||||||
if (end < 0)
|
if (end < 0)
|
||||||
{
|
{
|
||||||
tokens.Add(new CodeToken(start, line.Length - start, CodeTokenKind.Comment));
|
tokens.Add(new CodeToken(start, line.Length - start, CodeTokenKind.Comment));
|
||||||
stillInBlockComment = true;
|
next = CarryOver.BlockComment;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -345,12 +345,12 @@ namespace MergeHub.Services
|
|||||||
{
|
{
|
||||||
var text = TextFile.Read(path);
|
var text = TextFile.Read(path);
|
||||||
var builder = new System.Text.StringBuilder(text.Length);
|
var builder = new System.Text.StringBuilder(text.Length);
|
||||||
var inBlockComment = false;
|
var carry = CarryOver.None;
|
||||||
|
|
||||||
foreach (var line in text.Split('\n'))
|
foreach (var line in text.Split('\n'))
|
||||||
{
|
{
|
||||||
var body = line.TrimEnd('\r');
|
var body = line.TrimEnd('\r');
|
||||||
var tokens = CodeTokenizer.Tokenize(body, profile, inBlockComment, out inBlockComment);
|
var tokens = CodeTokenizer.Tokenize(body, profile, carry, out carry);
|
||||||
|
|
||||||
var isComment = new bool[body.Length];
|
var isComment = new bool[body.Length];
|
||||||
foreach (var token in tokens)
|
foreach (var token in tokens)
|
||||||
|
|||||||
@ -30,6 +30,7 @@ namespace MergeHub.Services
|
|||||||
bool isCode = true,
|
bool isCode = true,
|
||||||
bool hasBraces = false,
|
bool hasBraces = false,
|
||||||
bool useCSharpTokenizer = false,
|
bool useCSharpTokenizer = false,
|
||||||
|
bool useMarkupTokenizer = false,
|
||||||
bool hasPreprocessor = false)
|
bool hasPreprocessor = false)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
@ -42,6 +43,7 @@ namespace MergeHub.Services
|
|||||||
IsCode = isCode;
|
IsCode = isCode;
|
||||||
HasBraces = hasBraces;
|
HasBraces = hasBraces;
|
||||||
UseCSharpTokenizer = useCSharpTokenizer;
|
UseCSharpTokenizer = useCSharpTokenizer;
|
||||||
|
UseMarkupTokenizer = useMarkupTokenizer;
|
||||||
HasPreprocessor = hasPreprocessor;
|
HasPreprocessor = hasPreprocessor;
|
||||||
|
|
||||||
var comparer = caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase;
|
var comparer = caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase;
|
||||||
@ -73,6 +75,12 @@ namespace MergeHub.Services
|
|||||||
/// <summary>C# 전용 토크나이저를 쓸지.</summary>
|
/// <summary>C# 전용 토크나이저를 쓸지.</summary>
|
||||||
public bool UseCSharpTokenizer { get; }
|
public bool UseCSharpTokenizer { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 마크업 전용 토크나이저를 쓸지. 마크업은 단어가 아니라 <b>꺾쇠 안이냐 밖이냐</b>로 갈려서
|
||||||
|
/// 예약어 표로는 담기지 않는다. 까닭은 <see cref="MarkupTokenizer"/> 에 적었다.
|
||||||
|
/// </summary>
|
||||||
|
public bool UseMarkupTokenizer { get; }
|
||||||
|
|
||||||
public HashSet<string> Keywords { get; }
|
public HashSet<string> Keywords { get; }
|
||||||
public HashSet<string> ControlKeywords { get; }
|
public HashSet<string> ControlKeywords { get; }
|
||||||
public HashSet<string> TypeKeywords { get; }
|
public HashSet<string> TypeKeywords { get; }
|
||||||
@ -187,10 +195,10 @@ namespace MergeHub.Services
|
|||||||
caseSensitive: false);
|
caseSensitive: false);
|
||||||
|
|
||||||
public static readonly LanguageProfile Markup = new LanguageProfile(
|
public static readonly LanguageProfile Markup = new LanguageProfile(
|
||||||
"XML / HTML", new[] { ".xml", ".xaml", ".html", ".htm", ".svg", ".xsd", ".xsl", ".csproj", ".vbproj", ".config", ".props", ".targets", ".resx" },
|
"XML / XAML / HTML", new[] { ".xml", ".xaml", ".html", ".htm", ".svg", ".xsd", ".xsl", ".csproj", ".vbproj", ".config", ".props", ".targets", ".resx" },
|
||||||
blockStart: "<!--", blockEnd: "-->",
|
blockStart: "<!--", blockEnd: "-->",
|
||||||
quotes: new[] { '"', '\'' },
|
quotes: new[] { '"', '\'' },
|
||||||
caseSensitive: false);
|
caseSensitive: false, useMarkupTokenizer: true);
|
||||||
|
|
||||||
public static readonly LanguageProfile Json = new LanguageProfile(
|
public static readonly LanguageProfile Json = new LanguageProfile(
|
||||||
"JSON", new[] { ".json", ".jsonc" },
|
"JSON", new[] { ".json", ".jsonc" },
|
||||||
|
|||||||
198
src/MergeHub/Services/MarkupTokenizer.cs
Normal file
198
src/MergeHub/Services/MarkupTokenizer.cs
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using MergeHub.Views.Controls;
|
||||||
|
|
||||||
|
namespace MergeHub.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// XML · XAML · HTML 한 줄을 토큰으로 쪼갠다.
|
||||||
|
///
|
||||||
|
/// 마크업은 <b>단어가 무엇이냐가 아니라 꺾쇠 안이냐 밖이냐</b>로 갈린다.
|
||||||
|
/// 같은 <c>Content</c> 가 꺾쇠 안에서는 속성 이름이고 밖에서는 그냥 글자다.
|
||||||
|
/// 예약어 목록으로 갈리는 다른 언어와 규칙이 아예 달라서 <see cref="CodeTokenizer"/> 의
|
||||||
|
/// 단어 단위 훑기에 얹을 수가 없었다. 그래서 마크업만 따로 둔다.
|
||||||
|
///
|
||||||
|
/// 무엇을 무슨 색으로 보는지:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>태그 이름 → 예약어 색 (파랑)</item>
|
||||||
|
/// <item>속성 이름 → 메서드 색 (황토/연노랑)</item>
|
||||||
|
/// <item>속성 값 → 문자열 색 (빨강/주황)</item>
|
||||||
|
/// <item>앞가지(<c>x:</c> · <c>local:</c>) → 타입 예약어 색 (청록)</item>
|
||||||
|
/// <item><c>< > / :</c> → 구두점 색, <c>=</c> → 연산자 색</item>
|
||||||
|
/// <item><c><?xml ?></c> · <c><!DOCTYPE></c> · <c><![CDATA[</c> → 지시문 색 (회색)</item>
|
||||||
|
/// </list>
|
||||||
|
///
|
||||||
|
/// <b>태그 밖의 글은 칠하지 않는다.</b> 물려받은 색 그대로 둔다 - 그래야 본문이 눈에 띈다.
|
||||||
|
///
|
||||||
|
/// 못 하는 것: 속성 값이 줄을 넘어가면(닫는 따옴표가 다음 줄에 있으면) 그 줄까지만 문자열로 보고
|
||||||
|
/// 다음 줄은 속성 자리로 돌아간다. XAML 에서 드물고, 틀려도 색만 어긋난다.
|
||||||
|
/// </summary>
|
||||||
|
public static class MarkupTokenizer
|
||||||
|
{
|
||||||
|
private static readonly IReadOnlyList<CodeToken> Empty = Array.Empty<CodeToken>();
|
||||||
|
|
||||||
|
public static IReadOnlyList<CodeToken> Tokenize(string line, LanguageProfile profile,
|
||||||
|
CarryOver carry, out CarryOver next)
|
||||||
|
{
|
||||||
|
next = carry;
|
||||||
|
if (string.IsNullOrEmpty(line)) return Empty;
|
||||||
|
|
||||||
|
var commentStart = profile.BlockStart ?? "<!--";
|
||||||
|
var commentEnd = profile.BlockEnd ?? "-->";
|
||||||
|
|
||||||
|
var tokens = new List<CodeToken>();
|
||||||
|
var inTag = (carry & CarryOver.Tag) != 0;
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
// 앞 줄에서 넘어온 주석을 먼저 닫는다. 닫히지 않으면 이 줄도 통째로 주석이다.
|
||||||
|
if ((carry & CarryOver.BlockComment) != 0)
|
||||||
|
{
|
||||||
|
var close = line.IndexOf(commentEnd, StringComparison.Ordinal);
|
||||||
|
if (close < 0)
|
||||||
|
{
|
||||||
|
tokens.Add(new CodeToken(0, line.Length, CodeTokenKind.Comment));
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
i = close + commentEnd.Length;
|
||||||
|
tokens.Add(new CodeToken(0, i, CodeTokenKind.Comment));
|
||||||
|
next &= ~CarryOver.BlockComment;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (i < line.Length)
|
||||||
|
{
|
||||||
|
var c = line[i];
|
||||||
|
|
||||||
|
if (char.IsWhiteSpace(c)) { i++; continue; }
|
||||||
|
|
||||||
|
if (!inTag)
|
||||||
|
{
|
||||||
|
// 태그 밖의 글. 다음 꺾쇠까지 건너뛴다 - 칠하지 않는다.
|
||||||
|
if (c != '<')
|
||||||
|
{
|
||||||
|
var lt = line.IndexOf('<', i);
|
||||||
|
i = lt < 0 ? line.Length : lt;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <!-- 주석 -->
|
||||||
|
if (Matches(line, i, commentStart))
|
||||||
|
{
|
||||||
|
var close = line.IndexOf(commentEnd, i + commentStart.Length, StringComparison.Ordinal);
|
||||||
|
if (close < 0)
|
||||||
|
{
|
||||||
|
tokens.Add(new CodeToken(i, line.Length - i, CodeTokenKind.Comment));
|
||||||
|
next |= CarryOver.BlockComment;
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
var closeEnd = close + commentEnd.Length;
|
||||||
|
tokens.Add(new CodeToken(i, closeEnd - i, CodeTokenKind.Comment));
|
||||||
|
i = closeEnd;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <?xml ... ?> · <!DOCTYPE ...> · <![CDATA[ ... ]]>
|
||||||
|
// 안을 더 쪼개지 않는다. 파일마다 한두 줄이고, 고칠 일도 거의 없다.
|
||||||
|
if (Matches(line, i, "<?") || Matches(line, i, "<!"))
|
||||||
|
{
|
||||||
|
var gt = line.IndexOf('>', i);
|
||||||
|
var stop = gt < 0 ? line.Length : gt + 1;
|
||||||
|
tokens.Add(new CodeToken(i, stop - i, CodeTokenKind.Preprocessor));
|
||||||
|
i = stop;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <이름 · </이름
|
||||||
|
var open = i;
|
||||||
|
i++;
|
||||||
|
if (i < line.Length && line[i] == '/') i++;
|
||||||
|
tokens.Add(new CodeToken(open, i - open, CodeTokenKind.Punctuation));
|
||||||
|
|
||||||
|
i = ReadName(line, i, tokens, CodeTokenKind.Keyword);
|
||||||
|
inTag = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 꺾쇠 안 ----
|
||||||
|
|
||||||
|
if (c == '>' || (c == '/' && i + 1 < line.Length && line[i + 1] == '>'))
|
||||||
|
{
|
||||||
|
var length = c == '>' ? 1 : 2;
|
||||||
|
tokens.Add(new CodeToken(i, length, CodeTokenKind.Punctuation));
|
||||||
|
i += length;
|
||||||
|
inTag = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '=')
|
||||||
|
{
|
||||||
|
tokens.Add(new CodeToken(i, 1, CodeTokenKind.Operator));
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '"' || c == '\'')
|
||||||
|
{
|
||||||
|
var start = i;
|
||||||
|
i++;
|
||||||
|
while (i < line.Length && line[i] != c) i++;
|
||||||
|
if (i < line.Length) i++; // 닫는 따옴표까지 포함한다
|
||||||
|
tokens.Add(new CodeToken(start, i - start, CodeTokenKind.Text));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsNameStart(c))
|
||||||
|
{
|
||||||
|
i = ReadName(line, i, tokens, CodeTokenKind.Method);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inTag) next |= CarryOver.Tag;
|
||||||
|
else next &= ~CarryOver.Tag;
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이름을 읽는다. <c>x:Name</c> · <c>local:MyControl</c> 처럼 <b>앞가지가 붙은 이름</b>은
|
||||||
|
/// 앞가지를 따로 칠한다 - XAML 에서 앞가지는 "어느 네임스페이스의 것이냐"를 말해 주는데,
|
||||||
|
/// 이름에 묻어 두면 <c>x:Name</c> 과 <c>Name</c> 이 같아 보인다.
|
||||||
|
/// </summary>
|
||||||
|
private static int ReadName(string line, int index, List<CodeToken> tokens, CodeTokenKind kind)
|
||||||
|
{
|
||||||
|
var start = index;
|
||||||
|
var i = index;
|
||||||
|
while (i < line.Length && IsNameChar(line[i])) i++;
|
||||||
|
if (i == start) return index;
|
||||||
|
|
||||||
|
if (i < line.Length && line[i] == ':')
|
||||||
|
{
|
||||||
|
tokens.Add(new CodeToken(start, i - start, CodeTokenKind.TypeKeyword));
|
||||||
|
tokens.Add(new CodeToken(i, 1, CodeTokenKind.Punctuation));
|
||||||
|
i++;
|
||||||
|
|
||||||
|
var nameStart = i;
|
||||||
|
while (i < line.Length && IsNameChar(line[i])) i++;
|
||||||
|
if (i > nameStart) tokens.Add(new CodeToken(nameStart, i - nameStart, kind));
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.Add(new CodeToken(start, i - start, kind));
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNameStart(char c) => char.IsLetter(c) || c == '_';
|
||||||
|
|
||||||
|
/// <summary><c>Grid.Row</c> 의 점과 <c>data-id</c> 의 붙임표도 이름의 일부다.</summary>
|
||||||
|
private static bool IsNameChar(char c) =>
|
||||||
|
char.IsLetterOrDigit(c) || c == '_' || c == '-' || c == '.';
|
||||||
|
|
||||||
|
private static bool Matches(string line, int index, string token) =>
|
||||||
|
index + token.Length <= line.Length &&
|
||||||
|
string.Compare(line, index, token, 0, token.Length, StringComparison.Ordinal) == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1068,14 +1068,15 @@ namespace MergeHub.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 여러 줄에 걸친 /* ... */ 를 이어서 칠하려면 각 줄이 "앞에서 주석이 열린 채 왔는지"를 알아야 한다.
|
/// 줄을 넘어가는 것들(<c>/* ... */</c> · XAML 의 여러 줄짜리 여는 태그)을 이어서 칠하려면
|
||||||
|
/// 각 줄이 "앞에서 무엇이 열린 채 왔는지"를 알아야 한다.
|
||||||
/// 위에서 아래로 한 번 훑으며 BASE/LOCAL 각각의 상태를 기록한다.
|
/// 위에서 아래로 한 번 훑으며 BASE/LOCAL 각각의 상태를 기록한다.
|
||||||
/// LOCAL 을 직접 고치면 그 아래 줄들의 상태가 달라지므로 편집 후에도 다시 계산한다.
|
/// LOCAL 을 직접 고치면 그 아래 줄들의 상태가 달라지므로 편집 후에도 다시 계산한다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void RefreshBlockCommentState(IEnumerable<MergeLine> rows)
|
private void RefreshBlockCommentState(IEnumerable<MergeLine> rows)
|
||||||
{
|
{
|
||||||
var inBase = false;
|
var inBase = CarryOver.None;
|
||||||
var inLocal = false;
|
var inLocal = CarryOver.None;
|
||||||
|
|
||||||
foreach (var row in rows)
|
foreach (var row in rows)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
@ -57,13 +57,20 @@ namespace MergeHub.Views.Controls
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static readonly DependencyProperty InBlockCommentProperty = DependencyProperty.Register(
|
public static readonly DependencyProperty InBlockCommentProperty = DependencyProperty.Register(
|
||||||
nameof(InBlockComment), typeof(bool), typeof(CodeTextBlock),
|
nameof(InBlockComment), typeof(Services.CarryOver), typeof(CodeTextBlock),
|
||||||
new FrameworkPropertyMetadata(false, OnVisualInputChanged));
|
new FrameworkPropertyMetadata(Services.CarryOver.None, OnVisualInputChanged));
|
||||||
|
|
||||||
/// <summary>앞 줄에서 /* 가 열린 채 넘어왔는지. 여러 줄 블록 주석을 이어서 칠하기 위한 값.</summary>
|
/// <summary>
|
||||||
public bool InBlockComment
|
/// 앞 줄에서 무엇이 열린 채 넘어왔는지. 여러 줄 블록 주석과
|
||||||
|
/// 여러 줄에 걸친 XAML 여는 태그를 이어서 칠하기 위한 값이다.
|
||||||
|
///
|
||||||
|
/// <b>기본값의 타입이 이 속성의 타입과 정확히 같아야 한다.</b> 여기에 <c>false</c> 를 두면
|
||||||
|
/// 이 클래스를 처음 쓰는 순간 터지고, 그러면 <b>파일 비교 화면이 통째로 빈 채로 뜬다</b> -
|
||||||
|
/// 빌드는 멀쩡하고 로그에도 안 남아서 어디가 잘못됐는지 알 길이 없다. 한 번 겪었다.
|
||||||
|
/// </summary>
|
||||||
|
public Services.CarryOver InBlockComment
|
||||||
{
|
{
|
||||||
get => (bool)GetValue(InBlockCommentProperty);
|
get => (Services.CarryOver)GetValue(InBlockCommentProperty);
|
||||||
set => SetValue(InBlockCommentProperty, value);
|
set => SetValue(InBlockCommentProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user