This document outlines the implementation plan for porting the TypeScript pretext text layout library to PHP 8.
src/
Enum/
SegmentBreakKind.php
DTO/
SegmentMetrics.php
EngineProfile.php
TextAnalysis.php
MergedSegmentation.php
PreparedText.php
PreparedTextWithSegments.php
LayoutCursor.php
LayoutResult.php
LayoutLine.php
LayoutLineRange.php
InternalLayoutLine.php
LineBreakCursor.php
Measurement/
FontMeasurerInterface.php
GdFontMeasurer.php
StubFontMeasurer.php
Analysis.php
Bidi.php
Measurement.php
LineBreak.php
Pretext.php
PretextFactory.php
tests/
Unit/
AnalysisTest.php
BidiTest.php
MeasurementTest.php
LineBreakTest.php
PretextTest.php
FontMeasurerTest.php
Stub/
TestFontMeasurer.php
bootstrap.php
phpunit.xml
composer.json
- Create
SegmentBreakKindenum (5 cases) - Create all DTO classes as readonly classes
- Set up composer.json with required dependencies
- Set up PHPUnit configuration
- Implement
Analysis.php- Text segmentation and merging logic - Implement
Bidi.php- Bidirectional text algorithm - Implement
Measurement.php- Text measurement with injectable font measurer - Implement
LineBreak.php- Line breaking algorithm
- Implement
Pretext.php- Main class with instance-based API - Implement
PretextFactory.php- Static factory for convenience
- Implement
FontMeasurerInterface - Implement
StubFontMeasurerfor testing - Implement
GdFontMeasurerfor production use with GD/FreeType
- Port all TypeScript tests to PHPUnit
- Implement test stubs matching the original behavior
- Add edge case tests for PHP-specific behavior
- Ensure 100% feature parity with original test suite
- Use
mb_*functions for all string operations - Use
mb_str_split($str, 1)for codepoint iteration - All internal strings are UTF-8
private const KINSOKU_START = ["\u{FF0C}" => true, ...];
// Usage: isset(self::KINSOKU_START[$ch])- Word segmentation:
IntlBreakIterator::createWordInstance() - Grapheme segmentation:
IntlBreakIterator::createCharacterInstance() isWordLike→getRuleStatus() !== IntlBreakIterator::WORD_NONE
- PHP 8.0+ has native
WeakMapsupport - Use for
lineTextCacheskeyed byPreparedTextWithSegmentsobjects
- Abstract font measurement behind interface
GdFontMeasurerusesimagettfbbox()for productionStubFontMeasurermirrors the original test fake
- Always use server/headless profile (no browser detection)
lineFitEpsilon: 0.005carryCJKAfterClosingQuote: falsepreferPrefixWidthsForBreakableRuns: falsepreferEarlySoftHyphenBreak: false
intl- ICU for word/grapheme segmentation,IntlCharmbstring- Multibyte string functionsgd(optional, for production) - FreeType font measurement- PCRE2 with Unicode support (PHP 7.3+)
phpunit/phpunit- Testing frameworkphpstan/phpstan- Static analysis (optional)
- Unit Tests - Individual module testing
- Integration Tests - Full prepare → layout pipeline
- Locale Tests - Thai, Arabic, CJK, Devanagari, Myanmar
- Edge Cases - Empty strings, whitespace, special characters
- Performance Tests - Ensure O(n) complexity maintained
- All 50+ test cases from
layout.test.ts - Character width mapping from
measureWidth() - Segment expectations for various languages
class Pretext {
public function __construct(FontMeasurerInterface $measurer);
public function prepare(string $text, string $font): PreparedText;
public function prepareWithSegments(string $text, string $font): PreparedTextWithSegments;
public function layout(PreparedText $prepared, float $maxWidth, float $lineHeight): LayoutResult;
public function layoutWithLines(...): LayoutLinesResult;
public function walkLineRanges(...): int;
public function layoutNextLine(...): ?LayoutLine;
public function clearCache(): void;
public function setLocale(?string $locale): void;
}class PretextFactory {
public static function create(?FontMeasurerInterface $measurer = null): Pretext;
public static function prepare(string $text, string $font): PreparedText;
public static function layout(...): LayoutResult;
}- Most complex module due to multiple merge passes
IntlBreakIteratorAPI differs slightly fromIntl.Segmenter- Careful with
isWordLikedetection logic
- Pure arithmetic, most straightforward to port
- Inner functions become closures with
use (&$var) - Track both segment indices and grapheme indices
- All bidi-relevant ranges are BMP (≤ U+08AC)
- Safe to use
mb_ord()which returns code points segStartstracked as codepoint offsets
- Browser-specific DOM comparison not available
- Emoji correction always returns 0 in PHP
- Acceptable for server-side use
- All TypeScript types mapped to PHP DTOs
- All constants (Sets, regexes) ported
- All public functions from
layout.tsimplemented - All merge functions from
analysis.tsimplemented - Bidi algorithm produces same levels
- Line breaking matches original behavior
- Test suite passes with StubFontMeasurer
- No browser-specific dependencies in core logic
- PHP 8.0+ features used appropriately (readonly, WeakMap, match)
- Feature Parity: 100% of original test cases pass
- Performance: ≤2x slower than JS (acceptable for PHP)
- Memory: Efficient caching with WeakMap
- API: Clean, idiomatic PHP interface
- Documentation: Full PHPDoc coverage
- Phase 1 (DTOs/Enums): 30 minutes
- Phase 2 (Core): 2 hours
- Phase 3 (API): 30 minutes
- Phase 4 (Fonts): 30 minutes
- Phase 5 (Tests): 1 hour
Total Estimated Time: 4.5 hours