mirror of
https://github.com/waaraawa/ByteGrid.git
synced 2026-07-22 06:41:47 +00:00
feat(core): add legend position and footer control options
- Add legendPosition option (right, left, bottom, none) to control legend placement - Add showFooter option to show/hide footer info - Support both config (YAML) and render options for flexibility - Fix legend overlapping issue when positioned at bottom - Maintain backward compatibility with showLegend option
This commit is contained in:
parent
43ae040047
commit
4d5cd4e289
14 changed files with 1387 additions and 119 deletions
|
|
@ -1,92 +1,122 @@
|
|||
# ByteGrid Examples
|
||||
|
||||
이 폴더에는 ByteGrid 플러그인에서 사용할 수 있는 예시 파일들이 있습니다.
|
||||
This folder contains example files for the ByteGrid plugin.
|
||||
|
||||
## 사용 방법
|
||||
## How to Use
|
||||
|
||||
1. 원하는 예시 파일을 엽니다
|
||||
2. 내용을 **모두 선택**하여 복사합니다
|
||||
3. Obsidian 노트에서 코드블록을 만듭니다:
|
||||
1. Open the example file you want to use
|
||||
2. **Select all** content and copy it
|
||||
3. Create a code block in your Obsidian note:
|
||||
```
|
||||
```bytegrid
|
||||
[여기에 붙여넣기]
|
||||
[paste here]
|
||||
```
|
||||
```
|
||||
|
||||
## 예시 파일 목록
|
||||
## Example Files
|
||||
|
||||
### 기본 예시
|
||||
### Basic Examples
|
||||
|
||||
#### 1. simple-struct.yaml
|
||||
- 간단한 16바이트 구조체
|
||||
- 4개 필드 (ID, Timestamp, Value, Checksum)
|
||||
- 8바이트/행 레이아웃
|
||||
- Simple 16-byte structure
|
||||
- 4 fields (ID, Timestamp, Value, Checksum)
|
||||
- 8 bytes/row layout
|
||||
|
||||
#### 2. wav-header.yaml
|
||||
- WAV 오디오 파일 헤더 (44바이트)
|
||||
- 14개 필드 (RIFF, WAVE, fmt, data 청크)
|
||||
- 16바이트/행 레이아웃
|
||||
- 오디오 형식, 샘플레이트 등 포함
|
||||
- WAV audio file header (44 bytes)
|
||||
- 14 fields (RIFF, WAVE, fmt, data chunks)
|
||||
- 16 bytes/row layout
|
||||
- Includes audio format, sample rate, etc.
|
||||
|
||||
### 네트워크 프로토콜
|
||||
### Network Protocols
|
||||
|
||||
#### 3. tcp-header.yaml
|
||||
- TCP 프로토콜 헤더 (20바이트)
|
||||
- 10개 필드 (포트, 시퀀스 번호, 플래그 등)
|
||||
- **비트필드 예시**: 8개 TCP 플래그 (CWR, ECE, URG, ACK, PSH, RST, SYN, FIN)
|
||||
- 16바이트/행 레이아웃
|
||||
- TCP protocol header (20 bytes)
|
||||
- 10 fields (ports, sequence number, flags, etc.)
|
||||
- **Bitfield example**: 8 TCP flags (CWR, ECE, URG, ACK, PSH, RST, SYN, FIN)
|
||||
- 16 bytes/row layout
|
||||
|
||||
#### 4. ip-header.yaml
|
||||
- IPv4 헤더 (20바이트)
|
||||
- 10개 필드 (Version, DSCP, TTL, Protocol, IP 주소 등)
|
||||
- **비트필드 예시**: Version+IHL, DSCP+ECN, Flags+FragmentOffset
|
||||
- IP 단편화 플래그 포함 (DF, MF)
|
||||
- IPv4 header (20 bytes)
|
||||
- 10 fields (Version, DSCP, TTL, Protocol, IP addresses, etc.)
|
||||
- **Bitfield example**: Version+IHL, DSCP+ECN, Flags+FragmentOffset
|
||||
- Includes IP fragmentation flags (DF, MF)
|
||||
|
||||
### 파일 시스템 & 권한
|
||||
### File System & Permissions
|
||||
|
||||
#### 5. file-attributes.yaml
|
||||
- FAT32 파일 속성 (4바이트)
|
||||
- **비트필드 예시**: 8개 파일 속성 플래그
|
||||
- READ_ONLY, HIDDEN, SYSTEM, DIRECTORY, ARCHIVE 등
|
||||
- FAT32 file attributes (4 bytes)
|
||||
- **Bitfield example**: 8 file attribute flags
|
||||
- READ_ONLY, HIDDEN, SYSTEM, DIRECTORY, ARCHIVE, etc.
|
||||
|
||||
#### 6. unix-permissions.yaml
|
||||
- Unix 파일 권한 (2바이트)
|
||||
- **비트필드 예시**: rwxrwxrwx (9비트)
|
||||
- Owner/Group/Others 권한 (읽기/쓰기/실행)
|
||||
- Unix file permissions (2 bytes)
|
||||
- **Bitfield example**: rwxrwxrwx (9 bits)
|
||||
- Owner/Group/Others permissions (read/write/execute)
|
||||
|
||||
### 하드웨어 & CPU
|
||||
### Hardware & CPU
|
||||
|
||||
#### 7. cpu-flags.yaml
|
||||
- x86 CPU FLAGS 레지스터 (4바이트)
|
||||
- **비트필드 예시**: 16개 CPU 플래그
|
||||
- CF, ZF, SF, IF, DF, OF, IOPL 등
|
||||
- x86 CPU FLAGS register (4 bytes)
|
||||
- **Bitfield example**: 16 CPU flags
|
||||
- CF, ZF, SF, IF, DF, OF, IOPL, etc.
|
||||
|
||||
## 주의사항
|
||||
## Important Notes
|
||||
|
||||
⚠️ **복사 시 들여쓰기 주의**
|
||||
⚠️ **Watch Out for Indentation When Copying**
|
||||
|
||||
Obsidian 코드블록에 붙여넣을 때 자동으로 들여쓰기가 추가될 수 있습니다.
|
||||
Obsidian may automatically add indentation when pasting into code blocks.
|
||||
|
||||
**문제 발생 시:**
|
||||
1. Obsidian **소스 모드**로 전환 (Ctrl/Cmd + E)
|
||||
2. ` ```bytegrid` 다음 줄 확인
|
||||
3. `name:` 앞에 공백이 **없어야** 정상
|
||||
4. 공백이 있다면 제거
|
||||
**If you encounter issues:**
|
||||
1. Switch to Obsidian **Source Mode** (Ctrl/Cmd + E)
|
||||
2. Check the line after ` ```bytegrid`
|
||||
3. There should be **no spaces** before `name:`
|
||||
4. Remove any extra spaces if present
|
||||
|
||||
## 직접 만들기
|
||||
## Create Your Own
|
||||
|
||||
새로운 구조체를 시각화하려면:
|
||||
To visualize a new structure:
|
||||
|
||||
```yaml
|
||||
name: 구조체 이름
|
||||
size: 전체 바이트 크기
|
||||
layout: 16 # 한 행당 바이트 수 (선택, 기본값: 16)
|
||||
name: Structure Name
|
||||
size: total byte size
|
||||
layout: 16 # bytes per row (optional, default: 16)
|
||||
legendPosition: right # right, left, bottom, none (optional, default: right)
|
||||
showFooter: true # show/hide footer (optional, default: true)
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: 필드명
|
||||
name: FieldName
|
||||
type: uint32_t
|
||||
color: blue # blue, cyan, yellow, green, orange, purple, mint, pink, gray
|
||||
description: "설명" # 선택사항
|
||||
description: "Description" # optional
|
||||
```
|
||||
|
||||
더 자세한 내용은 [CLAUDE.md](../CLAUDE.md)를 참조하세요.
|
||||
## Bit-Level Layout Examples
|
||||
|
||||
### 8. simple-bit-layout.yaml
|
||||
- Basic bit-level layout test (16 bits)
|
||||
- **Suffix notation**: `size: 16b`, `layout: 16b`, `offset: 0-7b`
|
||||
- `layoutUnit` can be omitted (auto-inferred from suffix)
|
||||
|
||||
### 9. nibble-test.yaml
|
||||
- 4-bit field (nibble) test
|
||||
- Each nibble represented independently
|
||||
|
||||
### 10. mixed-bit-byte.yaml
|
||||
- Mixed bit and byte offsets
|
||||
- `0-3b` (bit), `1B` (Byte), `2-3` (byte, default)
|
||||
|
||||
### 11. ipv4-bit-layout.yaml
|
||||
- IPv4 header in bit-level representation
|
||||
- Sub-byte fields like Version(4bit) + IHL(4bit)
|
||||
|
||||
**Suffix Notation:**
|
||||
- `b` (lowercase) = bit: `32b`, `0-7b`
|
||||
- `B` (uppercase) = Byte: `4B`, `0-3B`
|
||||
- no suffix = byte (default)
|
||||
|
||||
**layoutUnit field:**
|
||||
- Can be omitted (auto-inferred from suffix)
|
||||
- Can be explicit (use `layoutUnit: bit` or `layoutUnit: byte` if desired)
|
||||
|
||||
For more details, see [CLAUDE.md](../CLAUDE.md).
|
||||
|
|
|
|||
24
examples/ipv4-bit-layout.yaml
Normal file
24
examples/ipv4-bit-layout.yaml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
name: IPv4 Header (Bit Layout)
|
||||
size: 4B
|
||||
layout: 32b
|
||||
fields:
|
||||
- offset: 0-3b
|
||||
name: Version
|
||||
type: bits
|
||||
description: "IP version (4 for IPv4)"
|
||||
color: blue
|
||||
- offset: 4-7b
|
||||
name: IHL
|
||||
type: bits
|
||||
description: "Header length in 32-bit words"
|
||||
color: cyan
|
||||
- offset: 1B
|
||||
name: DSCP+ECN
|
||||
type: uint8_t
|
||||
description: "Byte 1 using B suffix"
|
||||
color: yellow
|
||||
- offset: 16-31b
|
||||
name: TotalLength
|
||||
type: uint16_t
|
||||
description: "Total packet length (2 bytes = 16 bits)"
|
||||
color: green
|
||||
24
examples/mixed-bit-byte.yaml
Normal file
24
examples/mixed-bit-byte.yaml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
name: Mixed Bit and Byte Offsets
|
||||
size: 32b
|
||||
layout: 32b
|
||||
fields:
|
||||
- offset: 0-3b
|
||||
name: Version
|
||||
type: bits
|
||||
description: "4 bits with b suffix"
|
||||
color: blue
|
||||
- offset: 4-7b
|
||||
name: IHL
|
||||
type: bits
|
||||
description: "4 bits with b suffix"
|
||||
color: cyan
|
||||
- offset: 1B
|
||||
name: SecondByte
|
||||
type: uint8_t
|
||||
description: "Byte 1 with B suffix (= bits 8-15)"
|
||||
color: yellow
|
||||
- offset: 2-3
|
||||
name: ThirdFourthByte
|
||||
type: uint16_t
|
||||
description: "Bytes 2-3 no suffix (= bits 16-31)"
|
||||
color: green
|
||||
14
examples/nibble-test.yaml
Normal file
14
examples/nibble-test.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
name: 4-bit Fields (Nibbles)
|
||||
size: 8b
|
||||
layout: 8b
|
||||
fields:
|
||||
- offset: 0-3b
|
||||
name: Nibble1
|
||||
type: bits
|
||||
description: "First 4 bits"
|
||||
color: blue
|
||||
- offset: 4-7b
|
||||
name: Nibble2
|
||||
type: bits
|
||||
description: "Second 4 bits"
|
||||
color: cyan
|
||||
14
examples/simple-bit-layout.yaml
Normal file
14
examples/simple-bit-layout.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
name: Simple Bit Layout Test
|
||||
size: 16b
|
||||
layout: 16b
|
||||
fields:
|
||||
- offset: 0-7b
|
||||
name: FirstByte
|
||||
type: uint8_t
|
||||
description: "8 bits using b suffix"
|
||||
color: blue
|
||||
- offset: 8-15b
|
||||
name: SecondByte
|
||||
type: uint8_t
|
||||
description: "Another 8 bits"
|
||||
color: cyan
|
||||
|
|
@ -14,21 +14,37 @@ import { parseOffset } from './validator';
|
|||
*/
|
||||
export function createLayout(config: ByteGridConfig): LayoutBlock[] {
|
||||
const layout = config.layout || 16;
|
||||
const layoutUnit = config.layoutUnit || 'byte';
|
||||
const blocks: LayoutBlock[] = [];
|
||||
|
||||
// Convert offset to bits for unified processing
|
||||
const toBits = (offset: ReturnType<typeof parseOffset>): { start: number; end: number; size: number } => {
|
||||
if (layoutUnit === 'bit') {
|
||||
// In bit layout mode, convert everything to bits
|
||||
if (offset.unit === 'bit') {
|
||||
return { start: offset.start, end: offset.end, size: offset.size };
|
||||
}
|
||||
// Convert byte offset to bits
|
||||
return { start: offset.start * 8, end: offset.end * 8 + 7, size: (offset.end - offset.start + 1) * 8 };
|
||||
}
|
||||
// In byte layout mode, keep as bytes
|
||||
return { start: offset.start, end: offset.end, size: offset.size };
|
||||
};
|
||||
|
||||
for (const field of config.fields) {
|
||||
const offset = parseOffset(field.offset);
|
||||
const offsetInUnits = toBits(offset);
|
||||
const color = (field.color || 'gray') as ColorName;
|
||||
const isPadding = field.type === 'reserved' || field.type === 'padding';
|
||||
|
||||
// Check if field spans multiple rows
|
||||
const startRow = Math.floor(offset.start / layout);
|
||||
const endRow = Math.floor(offset.end / layout);
|
||||
const startRow = Math.floor(offsetInUnits.start / layout);
|
||||
const endRow = Math.floor(offsetInUnits.end / layout);
|
||||
|
||||
if (startRow === endRow) {
|
||||
// Field fits in a single row
|
||||
const col = offset.start % layout;
|
||||
const span = offset.size;
|
||||
const col = offsetInUnits.start % layout;
|
||||
const span = offsetInUnits.size;
|
||||
|
||||
blocks.push({
|
||||
row: startRow,
|
||||
|
|
@ -39,42 +55,42 @@ export function createLayout(config: ByteGridConfig): LayoutBlock[] {
|
|||
color,
|
||||
value: field.value,
|
||||
description: field.description,
|
||||
offsetStart: offset.start,
|
||||
offsetEnd: offset.end,
|
||||
offsetStart: offsetInUnits.start,
|
||||
offsetEnd: offsetInUnits.end,
|
||||
isPadding,
|
||||
bitfields: field.bitfields,
|
||||
});
|
||||
} else {
|
||||
// Field spans multiple rows - split it
|
||||
let currentByte = offset.start;
|
||||
let currentUnit = offsetInUnits.start;
|
||||
|
||||
while (currentByte <= offset.end) {
|
||||
const row = Math.floor(currentByte / layout);
|
||||
const col = currentByte % layout;
|
||||
while (currentUnit <= offsetInUnits.end) {
|
||||
const row = Math.floor(currentUnit / layout);
|
||||
const col = currentUnit % layout;
|
||||
|
||||
// Calculate how many bytes fit in this row
|
||||
const bytesLeftInRow = layout - col;
|
||||
const bytesLeftInField = offset.end - currentByte + 1;
|
||||
const bytesInThisBlock = Math.min(bytesLeftInRow, bytesLeftInField);
|
||||
// Calculate how many units fit in this row
|
||||
const unitsLeftInRow = layout - col;
|
||||
const unitsLeftInField = offsetInUnits.end - currentUnit + 1;
|
||||
const unitsInThisBlock = Math.min(unitsLeftInRow, unitsLeftInField);
|
||||
|
||||
const blockEnd = currentByte + bytesInThisBlock - 1;
|
||||
const blockEnd = currentUnit + unitsInThisBlock - 1;
|
||||
|
||||
blocks.push({
|
||||
row,
|
||||
col,
|
||||
span: bytesInThisBlock,
|
||||
span: unitsInThisBlock,
|
||||
fieldName: field.name,
|
||||
fieldType: field.type,
|
||||
color,
|
||||
value: field.value,
|
||||
description: field.description,
|
||||
offsetStart: currentByte,
|
||||
offsetStart: currentUnit,
|
||||
offsetEnd: blockEnd,
|
||||
isPadding,
|
||||
bitfields: field.bitfields,
|
||||
});
|
||||
|
||||
currentByte += bytesInThisBlock;
|
||||
currentUnit += unitsInThisBlock;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export function parse(source: string): ByteGridConfig {
|
|||
throw new ParseError('Missing or invalid required field: name');
|
||||
}
|
||||
|
||||
if (!obj.size || typeof obj.size !== 'number') {
|
||||
if (!obj.size) {
|
||||
throw new ParseError('Missing or invalid required field: size');
|
||||
}
|
||||
|
||||
|
|
@ -49,8 +49,98 @@ export function parse(source: string): ByteGridConfig {
|
|||
throw new ParseError('Fields array must contain at least one field');
|
||||
}
|
||||
|
||||
// Parse layout (optional, defaults to 16)
|
||||
const layout = obj.layout !== undefined ? Number(obj.layout) : 16;
|
||||
// Parse layoutUnit (optional)
|
||||
let layoutUnit: 'byte' | 'bit' | undefined = obj.layoutUnit && typeof obj.layoutUnit === 'string'
|
||||
? (obj.layoutUnit as 'byte' | 'bit')
|
||||
: undefined;
|
||||
|
||||
// Parse layout (optional, defaults to 16) and infer layoutUnit if not specified
|
||||
let layout: number;
|
||||
if (obj.layout === undefined) {
|
||||
layout = 16;
|
||||
} else if (typeof obj.layout === 'number') {
|
||||
layout = obj.layout;
|
||||
} else if (typeof obj.layout === 'string') {
|
||||
const layoutStr = obj.layout.trim();
|
||||
let layoutValue: number;
|
||||
let layoutUnitSuffix: 'byte' | 'bit' | undefined;
|
||||
|
||||
// Extract suffix
|
||||
if (layoutStr.endsWith('b')) {
|
||||
layoutValue = parseInt(layoutStr.slice(0, -1).trim());
|
||||
layoutUnitSuffix = 'bit';
|
||||
// Auto-infer layoutUnit from suffix if not explicitly set
|
||||
if (!layoutUnit) layoutUnit = 'bit';
|
||||
} else if (layoutStr.endsWith('B')) {
|
||||
layoutValue = parseInt(layoutStr.slice(0, -1).trim());
|
||||
layoutUnitSuffix = 'byte';
|
||||
} else {
|
||||
layoutValue = parseInt(layoutStr);
|
||||
layoutUnitSuffix = undefined;
|
||||
}
|
||||
|
||||
if (isNaN(layoutValue) || layoutValue <= 0) {
|
||||
throw new ParseError('Invalid layout value');
|
||||
}
|
||||
|
||||
// Default layoutUnit to byte if still not set
|
||||
const effectiveLayoutUnit = layoutUnit || 'byte';
|
||||
const effectiveSuffix = layoutUnitSuffix || effectiveLayoutUnit;
|
||||
|
||||
// Convert to the layoutUnit
|
||||
if (effectiveLayoutUnit === 'bit') {
|
||||
// In bit layout mode, layout must be in bits
|
||||
layout = effectiveSuffix === 'byte' ? layoutValue * 8 : layoutValue;
|
||||
} else {
|
||||
// In byte layout mode, layout must be in bytes
|
||||
layout = effectiveSuffix === 'bit' ? Math.ceil(layoutValue / 8) : layoutValue;
|
||||
}
|
||||
} else {
|
||||
layout = 16;
|
||||
}
|
||||
|
||||
// Parse size (supports suffix: b = bit, B = Byte, no suffix = depends on layoutUnit) and infer layoutUnit if not specified
|
||||
let size: number;
|
||||
if (typeof obj.size === 'number') {
|
||||
size = obj.size;
|
||||
} else if (typeof obj.size === 'string') {
|
||||
const sizeStr = obj.size.trim();
|
||||
let sizeValue: number;
|
||||
let sizeUnitSuffix: 'byte' | 'bit' | undefined;
|
||||
|
||||
// Extract suffix
|
||||
if (sizeStr.endsWith('b')) {
|
||||
sizeValue = parseInt(sizeStr.slice(0, -1).trim());
|
||||
sizeUnitSuffix = 'bit';
|
||||
// Auto-infer layoutUnit from suffix if not explicitly set
|
||||
if (!layoutUnit) layoutUnit = 'bit';
|
||||
} else if (sizeStr.endsWith('B')) {
|
||||
sizeValue = parseInt(sizeStr.slice(0, -1).trim());
|
||||
sizeUnitSuffix = 'byte';
|
||||
} else {
|
||||
sizeValue = parseInt(sizeStr);
|
||||
sizeUnitSuffix = undefined;
|
||||
}
|
||||
|
||||
if (isNaN(sizeValue) || sizeValue <= 0) {
|
||||
throw new ParseError('Invalid size value');
|
||||
}
|
||||
|
||||
// Default layoutUnit to byte if still not set
|
||||
const effectiveLayoutUnit = layoutUnit || 'byte';
|
||||
const effectiveSizeUnit = sizeUnitSuffix || effectiveLayoutUnit;
|
||||
|
||||
// Convert to the layoutUnit
|
||||
if (effectiveLayoutUnit === 'bit') {
|
||||
// In bit layout mode, size must be in bits
|
||||
size = effectiveSizeUnit === 'byte' ? sizeValue * 8 : sizeValue;
|
||||
} else {
|
||||
// In byte layout mode, size must be in bytes
|
||||
size = effectiveSizeUnit === 'bit' ? Math.ceil(sizeValue / 8) : sizeValue;
|
||||
}
|
||||
} else {
|
||||
throw new ParseError('Size must be a number or string');
|
||||
}
|
||||
|
||||
// Parse fields
|
||||
const fields: Field[] = obj.fields.map((fieldObj, index) => {
|
||||
|
|
@ -152,11 +242,27 @@ export function parse(source: string): ByteGridConfig {
|
|||
? (obj.colorScheme as 'default' | 'dark' | 'light')
|
||||
: undefined;
|
||||
|
||||
// Parse legendPosition (optional)
|
||||
const legendPosition = obj.legendPosition && typeof obj.legendPosition === 'string'
|
||||
? (obj.legendPosition as 'right' | 'left' | 'bottom' | 'none')
|
||||
: undefined;
|
||||
|
||||
// Parse showFooter (optional)
|
||||
const showFooter = obj.showFooter !== undefined && typeof obj.showFooter === 'boolean'
|
||||
? obj.showFooter
|
||||
: undefined;
|
||||
|
||||
// Set final layoutUnit (default to byte if not inferred)
|
||||
const finalLayoutUnit = layoutUnit || 'byte';
|
||||
|
||||
return {
|
||||
name: obj.name,
|
||||
size: obj.size,
|
||||
size,
|
||||
layout,
|
||||
layoutUnit: finalLayoutUnit,
|
||||
colorScheme,
|
||||
legendPosition,
|
||||
showFooter,
|
||||
fields,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* Renders LayoutBlocks to SVG string
|
||||
*/
|
||||
|
||||
import { ByteGridConfig, LayoutBlock, RenderOptions, ColorName } from './types';
|
||||
import { ByteGridConfig, LayoutBlock, RenderOptions, ColorName, LegendPosition } from './types';
|
||||
|
||||
/**
|
||||
* Color palette mapping
|
||||
|
|
@ -26,6 +26,8 @@ const COLORS: Record<ColorName, string> = {
|
|||
const DEFAULT_OPTIONS: Required<RenderOptions> = {
|
||||
showHexDump: false,
|
||||
showLegend: true,
|
||||
legendPosition: 'right',
|
||||
showFooter: true,
|
||||
showGrid: true,
|
||||
cellWidth: 40, // Increased from 30 to make bit numbers readable
|
||||
cellHeight: 30,
|
||||
|
|
@ -47,8 +49,35 @@ export function renderSVG(
|
|||
options?: RenderOptions
|
||||
): string {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||
const layoutUnit = config.layoutUnit || 'byte';
|
||||
const layout = config.layout || 16;
|
||||
|
||||
// Adjust cellWidth for bit layout
|
||||
if (layoutUnit === 'bit' && !options?.cellWidth) {
|
||||
opts.cellWidth = 10; // Smaller cells for bit-level layout
|
||||
}
|
||||
|
||||
// Determine legend position
|
||||
// Priority: options.legendPosition > config.legendPosition > options.showLegend (backward compat) > default
|
||||
let effectiveLegendPosition: LegendPosition;
|
||||
if (options?.legendPosition !== undefined) {
|
||||
effectiveLegendPosition = options.legendPosition;
|
||||
} else if (config.legendPosition !== undefined) {
|
||||
effectiveLegendPosition = config.legendPosition;
|
||||
} else if (options?.showLegend === false) {
|
||||
effectiveLegendPosition = 'none';
|
||||
} else {
|
||||
effectiveLegendPosition = 'right'; // default
|
||||
}
|
||||
|
||||
// Determine showFooter
|
||||
// Priority: options.showFooter > config.showFooter > default (true)
|
||||
const effectiveShowFooter = options?.showFooter !== undefined
|
||||
? options.showFooter
|
||||
: config.showFooter !== undefined
|
||||
? config.showFooter
|
||||
: opts.showFooter;
|
||||
|
||||
// Detect rows with bitfields
|
||||
const rowsWithBitfields = new Set<number>();
|
||||
for (const block of blocks) {
|
||||
|
|
@ -65,13 +94,33 @@ export function renderSVG(
|
|||
// Calculate dimensions
|
||||
const rows = Math.ceil(config.size / layout);
|
||||
const gridWidth = layout * opts.cellWidth;
|
||||
const legendWidth = opts.showLegend ? 200 : 0;
|
||||
const legendWidth = effectiveLegendPosition !== 'none' && effectiveLegendPosition !== 'bottom' ? 200 : 0;
|
||||
const margin = 20;
|
||||
|
||||
// Count unique fields for legend height calculation
|
||||
const uniqueFieldNames = new Set(blocks.map((b) => b.fieldName));
|
||||
const fieldCount = uniqueFieldNames.size;
|
||||
const legendHeight = opts.showLegend && fieldCount > 0 ? fieldCount * 50 + 40 : 0;
|
||||
|
||||
// Calculate legend height (for 'bottom' position or side positions)
|
||||
let legendHeight = 0;
|
||||
if (effectiveLegendPosition !== 'none' && fieldCount > 0) {
|
||||
// Calculate actual height needed based on bitfields
|
||||
let totalLegendHeight = 40; // Header space
|
||||
const uniqueFields = new Map<string, LayoutBlock>();
|
||||
for (const block of blocks) {
|
||||
if (!uniqueFields.has(block.fieldName)) {
|
||||
uniqueFields.set(block.fieldName, block);
|
||||
}
|
||||
}
|
||||
for (const [, block] of uniqueFields) {
|
||||
let entryHeight = 50;
|
||||
if (block.bitfields && block.bitfields.length > 0) {
|
||||
entryHeight = 50 + block.bitfields.length * 12;
|
||||
}
|
||||
totalLegendHeight += entryHeight;
|
||||
}
|
||||
legendHeight = totalLegendHeight;
|
||||
}
|
||||
|
||||
// Calculate grid height based on row heights
|
||||
let gridHeight = 0;
|
||||
|
|
@ -89,14 +138,33 @@ export function renderSVG(
|
|||
}
|
||||
}
|
||||
|
||||
// Use the larger of grid height or legend height
|
||||
const contentHeight = Math.max(gridHeight, legendHeight);
|
||||
// Calculate total width and height based on legend position
|
||||
const footerHeight = effectiveShowFooter ? 60 : 20; // Footer space or just bottom margin
|
||||
let totalWidth: number;
|
||||
let totalHeight: number;
|
||||
let gridStartX: number;
|
||||
let gridStartY: number;
|
||||
|
||||
const totalWidth = margin + gridWidth + margin + legendWidth + margin;
|
||||
const totalHeight = margin + 60 + contentHeight + margin + 80;
|
||||
|
||||
const gridStartX = margin;
|
||||
const gridStartY = margin + 60;
|
||||
if (effectiveLegendPosition === 'left') {
|
||||
// Legend on left, grid on right
|
||||
totalWidth = margin + legendWidth + margin + gridWidth + margin;
|
||||
totalHeight = margin + 60 + Math.max(gridHeight, legendHeight) + margin + footerHeight;
|
||||
gridStartX = margin + legendWidth + margin;
|
||||
gridStartY = margin + 60;
|
||||
} else if (effectiveLegendPosition === 'bottom') {
|
||||
// Legend at bottom (with extra 30px spacing for title)
|
||||
totalWidth = margin + gridWidth + margin;
|
||||
totalHeight = margin + 60 + gridHeight + margin + 30 + legendHeight + footerHeight;
|
||||
gridStartX = margin;
|
||||
gridStartY = margin + 60;
|
||||
} else {
|
||||
// Legend on right (default) or none
|
||||
const contentHeight = Math.max(gridHeight, legendHeight);
|
||||
totalWidth = margin + gridWidth + margin + legendWidth + margin;
|
||||
totalHeight = margin + 60 + contentHeight + margin + footerHeight;
|
||||
gridStartX = margin;
|
||||
gridStartY = margin + 60;
|
||||
}
|
||||
|
||||
// Calculate cumulative Y position for each row
|
||||
const rowYPositions = new Map<number, number>();
|
||||
|
|
@ -125,12 +193,27 @@ export function renderSVG(
|
|||
// Title
|
||||
svg += `<text x="${totalWidth / 2}" y="30" text-anchor="middle" font-size="16" font-weight="bold">${escapeXml(config.name)}</text>`;
|
||||
|
||||
// Column headers (byte offsets)
|
||||
// Column headers
|
||||
if (opts.showGrid) {
|
||||
for (let col = 0; col < layout; col++) {
|
||||
const x = gridStartX + col * opts.cellWidth + opts.cellWidth / 2;
|
||||
const y = gridStartY - 10;
|
||||
svg += `<text x="${x}" y="${y}" text-anchor="middle" font-size="9" fill="#666">${col}</text>`;
|
||||
// For bit layout, show index 0-7 pattern, for byte layout show absolute position
|
||||
const headerLabel = layoutUnit === 'bit' ? col % 8 : col;
|
||||
svg += `<text x="${x}" y="${y}" text-anchor="middle" font-size="9" fill="#666">${headerLabel}</text>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Row headers (left side) for bit layout
|
||||
if (opts.showGrid && layoutUnit === 'bit') {
|
||||
for (let row = 0; row < rows; row++) {
|
||||
const y = rowYPositions.get(row) || gridStartY;
|
||||
const cellHeight = rowsWithBitfields.has(row)
|
||||
? (opts.uniformRowHeight && hasBitfields ? bitfieldCellHeight : bitfieldCellHeight)
|
||||
: (opts.uniformRowHeight && hasBitfields ? bitfieldCellHeight : normalCellHeight);
|
||||
const startOffset = row * layout;
|
||||
const textY = y + cellHeight / 2 + 4;
|
||||
svg += `<text x="${gridStartX - 5}" y="${textY}" text-anchor="end" font-size="9" fill="#666">${startOffset}</text>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,16 +228,18 @@ export function renderSVG(
|
|||
// Draw block rectangle
|
||||
svg += `<rect x="${x}" y="${y}" width="${width}" height="${cellHeight}" fill="${color}" stroke="#333" stroke-width="1"/>`;
|
||||
|
||||
// Draw byte numbers
|
||||
for (let i = 0; i < block.span; i++) {
|
||||
const byteNum = block.offsetStart + i;
|
||||
const cellX = x + i * opts.cellWidth + opts.cellWidth / 2;
|
||||
const cellY = y + (cellHeight / 3); // Upper third for byte number
|
||||
svg += `<text x="${cellX}" y="${cellY}" text-anchor="middle" font-size="${opts.fontSize}" fill="#333">${byteNum}</text>`;
|
||||
// Draw numbers inside cells (only for byte layout)
|
||||
if (layoutUnit !== 'bit') {
|
||||
for (let i = 0; i < block.span; i++) {
|
||||
const byteNum = block.offsetStart + i;
|
||||
const cellX = x + i * opts.cellWidth + opts.cellWidth / 2;
|
||||
const cellY = y + (cellHeight / 3); // Upper third for byte number
|
||||
svg += `<text x="${cellX}" y="${cellY}" text-anchor="middle" font-size="${opts.fontSize}" fill="#333">${byteNum}</text>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw bit grid for bitfield cells
|
||||
if (block.bitfields && block.bitfields.length > 0) {
|
||||
// Draw bit grid for bitfield cells (only in byte layout mode)
|
||||
if (block.bitfields && block.bitfields.length > 0 && layoutUnit !== 'bit') {
|
||||
for (let i = 0; i < block.span; i++) {
|
||||
const cellX = x + i * opts.cellWidth;
|
||||
const bitCellWidth = opts.cellWidth / 8;
|
||||
|
|
@ -175,12 +260,43 @@ export function renderSVG(
|
|||
}
|
||||
}
|
||||
|
||||
// Legend
|
||||
if (opts.showLegend && blocks.length > 0) {
|
||||
const legendX = margin + gridWidth + margin * 2;
|
||||
const legendStartY = gridStartY;
|
||||
// Draw bit boundary lines for bit layout
|
||||
if (layoutUnit === 'bit') {
|
||||
for (let col = 1; col < layout; col++) {
|
||||
const lineX = gridStartX + col * opts.cellWidth;
|
||||
// Every 8 bits: darker dotted line (byte boundary)
|
||||
// Other bits: lighter dotted line (bit boundary)
|
||||
if (col % 8 === 0) {
|
||||
svg += `<line x1="${lineX}" y1="${gridStartY}" x2="${lineX}" y2="${gridStartY + gridHeight}" stroke="#999" stroke-width="1" stroke-dasharray="3,3"/>`;
|
||||
} else {
|
||||
svg += `<line x1="${lineX}" y1="${gridStartY}" x2="${lineX}" y2="${gridStartY + gridHeight}" stroke="#ddd" stroke-width="0.5" stroke-dasharray="2,2"/>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg += `<text x="${legendX}" y="${legendStartY - 20}" font-size="12" font-weight="bold" fill="#333">Fields</text>`;
|
||||
// Legend
|
||||
if (effectiveLegendPosition !== 'none' && blocks.length > 0) {
|
||||
let legendX: number;
|
||||
let legendStartY: number;
|
||||
let legendTitleY: number;
|
||||
|
||||
// Position legend based on effectiveLegendPosition
|
||||
if (effectiveLegendPosition === 'left') {
|
||||
legendX = margin;
|
||||
legendStartY = gridStartY;
|
||||
legendTitleY = legendStartY - 20;
|
||||
} else if (effectiveLegendPosition === 'bottom') {
|
||||
legendX = margin;
|
||||
legendStartY = gridStartY + gridHeight + margin + 30; // Extra space for title
|
||||
legendTitleY = gridStartY + gridHeight + margin + 10;
|
||||
} else {
|
||||
// 'right' (default)
|
||||
legendX = margin + gridWidth + margin * 2;
|
||||
legendStartY = gridStartY;
|
||||
legendTitleY = legendStartY - 20;
|
||||
}
|
||||
|
||||
svg += `<text x="${legendX}" y="${legendTitleY}" font-size="12" font-weight="bold" fill="#333">Fields</text>`;
|
||||
|
||||
// Group blocks by field name to avoid duplicates
|
||||
const uniqueFields = new Map<string, LayoutBlock>();
|
||||
|
|
@ -212,7 +328,9 @@ export function renderSVG(
|
|||
svg += `<text x="${legendX + 30}" y="${y + 24}" font-size="9" fill="#666">${escapeXml(block.fieldType)}</text>`;
|
||||
|
||||
// Offset info
|
||||
svg += `<text x="${legendX + 30}" y="${y + 36}" font-size="9" fill="#999">offset: ${block.offsetStart}-${block.offsetEnd} (${block.offsetEnd - block.offsetStart + 1} bytes)</text>`;
|
||||
const unit = layoutUnit === 'bit' ? 'bits' : 'bytes';
|
||||
const size = block.offsetEnd - block.offsetStart + 1;
|
||||
svg += `<text x="${legendX + 30}" y="${y + 36}" font-size="9" fill="#999">offset: ${block.offsetStart}-${block.offsetEnd} (${size} ${unit})</text>`;
|
||||
|
||||
// Bitfields (if any)
|
||||
if (block.bitfields && block.bitfields.length > 0) {
|
||||
|
|
@ -231,8 +349,12 @@ export function renderSVG(
|
|||
}
|
||||
|
||||
// Footer
|
||||
const footerY = totalHeight - 40;
|
||||
svg += `<text x="${totalWidth / 2}" y="${footerY}" text-anchor="middle" font-size="11" fill="#999">Total size: ${config.size} bytes | Layout: ${layout} bytes/row</text>`;
|
||||
if (effectiveShowFooter) {
|
||||
const footerY = totalHeight - 40;
|
||||
const sizeUnit = layoutUnit === 'bit' ? 'bits' : 'bytes';
|
||||
const layoutUnitText = layoutUnit === 'bit' ? 'bits/row' : 'bytes/row';
|
||||
svg += `<text x="${totalWidth / 2}" y="${footerY}" text-anchor="middle" font-size="11" fill="#999">Total size: ${config.size} ${sizeUnit} | Layout: ${layout} ${layoutUnitText}</text>`;
|
||||
}
|
||||
|
||||
svg += `</svg>`;
|
||||
return svg;
|
||||
|
|
|
|||
|
|
@ -72,14 +72,27 @@ export interface Field {
|
|||
bitfields?: Bitfield[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout unit type
|
||||
*/
|
||||
export type LayoutUnit = 'byte' | 'bit';
|
||||
|
||||
/**
|
||||
* Legend position options
|
||||
*/
|
||||
export type LegendPosition = 'right' | 'left' | 'bottom' | 'none';
|
||||
|
||||
/**
|
||||
* Main configuration for ByteGrid visualization
|
||||
*/
|
||||
export interface ByteGridConfig {
|
||||
name: string;
|
||||
size: number; // Total size in bytes
|
||||
layout?: number; // Bytes per row (default: 16)
|
||||
size: number; // Total size in bytes (for bit layouts, this is total bits)
|
||||
layout?: number; // Units per row (default: 16), unit depends on layoutUnit
|
||||
layoutUnit?: LayoutUnit; // 'byte' (default) or 'bit'
|
||||
colorScheme?: ColorScheme;
|
||||
legendPosition?: LegendPosition; // 'right' (default), 'left', 'bottom', or 'none'
|
||||
showFooter?: boolean; // Show footer with total size and layout info (default: true)
|
||||
fields: Field[];
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +103,7 @@ export interface OffsetRange {
|
|||
start: number;
|
||||
end: number;
|
||||
size: number;
|
||||
unit: LayoutUnit; // 'byte' or 'bit'
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -115,7 +129,9 @@ export interface LayoutBlock {
|
|||
*/
|
||||
export interface RenderOptions {
|
||||
showHexDump?: boolean;
|
||||
showLegend?: boolean;
|
||||
showLegend?: boolean; // Deprecated: use legendPosition instead
|
||||
legendPosition?: LegendPosition; // 'right' (default), 'left', 'bottom', or 'none'
|
||||
showFooter?: boolean; // Show footer with total size and layout info (default: true)
|
||||
showGrid?: boolean;
|
||||
cellWidth?: number;
|
||||
cellHeight?: number;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const SUPPORTED_TYPES: Set<string> = new Set([
|
|||
'double',
|
||||
'reserved',
|
||||
'padding',
|
||||
'bits', // For bit-level fields
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
@ -47,10 +48,14 @@ const TYPE_BITS: Record<string, number> = {
|
|||
|
||||
/**
|
||||
* Parse offset string to OffsetRange
|
||||
* Supports formats: "0-3" (range) or "4" (single byte)
|
||||
* Supports formats:
|
||||
* "0-3" (byte range), "4" (single byte)
|
||||
* "0-7b" (bit range), "5b" (single bit)
|
||||
* "0-3B" (byte range), "0B" (single byte)
|
||||
* No suffix defaults to byte
|
||||
*
|
||||
* @param offset Offset string
|
||||
* @returns Parsed offset range
|
||||
* @returns Parsed offset range with unit
|
||||
* @throws ValidationError if format is invalid
|
||||
*/
|
||||
export function parseOffset(offset: string): OffsetRange {
|
||||
|
|
@ -60,9 +65,21 @@ export function parseOffset(offset: string): OffsetRange {
|
|||
throw new ValidationError('Offset cannot be empty');
|
||||
}
|
||||
|
||||
// Extract suffix (b = bit, B = Byte, none = byte default)
|
||||
let unit: 'byte' | 'bit' = 'byte';
|
||||
let offsetWithoutSuffix = trimmed;
|
||||
|
||||
if (trimmed.endsWith('b')) {
|
||||
unit = 'bit';
|
||||
offsetWithoutSuffix = trimmed.slice(0, -1).trim();
|
||||
} else if (trimmed.endsWith('B')) {
|
||||
unit = 'byte';
|
||||
offsetWithoutSuffix = trimmed.slice(0, -1).trim();
|
||||
}
|
||||
|
||||
// Check for range format "0-3"
|
||||
if (trimmed.includes('-')) {
|
||||
const parts = trimmed.split('-');
|
||||
if (offsetWithoutSuffix.includes('-')) {
|
||||
const parts = offsetWithoutSuffix.split('-');
|
||||
|
||||
if (parts.length !== 2) {
|
||||
throw new ValidationError(`Invalid offset format: "${offset}"`);
|
||||
|
|
@ -89,11 +106,12 @@ export function parseOffset(offset: string): OffsetRange {
|
|||
start,
|
||||
end,
|
||||
size: end - start + 1,
|
||||
unit,
|
||||
};
|
||||
}
|
||||
|
||||
// Single byte format "4"
|
||||
const value = parseInt(trimmed);
|
||||
// Single value format "4"
|
||||
const value = parseInt(offsetWithoutSuffix);
|
||||
|
||||
if (isNaN(value)) {
|
||||
throw new ValidationError(`Invalid offset format: "${offset}"`);
|
||||
|
|
@ -107,6 +125,7 @@ export function parseOffset(offset: string): OffsetRange {
|
|||
start: value,
|
||||
end: value,
|
||||
size: 1,
|
||||
unit,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -168,9 +187,24 @@ function parseBitfieldBits(bits: string): { start: number; end: number } {
|
|||
* @throws ValidationError if validation fails
|
||||
*/
|
||||
export function validate(config: ByteGridConfig): void {
|
||||
const layoutUnit = config.layoutUnit || 'byte';
|
||||
|
||||
// Convert offset to bits for unified comparison
|
||||
const toBits = (offset: OffsetRange): { start: number; end: number } => {
|
||||
if (offset.unit === 'bit') {
|
||||
return { start: offset.start, end: offset.end };
|
||||
}
|
||||
// Convert bytes to bits
|
||||
return { start: offset.start * 8, end: offset.end * 8 + 7 };
|
||||
};
|
||||
|
||||
// Total size in bits
|
||||
const totalSizeInBits = layoutUnit === 'bit' ? config.size : config.size * 8;
|
||||
|
||||
const parsedFields: Array<{
|
||||
index: number;
|
||||
offset: OffsetRange;
|
||||
offsetInBits: { start: number; end: number };
|
||||
name: string;
|
||||
type: DataType;
|
||||
}> = [];
|
||||
|
|
@ -190,10 +224,12 @@ export function validate(config: ByteGridConfig): void {
|
|||
);
|
||||
}
|
||||
|
||||
// Validate field doesn't exceed total size
|
||||
if (offset.end >= config.size) {
|
||||
const offsetInBits = toBits(offset);
|
||||
|
||||
// Validate field doesn't exceed total size (in bits)
|
||||
if (offsetInBits.end >= totalSizeInBits) {
|
||||
throw new ValidationError(
|
||||
`Field "${field.name}" at index ${i} (offset ${offset.start}-${offset.end}) exceeds total size ${config.size}`,
|
||||
`Field "${field.name}" at index ${i} (offset ${field.offset}) exceeds total size ${config.size} ${layoutUnit}${config.size !== 1 ? 's' : ''}`,
|
||||
i
|
||||
);
|
||||
}
|
||||
|
|
@ -246,26 +282,29 @@ export function validate(config: ByteGridConfig): void {
|
|||
parsedFields.push({
|
||||
index: i,
|
||||
offset,
|
||||
offsetInBits,
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
});
|
||||
}
|
||||
|
||||
// Check for overlapping fields
|
||||
// Check for overlapping fields (in bit space)
|
||||
for (let i = 0; i < parsedFields.length; i++) {
|
||||
for (let j = i + 1; j < parsedFields.length; j++) {
|
||||
const field1 = parsedFields[i];
|
||||
const field2 = parsedFields[j];
|
||||
|
||||
// Check if ranges overlap
|
||||
// Check if ranges overlap (in bits)
|
||||
const overlap =
|
||||
(field1.offset.start <= field2.offset.end && field1.offset.end >= field2.offset.start) ||
|
||||
(field2.offset.start <= field1.offset.end && field2.offset.end >= field1.offset.start);
|
||||
(field1.offsetInBits.start <= field2.offsetInBits.end && field1.offsetInBits.end >= field2.offsetInBits.start) ||
|
||||
(field2.offsetInBits.start <= field1.offsetInBits.end && field2.offsetInBits.end >= field1.offsetInBits.start);
|
||||
|
||||
if (overlap) {
|
||||
const field1Desc = `${config.fields[field1.index].offset}`;
|
||||
const field2Desc = `${config.fields[field2.index].offset}`;
|
||||
throw new ValidationError(
|
||||
`Field "${field2.name}" at index ${field2.index} (offset ${field2.offset.start}-${field2.offset.end}) ` +
|
||||
`overlaps with field "${field1.name}" (offset ${field1.offset.start}-${field1.offset.end})`,
|
||||
`Field "${field2.name}" at index ${field2.index} (offset ${field2Desc}) ` +
|
||||
`overlaps with field "${field1.name}" (offset ${field1Desc})`,
|
||||
field2.index
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -247,4 +247,141 @@ describe('LayoutEngine', () => {
|
|||
expect(blocks[3].col).toBe(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createLayout() - bit-unit layout', () => {
|
||||
it('should create layout blocks for bit-unit fields', () => {
|
||||
const config: ByteGridConfig = {
|
||||
name: 'Bit Layout',
|
||||
size: 16, // 16 bits
|
||||
layoutUnit: 'bit',
|
||||
layout: 16, // 16 bits per row
|
||||
fields: [
|
||||
{ offset: '0-7b', name: 'FirstByte', type: 'uint8_t', color: 'blue' },
|
||||
{ offset: '8-15b', name: 'SecondByte', type: 'uint8_t', color: 'cyan' },
|
||||
],
|
||||
};
|
||||
|
||||
const blocks = createLayout(config);
|
||||
|
||||
expect(blocks).toHaveLength(2);
|
||||
|
||||
// FirstByte: bits 0-7
|
||||
expect(blocks[0].row).toBe(0);
|
||||
expect(blocks[0].col).toBe(0);
|
||||
expect(blocks[0].span).toBe(8);
|
||||
expect(blocks[0].fieldName).toBe('FirstByte');
|
||||
expect(blocks[0].offsetStart).toBe(0);
|
||||
expect(blocks[0].offsetEnd).toBe(7);
|
||||
|
||||
// SecondByte: bits 8-15
|
||||
expect(blocks[1].row).toBe(0);
|
||||
expect(blocks[1].col).toBe(8);
|
||||
expect(blocks[1].span).toBe(8);
|
||||
expect(blocks[1].fieldName).toBe('SecondByte');
|
||||
});
|
||||
|
||||
it('should handle Byte offset in bit layout', () => {
|
||||
const config: ByteGridConfig = {
|
||||
name: 'Mixed',
|
||||
size: 16, // 16 bits
|
||||
layoutUnit: 'bit',
|
||||
layout: 16,
|
||||
fields: [
|
||||
{ offset: '0B', name: 'ByteField', type: 'uint8_t', color: 'blue' },
|
||||
],
|
||||
};
|
||||
|
||||
const blocks = createLayout(config);
|
||||
|
||||
expect(blocks).toHaveLength(1);
|
||||
// 0B = byte 0 = bits 0-7
|
||||
expect(blocks[0].row).toBe(0);
|
||||
expect(blocks[0].col).toBe(0);
|
||||
expect(blocks[0].span).toBe(8);
|
||||
expect(blocks[0].offsetStart).toBe(0);
|
||||
expect(blocks[0].offsetEnd).toBe(7);
|
||||
});
|
||||
|
||||
it('should split bit fields across multiple rows', () => {
|
||||
const config: ByteGridConfig = {
|
||||
name: 'Multi-row Bits',
|
||||
size: 32, // 32 bits
|
||||
layoutUnit: 'bit',
|
||||
layout: 16, // 16 bits per row
|
||||
fields: [
|
||||
{ offset: '0-23b', name: 'LargeBitField', type: 'bits', color: 'green' },
|
||||
],
|
||||
};
|
||||
|
||||
const blocks = createLayout(config);
|
||||
|
||||
// Should split into 2 blocks (16 bits each)
|
||||
expect(blocks).toHaveLength(2);
|
||||
|
||||
// First row: bits 0-15
|
||||
expect(blocks[0].row).toBe(0);
|
||||
expect(blocks[0].col).toBe(0);
|
||||
expect(blocks[0].span).toBe(16);
|
||||
expect(blocks[0].offsetStart).toBe(0);
|
||||
expect(blocks[0].offsetEnd).toBe(15);
|
||||
|
||||
// Second row: bits 16-23
|
||||
expect(blocks[1].row).toBe(1);
|
||||
expect(blocks[1].col).toBe(0);
|
||||
expect(blocks[1].span).toBe(8);
|
||||
expect(blocks[1].offsetStart).toBe(16);
|
||||
expect(blocks[1].offsetEnd).toBe(23);
|
||||
});
|
||||
|
||||
it('should handle mixed bit and Byte offsets', () => {
|
||||
const config: ByteGridConfig = {
|
||||
name: 'IPv4 Header',
|
||||
size: 32, // 32 bits
|
||||
layoutUnit: 'bit',
|
||||
layout: 32,
|
||||
fields: [
|
||||
{ offset: '0-3b', name: 'Version', type: 'bits', color: 'blue' },
|
||||
{ offset: '4-7b', name: 'IHL', type: 'bits', color: 'cyan' },
|
||||
{ offset: '1B', name: 'DSCP', type: 'uint8_t', color: 'yellow' },
|
||||
],
|
||||
};
|
||||
|
||||
const blocks = createLayout(config);
|
||||
|
||||
expect(blocks).toHaveLength(3);
|
||||
|
||||
// Version: bits 0-3
|
||||
expect(blocks[0].col).toBe(0);
|
||||
expect(blocks[0].span).toBe(4);
|
||||
|
||||
// IHL: bits 4-7
|
||||
expect(blocks[1].col).toBe(4);
|
||||
expect(blocks[1].span).toBe(4);
|
||||
|
||||
// DSCP: byte 1 = bits 8-15
|
||||
expect(blocks[2].col).toBe(8);
|
||||
expect(blocks[2].span).toBe(8);
|
||||
expect(blocks[2].offsetStart).toBe(8);
|
||||
expect(blocks[2].offsetEnd).toBe(15);
|
||||
});
|
||||
|
||||
it('should handle 4-bit fields', () => {
|
||||
const config: ByteGridConfig = {
|
||||
name: '4-bit Fields',
|
||||
size: 8, // 8 bits = 1 byte
|
||||
layoutUnit: 'bit',
|
||||
layout: 8,
|
||||
fields: [
|
||||
{ offset: '0-3b', name: 'Nibble1', type: 'bits' },
|
||||
{ offset: '4-7b', name: 'Nibble2', type: 'bits' },
|
||||
],
|
||||
};
|
||||
|
||||
const blocks = createLayout(config);
|
||||
|
||||
expect(blocks).toHaveLength(2);
|
||||
expect(blocks[0].span).toBe(4);
|
||||
expect(blocks[1].span).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -230,5 +230,399 @@ fields:
|
|||
expect(() => parse(yaml)).toThrow(ParseError);
|
||||
expect(() => parse(yaml)).toThrow(/field.*type/i);
|
||||
});
|
||||
|
||||
// Bit-unit layout tests
|
||||
describe('Bit-unit layout', () => {
|
||||
it('should parse layoutUnit: bit', () => {
|
||||
const yaml = `
|
||||
name: Bit Layout Test
|
||||
size: 2
|
||||
layoutUnit: bit
|
||||
layout: 16
|
||||
fields:
|
||||
- offset: 0-7b
|
||||
name: BitField
|
||||
type: uint8_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.layoutUnit).toBe('bit');
|
||||
expect(result.layout).toBe(16);
|
||||
});
|
||||
|
||||
it('should default layoutUnit to byte', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.layoutUnit).toBe('byte');
|
||||
});
|
||||
|
||||
it('should parse bit offset with b suffix: 0-7b', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 2
|
||||
layoutUnit: bit
|
||||
fields:
|
||||
- offset: 0-7b
|
||||
name: BitField
|
||||
type: uint8_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.fields[0].offset).toBe('0-7b');
|
||||
});
|
||||
|
||||
it('should parse Byte offset with B suffix: 0B', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 2
|
||||
layoutUnit: bit
|
||||
fields:
|
||||
- offset: 0B
|
||||
name: ByteField
|
||||
type: uint8_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.fields[0].offset).toBe('0B');
|
||||
});
|
||||
|
||||
it('should parse offset without suffix as Byte', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 2
|
||||
layoutUnit: bit
|
||||
fields:
|
||||
- offset: 0-1
|
||||
name: TwoBytes
|
||||
type: uint16_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.fields[0].offset).toBe('0-1');
|
||||
});
|
||||
|
||||
it('should parse mixed bit and Byte offsets', () => {
|
||||
const yaml = `
|
||||
name: IPv4 Header
|
||||
size: 4
|
||||
layoutUnit: bit
|
||||
layout: 32
|
||||
fields:
|
||||
- offset: 0-3b
|
||||
name: Version
|
||||
type: bits
|
||||
- offset: 4-7b
|
||||
name: IHL
|
||||
type: bits
|
||||
- offset: 1B
|
||||
name: DSCP
|
||||
type: uint8_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.fields).toHaveLength(3);
|
||||
expect(result.fields[0].offset).toBe('0-3b');
|
||||
expect(result.fields[1].offset).toBe('4-7b');
|
||||
expect(result.fields[2].offset).toBe('1B');
|
||||
});
|
||||
|
||||
it('should parse size with B suffix (Byte)', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 4B
|
||||
layoutUnit: bit
|
||||
fields:
|
||||
- offset: 0-31b
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.size).toBe(32); // 4 bytes = 32 bits (in bit layout mode)
|
||||
});
|
||||
|
||||
it('should parse size with b suffix (bit)', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 32b
|
||||
layoutUnit: bit
|
||||
fields:
|
||||
- offset: 0-31b
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.size).toBe(32); // 32 bits
|
||||
});
|
||||
|
||||
it('should parse size without suffix (uses layoutUnit)', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 4
|
||||
layoutUnit: byte
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.size).toBe(4); // 4 bytes (layoutUnit=byte)
|
||||
});
|
||||
|
||||
it('should parse layout with B suffix (Byte)', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16b
|
||||
layoutUnit: bit
|
||||
layout: 2B
|
||||
fields:
|
||||
- offset: 0-15b
|
||||
name: Field1
|
||||
type: uint16_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.layout).toBe(16); // 2 Bytes = 16 bits (in bit layout mode)
|
||||
});
|
||||
|
||||
it('should parse layout with b suffix (bit)', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 32b
|
||||
layoutUnit: bit
|
||||
layout: 24b
|
||||
fields:
|
||||
- offset: 0-31b
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.layout).toBe(24); // 24 bits
|
||||
});
|
||||
|
||||
it('should parse layout without suffix (uses layoutUnit)', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 32
|
||||
layoutUnit: byte
|
||||
layout: 8
|
||||
fields:
|
||||
- offset: 0-31
|
||||
name: Field1
|
||||
type: uint8_t[32]
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.layout).toBe(8); // 8 bytes (layoutUnit=byte)
|
||||
});
|
||||
|
||||
it('should auto-infer layoutUnit=bit from b suffix', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 32b
|
||||
layout: 16b
|
||||
fields:
|
||||
- offset: 0-7b
|
||||
name: Field1
|
||||
type: uint8_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.layoutUnit).toBe('bit'); // Auto-inferred from b suffix
|
||||
expect(result.size).toBe(32);
|
||||
expect(result.layout).toBe(16);
|
||||
});
|
||||
|
||||
it('should default to byte when no layoutUnit and no b suffix', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 4B
|
||||
layout: 16
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.layoutUnit).toBe('byte'); // Default
|
||||
expect(result.size).toBe(4);
|
||||
expect(result.layout).toBe(16);
|
||||
});
|
||||
|
||||
it('should prefer explicit layoutUnit over suffix inference', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 4B
|
||||
layoutUnit: byte
|
||||
layout: 16
|
||||
fields:
|
||||
- offset: 0-31b
|
||||
name: Field1
|
||||
type: bits
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.layoutUnit).toBe('byte'); // Explicit wins
|
||||
});
|
||||
});
|
||||
|
||||
// Legend position tests
|
||||
describe('Legend position', () => {
|
||||
it('should parse legendPosition: right', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
legendPosition: right
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.legendPosition).toBe('right');
|
||||
});
|
||||
|
||||
it('should parse legendPosition: left', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
legendPosition: left
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.legendPosition).toBe('left');
|
||||
});
|
||||
|
||||
it('should parse legendPosition: bottom', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
legendPosition: bottom
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.legendPosition).toBe('bottom');
|
||||
});
|
||||
|
||||
it('should parse legendPosition: none', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
legendPosition: none
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.legendPosition).toBe('none');
|
||||
});
|
||||
|
||||
it('should return undefined when legendPosition is not specified', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.legendPosition).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// showFooter tests
|
||||
describe('showFooter', () => {
|
||||
it('should parse showFooter: true', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
showFooter: true
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.showFooter).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse showFooter: false', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
showFooter: false
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.showFooter).toBe(false);
|
||||
});
|
||||
|
||||
it('should return undefined when showFooter is not specified', () => {
|
||||
const yaml = `
|
||||
name: Test
|
||||
size: 16
|
||||
fields:
|
||||
- offset: 0-3
|
||||
name: Field1
|
||||
type: uint32_t
|
||||
`;
|
||||
|
||||
const result = parse(yaml);
|
||||
|
||||
expect(result.showFooter).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -284,5 +284,204 @@ describe('SVGRenderer', () => {
|
|||
expect(svg).toContain('height="30"');
|
||||
});
|
||||
});
|
||||
|
||||
// Legend position tests
|
||||
describe('Legend position', () => {
|
||||
const legendConfig: ByteGridConfig = {
|
||||
name: 'Legend Position Test',
|
||||
size: 16,
|
||||
layout: 16,
|
||||
fields: [
|
||||
{ offset: '0-3', name: 'Field1', type: 'uint32_t', color: 'blue' },
|
||||
{ offset: '4-7', name: 'Field2', type: 'uint32_t', color: 'cyan' },
|
||||
],
|
||||
};
|
||||
|
||||
const legendBlocks: LayoutBlock[] = [
|
||||
{
|
||||
row: 0,
|
||||
col: 0,
|
||||
span: 4,
|
||||
fieldName: 'Field1',
|
||||
fieldType: 'uint32_t',
|
||||
color: 'blue',
|
||||
offsetStart: 0,
|
||||
offsetEnd: 3,
|
||||
isPadding: false,
|
||||
},
|
||||
{
|
||||
row: 0,
|
||||
col: 4,
|
||||
span: 4,
|
||||
fieldName: 'Field2',
|
||||
fieldType: 'uint32_t',
|
||||
color: 'cyan',
|
||||
offsetStart: 4,
|
||||
offsetEnd: 7,
|
||||
isPadding: false,
|
||||
},
|
||||
];
|
||||
|
||||
it('should render legend on right by default', () => {
|
||||
const svg = renderSVG(legendConfig, legendBlocks);
|
||||
|
||||
// Should contain legend title
|
||||
expect(svg).toContain('Fields');
|
||||
expect(svg).toContain('Field1');
|
||||
expect(svg).toContain('Field2');
|
||||
});
|
||||
|
||||
it('should render legend on right when legendPosition is "right"', () => {
|
||||
const svg = renderSVG(legendConfig, legendBlocks, {
|
||||
legendPosition: 'right',
|
||||
});
|
||||
|
||||
expect(svg).toContain('Fields');
|
||||
expect(svg).toContain('Field1');
|
||||
expect(svg).toContain('Field2');
|
||||
});
|
||||
|
||||
it('should render legend on left when legendPosition is "left"', () => {
|
||||
const svg = renderSVG(legendConfig, legendBlocks, {
|
||||
legendPosition: 'left',
|
||||
});
|
||||
|
||||
expect(svg).toContain('Fields');
|
||||
expect(svg).toContain('Field1');
|
||||
expect(svg).toContain('Field2');
|
||||
});
|
||||
|
||||
it('should render legend at bottom when legendPosition is "bottom"', () => {
|
||||
const svg = renderSVG(legendConfig, legendBlocks, {
|
||||
legendPosition: 'bottom',
|
||||
});
|
||||
|
||||
expect(svg).toContain('Fields');
|
||||
expect(svg).toContain('Field1');
|
||||
expect(svg).toContain('Field2');
|
||||
});
|
||||
|
||||
it('should not render legend when legendPosition is "none"', () => {
|
||||
const svg = renderSVG(legendConfig, legendBlocks, {
|
||||
legendPosition: 'none',
|
||||
});
|
||||
|
||||
// Should NOT contain legend
|
||||
expect(svg).not.toContain('Fields');
|
||||
});
|
||||
|
||||
it('should not render legend when showLegend is false (backward compatibility)', () => {
|
||||
const svg = renderSVG(legendConfig, legendBlocks, {
|
||||
showLegend: false,
|
||||
});
|
||||
|
||||
// Should NOT contain legend
|
||||
expect(svg).not.toContain('Fields');
|
||||
});
|
||||
|
||||
it('should prioritize legendPosition over showLegend', () => {
|
||||
// legendPosition: 'none' should override showLegend: true
|
||||
const svg1 = renderSVG(legendConfig, legendBlocks, {
|
||||
showLegend: true,
|
||||
legendPosition: 'none',
|
||||
});
|
||||
|
||||
expect(svg1).not.toContain('Fields');
|
||||
|
||||
// legendPosition: 'right' should override showLegend: false
|
||||
const svg2 = renderSVG(legendConfig, legendBlocks, {
|
||||
showLegend: false,
|
||||
legendPosition: 'right',
|
||||
});
|
||||
|
||||
expect(svg2).toContain('Fields');
|
||||
});
|
||||
|
||||
it('should use config.legendPosition when options.legendPosition is not provided', () => {
|
||||
const configWithLegend: ByteGridConfig = {
|
||||
...legendConfig,
|
||||
legendPosition: 'left',
|
||||
};
|
||||
|
||||
const svg = renderSVG(configWithLegend, legendBlocks);
|
||||
|
||||
expect(svg).toContain('Fields');
|
||||
});
|
||||
|
||||
it('should prioritize options.legendPosition over config.legendPosition', () => {
|
||||
const configWithLegend: ByteGridConfig = {
|
||||
...legendConfig,
|
||||
legendPosition: 'left',
|
||||
};
|
||||
|
||||
const svg = renderSVG(configWithLegend, legendBlocks, {
|
||||
legendPosition: 'none',
|
||||
});
|
||||
|
||||
expect(svg).not.toContain('Fields');
|
||||
});
|
||||
|
||||
it('should respect config.legendPosition: none', () => {
|
||||
const configWithLegend: ByteGridConfig = {
|
||||
...legendConfig,
|
||||
legendPosition: 'none',
|
||||
};
|
||||
|
||||
const svg = renderSVG(configWithLegend, legendBlocks);
|
||||
|
||||
expect(svg).not.toContain('Fields');
|
||||
});
|
||||
});
|
||||
|
||||
// showFooter tests
|
||||
describe('showFooter option', () => {
|
||||
it('should render footer by default', () => {
|
||||
const svg = renderSVG(sampleConfig, sampleBlocks);
|
||||
|
||||
expect(svg).toContain('Total size:');
|
||||
expect(svg).toContain('Layout:');
|
||||
});
|
||||
|
||||
it('should render footer when showFooter is true in options', () => {
|
||||
const svg = renderSVG(sampleConfig, sampleBlocks, {
|
||||
showFooter: true,
|
||||
});
|
||||
|
||||
expect(svg).toContain('Total size:');
|
||||
});
|
||||
|
||||
it('should not render footer when showFooter is false in options', () => {
|
||||
const svg = renderSVG(sampleConfig, sampleBlocks, {
|
||||
showFooter: false,
|
||||
});
|
||||
|
||||
expect(svg).not.toContain('Total size:');
|
||||
expect(svg).not.toContain('Layout:');
|
||||
});
|
||||
|
||||
it('should use config.showFooter when options.showFooter is not provided', () => {
|
||||
const configWithFooter: ByteGridConfig = {
|
||||
...sampleConfig,
|
||||
showFooter: false,
|
||||
};
|
||||
|
||||
const svg = renderSVG(configWithFooter, sampleBlocks);
|
||||
|
||||
expect(svg).not.toContain('Total size:');
|
||||
});
|
||||
|
||||
it('should prioritize options.showFooter over config.showFooter', () => {
|
||||
const configWithFooter: ByteGridConfig = {
|
||||
...sampleConfig,
|
||||
showFooter: true,
|
||||
};
|
||||
|
||||
const svg = renderSVG(configWithFooter, sampleBlocks, {
|
||||
showFooter: false,
|
||||
});
|
||||
|
||||
expect(svg).not.toContain('Total size:');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -246,4 +246,137 @@ describe('Validator', () => {
|
|||
expect(() => validate(config)).toThrow(/bit.*out of range/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseOffset() - bit/byte suffix', () => {
|
||||
it('should parse bit offset with b suffix: 0-7b', () => {
|
||||
const result = parseOffset('0-7b');
|
||||
|
||||
expect(result.start).toBe(0);
|
||||
expect(result.end).toBe(7);
|
||||
expect(result.size).toBe(8);
|
||||
expect(result.unit).toBe('bit');
|
||||
});
|
||||
|
||||
it('should parse single bit with b suffix: 5b', () => {
|
||||
const result = parseOffset('5b');
|
||||
|
||||
expect(result.start).toBe(5);
|
||||
expect(result.end).toBe(5);
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.unit).toBe('bit');
|
||||
});
|
||||
|
||||
it('should parse Byte offset with B suffix: 0B', () => {
|
||||
const result = parseOffset('0B');
|
||||
|
||||
expect(result.start).toBe(0);
|
||||
expect(result.end).toBe(0);
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.unit).toBe('byte');
|
||||
});
|
||||
|
||||
it('should parse Byte range with B suffix: 1-3B', () => {
|
||||
const result = parseOffset('1-3B');
|
||||
|
||||
expect(result.start).toBe(1);
|
||||
expect(result.end).toBe(3);
|
||||
expect(result.size).toBe(3);
|
||||
expect(result.unit).toBe('byte');
|
||||
});
|
||||
|
||||
it('should default to byte unit when no suffix: 0-3', () => {
|
||||
const result = parseOffset('0-3');
|
||||
|
||||
expect(result.start).toBe(0);
|
||||
expect(result.end).toBe(3);
|
||||
expect(result.size).toBe(4);
|
||||
expect(result.unit).toBe('byte');
|
||||
});
|
||||
|
||||
it('should parse with whitespace: 0 - 7b', () => {
|
||||
const result = parseOffset('0 - 7b');
|
||||
|
||||
expect(result.start).toBe(0);
|
||||
expect(result.end).toBe(7);
|
||||
expect(result.size).toBe(8);
|
||||
expect(result.unit).toBe('bit');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate() - bit-unit layout', () => {
|
||||
it('should validate bit-unit layout with bit offsets', () => {
|
||||
const config: ByteGridConfig = {
|
||||
name: 'Bit Layout',
|
||||
size: 16, // 16 bits = 2 bytes
|
||||
layoutUnit: 'bit',
|
||||
layout: 16,
|
||||
fields: [
|
||||
{ offset: '0-7b', name: 'FirstByte', type: 'uint8_t' },
|
||||
{ offset: '8-15b', name: 'SecondByte', type: 'uint8_t' },
|
||||
],
|
||||
};
|
||||
|
||||
expect(() => validate(config)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should validate mixed bit and Byte offsets', () => {
|
||||
const config: ByteGridConfig = {
|
||||
name: 'Mixed Layout',
|
||||
size: 32, // 32 bits = 4 bytes
|
||||
layoutUnit: 'bit',
|
||||
layout: 32,
|
||||
fields: [
|
||||
{ offset: '0-3b', name: 'Version', type: 'bits' },
|
||||
{ offset: '4-7b', name: 'IHL', type: 'bits' },
|
||||
{ offset: '1B', name: 'DSCP', type: 'uint8_t' },
|
||||
],
|
||||
};
|
||||
|
||||
expect(() => validate(config)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should detect overlap in bit-unit layout', () => {
|
||||
const config: ByteGridConfig = {
|
||||
name: 'Overlap Test',
|
||||
size: 16, // 16 bits = 2 bytes
|
||||
layoutUnit: 'bit',
|
||||
fields: [
|
||||
{ offset: '0-7b', name: 'Field1', type: 'uint8_t' },
|
||||
{ offset: '4-11b', name: 'Field2', type: 'uint8_t' }, // overlaps bits 4-7
|
||||
],
|
||||
};
|
||||
|
||||
expect(() => validate(config)).toThrow(ValidationError);
|
||||
expect(() => validate(config)).toThrow(/overlap/i);
|
||||
});
|
||||
|
||||
it('should detect overlap between bit and Byte offsets', () => {
|
||||
const config: ByteGridConfig = {
|
||||
name: 'Mixed Overlap',
|
||||
size: 16, // 16 bits = 2 bytes
|
||||
layoutUnit: 'bit',
|
||||
fields: [
|
||||
{ offset: '0-3b', name: 'Version', type: 'bits' },
|
||||
{ offset: '0B', name: 'FullByte', type: 'uint8_t' }, // overlaps bits 0-3
|
||||
],
|
||||
};
|
||||
|
||||
expect(() => validate(config)).toThrow(ValidationError);
|
||||
expect(() => validate(config)).toThrow(/overlap/i);
|
||||
});
|
||||
|
||||
it('should throw ValidationError if bit offset exceeds size', () => {
|
||||
const config: ByteGridConfig = {
|
||||
name: 'Size Test',
|
||||
size: 16, // 16 bits
|
||||
layoutUnit: 'bit',
|
||||
fields: [
|
||||
{ offset: '0-19b', name: 'TooBig', type: 'bits' }, // exceeds 16 bits
|
||||
],
|
||||
};
|
||||
|
||||
expect(() => validate(config)).toThrow(ValidationError);
|
||||
expect(() => validate(config)).toThrow(/exceeds total size/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue