diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md index ab9f51f..8e45330 100644 --- a/docs/CHEATSHEET.md +++ b/docs/CHEATSHEET.md @@ -64,6 +64,7 @@ $article->setMetadata( ```php new Title('Title'); +new ArticleTitle('Feed Title'); // Enhanced feed title new Heading('Heading', level: 2); // 1-6 new Body('Text'); (new Body('

HTML

'))->setFormat('html'); @@ -80,6 +81,9 @@ new Illustrator('Name'); ### Media ```php +ArticleThumbnail::fromBundle('feed.jpg'); // Custom feed thumbnail +ArticleThumbnail::fromUrl('...')->setCaption('...')->setAccessibilityCaption('...'); + Photo::fromUrl('https://...'); Photo::fromBundle('image.jpg'); Photo::fromUrl('...')->setCaption('...')->setAccessibilityCaption('...'); diff --git a/docs/COOKBOOK.md b/docs/COOKBOOK.md index 39f2b6f..98bf4ae 100644 --- a/docs/COOKBOOK.md +++ b/docs/COOKBOOK.md @@ -292,6 +292,7 @@ $article->addComponent( | Component | Role | Key Properties | |-----------|------|----------------| | `Title` | title | text | +| `ArticleTitle` | article_title | text, enhanced formatting for feeds | | `Heading` | heading1-6 | text, level (1-6) | | `Body` | body | text, format (html/none) | | `Intro` | intro | text, drop cap support | @@ -307,6 +308,7 @@ $article->addComponent( | Component | Factory Methods | Notes | |-----------|-----------------|-------| +| `ArticleThumbnail` | `fromUrl()`, `fromBundle()` | Custom feed thumbnail | | `Photo` | `fromUrl()`, `fromBundle()` | Single image | | `Image` | `fromUrl()`, `fromBundle()` | Generic image (alias) | | `Figure` | `fromUrl()`, `fromBundle()` | Image with caption | @@ -356,6 +358,39 @@ $article->addComponent( | `MediumRectangleAdvertisement` | - | MREC ad slot (300x250) | | `ReplicaAdvertisement` | - | Print replica ad slot | +### Feed Customization Components + +Control how your article appears in Apple News feeds: + +```php +use TomGould\AppleNews\Document\Components\{ArticleThumbnail, ArticleTitle}; + +// Custom thumbnail for feeds (different from article images) +$thumbnail = ArticleThumbnail::fromBundle('feed-preview.jpg') + ->setCaption('Breaking news coverage') + ->setAccessibilityCaption('Reporter at press conference'); + +$article->addComponent($thumbnail); + +// Enhanced title for feed display +$articleTitle = new ArticleTitle('Major Policy Change Announced'); +$articleTitle->setTextStyle('feedTitleStyle'); + +$article->addComponent($articleTitle); +``` + +**ArticleThumbnail properties:** +- `URL` - Image source (bundle:// or https://) +- `caption` - Visible caption text +- `accessibilityCaption` - VoiceOver description +- `explicitContent` - Content warning flag + +**ArticleTitle** extends TextComponent, supporting: +- `textStyle` - Named style reference +- `format` - html, markdown, or none +- `inlineTextStyles` - Range-based styling +- `additions` - Link ranges + --- ## Layouts & Containers diff --git a/tests/Document/Components/ArticleThumbnailTest.php b/tests/Document/Components/ArticleThumbnailTest.php new file mode 100644 index 0000000..16232b1 --- /dev/null +++ b/tests/Document/Components/ArticleThumbnailTest.php @@ -0,0 +1,112 @@ +jsonSerialize(); + + $this->assertSame('article_thumbnail', $json['role']); + $this->assertSame('https://example.com/image.jpg', $json['URL']); + } + + public function testFromUrl(): void + { + $thumbnail = ArticleThumbnail::fromUrl('https://example.com/thumb.png'); + $json = $thumbnail->jsonSerialize(); + + $this->assertSame('article_thumbnail', $json['role']); + $this->assertSame('https://example.com/thumb.png', $json['URL']); + } + + public function testFromBundle(): void + { + $thumbnail = ArticleThumbnail::fromBundle('feed-image.jpg'); + $json = $thumbnail->jsonSerialize(); + + $this->assertSame('article_thumbnail', $json['role']); + $this->assertSame('bundle://feed-image.jpg', $json['URL']); + } + + public function testWithCaption(): void + { + $thumbnail = (new ArticleThumbnail('https://example.com/image.jpg')) + ->setCaption('A descriptive caption'); + $json = $thumbnail->jsonSerialize(); + + $this->assertSame('A descriptive caption', $json['caption']); + } + + public function testWithAccessibilityCaption(): void + { + $thumbnail = (new ArticleThumbnail('https://example.com/image.jpg')) + ->setAccessibilityCaption('Image shows a sunset over mountains'); + $json = $thumbnail->jsonSerialize(); + + $this->assertSame('Image shows a sunset over mountains', $json['accessibilityCaption']); + } + + public function testWithExplicitContent(): void + { + $thumbnail = (new ArticleThumbnail('https://example.com/image.jpg')) + ->setExplicitContent(true); + $json = $thumbnail->jsonSerialize(); + + $this->assertTrue($json['explicitContent']); + } + + public function testExplicitContentFalse(): void + { + $thumbnail = (new ArticleThumbnail('https://example.com/image.jpg')) + ->setExplicitContent(false); + $json = $thumbnail->jsonSerialize(); + + $this->assertFalse($json['explicitContent']); + } + + public function testFullyConfigured(): void + { + $thumbnail = ArticleThumbnail::fromBundle('article-thumb.jpg') + ->setCaption('Breaking news thumbnail') + ->setAccessibilityCaption('News anchor at desk') + ->setExplicitContent(false); + + $json = $thumbnail->jsonSerialize(); + + $this->assertSame('article_thumbnail', $json['role']); + $this->assertSame('bundle://article-thumb.jpg', $json['URL']); + $this->assertSame('Breaking news thumbnail', $json['caption']); + $this->assertSame('News anchor at desk', $json['accessibilityCaption']); + $this->assertFalse($json['explicitContent']); + } + + public function testOptionalPropertiesNotIncludedWhenNull(): void + { + $thumbnail = new ArticleThumbnail('https://example.com/image.jpg'); + $json = $thumbnail->jsonSerialize(); + + $this->assertArrayNotHasKey('caption', $json); + $this->assertArrayNotHasKey('accessibilityCaption', $json); + $this->assertArrayNotHasKey('explicitContent', $json); + } + + public function testFluentInterface(): void + { + $thumbnail = new ArticleThumbnail('https://example.com/image.jpg'); + + $this->assertSame($thumbnail, $thumbnail->setCaption('test')); + $this->assertSame($thumbnail, $thumbnail->setAccessibilityCaption('test')); + $this->assertSame($thumbnail, $thumbnail->setExplicitContent(true)); + } +} diff --git a/tests/Document/Components/ArticleTitleTest.php b/tests/Document/Components/ArticleTitleTest.php new file mode 100644 index 0000000..b752f2d --- /dev/null +++ b/tests/Document/Components/ArticleTitleTest.php @@ -0,0 +1,110 @@ +jsonSerialize(); + + $this->assertSame('article_title', $json['role']); + $this->assertSame('Breaking News: Major Event', $json['text']); + } + + public function testWithTextStyle(): void + { + $title = (new ArticleTitle('Headline')) + ->setTextStyle('headlineStyle'); + $json = $title->jsonSerialize(); + + $this->assertSame('article_title', $json['role']); + $this->assertSame('Headline', $json['text']); + $this->assertSame('headlineStyle', $json['textStyle']); + } + + public function testWithFormat(): void + { + $title = (new ArticleTitle('Bold Headline')) + ->setFormat('html'); + $json = $title->jsonSerialize(); + + $this->assertSame('html', $json['format']); + } + + public function testWithMarkdownFormat(): void + { + $title = (new ArticleTitle('**Bold** and *italic*')) + ->setFormat('markdown'); + $json = $title->jsonSerialize(); + + $this->assertSame('markdown', $json['format']); + } + + public function testWithInlineTextStyles(): void + { + $title = (new ArticleTitle('Styled Title')) + ->setInlineTextStyles([ + [ + 'rangeStart' => 0, + 'rangeLength' => 6, + 'textStyle' => 'boldStyle', + ], + ]); + $json = $title->jsonSerialize(); + + $this->assertArrayHasKey('inlineTextStyles', $json); + $this->assertCount(1, $json['inlineTextStyles']); + $this->assertSame(0, $json['inlineTextStyles'][0]['rangeStart']); + } + + public function testWithAdditions(): void + { + $title = (new ArticleTitle('Click here for more')) + ->setAdditions([ + [ + 'type' => 'link', + 'rangeStart' => 0, + 'rangeLength' => 10, + 'URL' => 'https://example.com', + ], + ]); + $json = $title->jsonSerialize(); + + $this->assertArrayHasKey('additions', $json); + $this->assertSame('link', $json['additions'][0]['type']); + } + + public function testExtendsTextComponent(): void + { + $title = new ArticleTitle('Test'); + + $this->assertInstanceOf(\TomGould\AppleNews\Document\Components\TextComponent::class, $title); + } + + public function testEmptyText(): void + { + $title = new ArticleTitle(''); + $json = $title->jsonSerialize(); + + $this->assertSame('article_title', $json['role']); + $this->assertSame('', $json['text']); + } + + public function testUnicodeText(): void + { + $title = new ArticleTitle('日本語のタイトル 🎉'); + $json = $title->jsonSerialize(); + + $this->assertSame('日本語のタイトル 🎉', $json['text']); + } +}