Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions docs/classifier-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Classifier Guide

The classifier module (`src/measurement-validator/classifier.ts`) analyses a `MeasurementResult` and returns a `DivergenceAnalysis` that identifies the most likely root cause.

## Root Causes

| Root Cause | Description |
|-----------------|-------------|
| `font_fallback` | The requested font is not loaded; the browser fell back to a generic serif/sans-serif |
| `bidi_shaping` | RTL text causes different shaping between canvas and DOM |
| `emoji_rendering` | Emoji glyphs have browser-specific widths |
| `browser_quirk` | Known browser-specific kerning/spacing differences |
| `unknown` | Divergence detected but cause could not be classified |

## Usage

```typescript
import { classifyDivergence } from './src/measurement-validator/classifier.ts'

const analysis = await classifyDivergence(result, sample)

if (analysis.detected) {
console.log(`Root cause: ${analysis.rootCause}`)
console.log(`Confidence: ${(analysis.confidence * 100).toFixed(0)}%`)
console.log(`Recommendation: ${analysis.recommendation}`)
}
```

## Individual Detectors

### `detectBidi(sample)`

Checks whether the sample contains RTL characters (Arabic, Hebrew, and related Unicode ranges). Returns `{ detected, severity, confidence }`.

```typescript
import { detectBidi } from './src/measurement-validator/classifier.ts'

const result = detectBidi({ text: 'مرحبا', font: '16px Arial', maxWidth: 400 })
// { detected: true, severity: 'major', confidence: 0.9 }
```

### `detectEmoji(sample)`

Checks for emoji presentation characters using Unicode property escapes. Returns `{ detected, severity, confidence, emojiCount }`.

```typescript
import { detectEmoji } from './src/measurement-validator/classifier.ts'

const result = detectEmoji({ text: 'Hello 😀', font: '16px Arial', maxWidth: 400 })
// { detected: true, severity: 'major', confidence: 0.85, emojiCount: 1 }
```

### `detectBrowserQuirk(sample)`

Checks the current browser's User Agent for known quirky rendering behaviours (e.g., Safari-specific kerning).

```typescript
import { detectBrowserQuirk } from './src/measurement-validator/classifier.ts'

const result = detectBrowserQuirk(sample)
// { detected: false, severity: 'minor', confidence: 0, recommendation: '' }
```

### `detectFontFallback(sample)`

Compares measurements using the specified font against the `serif` fallback. If the widths are similar, font fallback is likely.

```typescript
import { detectFontFallback } from './src/measurement-validator/classifier.ts'

const result = await detectFontFallback(sample)
// { detected: false, severity: 'minor' }
```

## Classification Priority

`classifyDivergence` tests causes in priority order:

1. Font fallback (highest impact)
2. Bidi shaping
3. Emoji rendering
4. Browser quirks
5. Unknown (fallback)

The first matching cause is returned, so a sample with both RTL text and emoji would be classified as `bidi_shaping`.
45 changes: 45 additions & 0 deletions docs/language-matrix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Language Support Matrix

The measurement validator ships with test fixtures covering 20+ languages across five scripts categories.

## Supported Languages

| Language | Script | Fixture File | Known Issues |
|---------------|-------------|-------------------------------|--------------|
| English | Latin | `english-samples.json` | None |
| Arabic | Arabic | `rtl-samples.json` | Bidi shaping may differ between canvas and DOM |
| Hebrew | Hebrew | `rtl-samples.json` | Bidi shaping |
| Urdu | Arabic | `rtl-samples.json` | Naskh/Nastaliq glyph joining |
| Chinese | CJK | `cjk-samples.json` | Full-width glyph width variation |
| Japanese | CJK/Kana | `cjk-samples.json` | Kana iteration marks |
| Korean | Hangul | `cjk-samples.json` | None |
| Thai | Thai | `complex-script-samples.json` | No inter-word spaces; line-break is content-dependent |
| Myanmar | Myanmar | `complex-script-samples.json` | Medial consonant stacking |
| Khmer | Khmer | `complex-script-samples.json` | Zero-width separators in source text |
| Hindi | Devanagari | `complex-script-samples.json` | Conjunct consonants |
| Mixed EN+AR | Latin+Arabic| `mixed-bidi-samples.json` | Bidi embedding |
| Mixed EN+HE | Latin+Hebrew| `mixed-bidi-samples.json` | Bidi embedding |

## Fixture Summary

| File | Samples | Languages |
|--------------------------------|---------|-----------|
| `english-samples.json` | 10 | English |
| `rtl-samples.json` | 5 | Arabic, Hebrew, Urdu |
| `cjk-samples.json` | 5 | Chinese, Japanese, Korean |
| `complex-script-samples.json` | 5 | Thai, Myanmar, Khmer, Hindi |
| `mixed-bidi-samples.json` | 4 | English+Arabic, English+Hebrew |
| **Total** | **29** | **13+ languages** |

## Adding a New Language

1. Add fixture entries to the appropriate JSON file (or create a new one in `test/fixtures/`).
2. Each entry requires `id`, `text`, `font`, `maxWidth`; `lineHeight` is optional.
3. Run `bun test test/classifier.test.ts` to verify detection behaviour.

## Known Limitations

- **Font availability**: Fixtures use generic system fonts (`Arial`, `Georgia`, etc.). Rare-script fonts may not be installed on all systems, causing `font_fallback` divergence.
- **Bidi shaping**: Canvas `measureText` does not apply full Unicode bidirectional shaping. RTL samples will typically show divergence until Pretext ships bidi-aware measurement.
- **Complex scripts**: Thai, Myanmar, and Khmer use contextual glyph shaping that is not reproduced by canvas `measureText`. Expect major/critical divergence on complex-script samples.
- **Emoji**: Emoji widths vary by OS, browser, and emoji version. The `emoji_rendering` classifier will flag these samples.
131 changes: 131 additions & 0 deletions docs/measurement-validator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Measurement Validator

A module for detecting and analyzing divergence between canvas (Pretext) and DOM text measurements.

## Getting Started

```typescript
import {
MeasurementComparator,
classifyDivergence,
generateConsoleSummary,
} from './src/measurement-validator/index.ts'

const comparator = new MeasurementComparator()

const sample = {
text: 'Hello world',
font: '16px Arial',
maxWidth: 400,
lineHeight: 20,
}

// Run Pretext layout
import { prepare, layoutWithLines } from '@chenglou/pretext'
const prepared = prepare(sample.text, sample.font)
const pretextLayout = layoutWithLines(prepared, sample.maxWidth, sample.lineHeight)

// Compare with DOM
const result = await comparator.compare(sample, pretextLayout)
console.log(result.overallSeverity) // 'pass' | 'warning' | 'error' | 'critical'

// Classify divergence root cause
const analysis = await classifyDivergence(result, sample)
console.log(analysis.rootCause) // 'font_fallback' | 'bidi_shaping' | 'emoji_rendering' | ...

// Generate human-readable summary
console.log(generateConsoleSummary([result]))
```

## API Reference

### `MeasurementComparator`

Compares Pretext layout output against live DOM measurements.

```typescript
class MeasurementComparator {
async compare(
sample: MeasurementSample,
pretextLayout: PretextLayoutResult,
): Promise<MeasurementResult>
}
```

### `measureDOMText(sample)`

Measures text layout using the browser DOM.

```typescript
async function measureDOMText(sample: MeasurementSample): Promise<DOMTextMetrics>
```

### `classifyDivergence(result, sample)`

Identifies the root cause of measurement divergence.

```typescript
async function classifyDivergence(
result: MeasurementResult,
sample: MeasurementSample,
): Promise<DivergenceAnalysis>
```

### `generateJSONReport(results)`

Serializes results to a JSON string.

### `generateConsoleSummary(results)`

Returns a human-readable pass/warning/error/critical summary.

### `generateDetailedReport(results)`

Returns a detailed per-line breakdown.

### `generateLanguageBreakdown(results)`

Returns a count of results by severity.

## Types

```typescript
interface MeasurementSample {
text: string
font: string
maxWidth: number
lineHeight?: number
whiteSpace?: 'normal' | 'pre-wrap'
}

interface MeasurementResult {
sample: MeasurementSample
lines: LineComparison[]
totalLines: number
exactMatches: number
minorDelta: number
majorDelta: number
criticalDelta: number
overallSeverity: 'pass' | 'warning' | 'error' | 'critical'
timestamp: string
executionTimeMs: number
}

interface DivergenceAnalysis {
detected: boolean
severity: 'minor' | 'major' | 'critical'
rootCause?: 'font_fallback' | 'bidi_shaping' | 'emoji_rendering' | 'browser_quirk' | 'variable_font' | 'unknown'
confidence: number
recommendation: string
details: Record<string, unknown>
}
```

## Severity Thresholds

| Severity | Delta (px) |
|----------|-----------|
| exact | < 0.1 |
| minor | 0.1–0.5 |
| major | 0.5–2.0 |
| critical | ≥ 2.0 |
Loading