Skip to content
Merged
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
4 changes: 4 additions & 0 deletions docs/CHEATSHEET.md
Original file line number Diff line number Diff line change
Expand Up @@ -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('<p>HTML</p>'))->setFormat('html');
Expand All @@ -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('...');
Expand Down
35 changes: 35 additions & 0 deletions docs/COOKBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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 |
Expand Down Expand Up @@ -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
Expand Down
112 changes: 112 additions & 0 deletions tests/Document/Components/ArticleThumbnailTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

declare(strict_types=1);

namespace TomGould\AppleNews\Tests\Document\Components;

use PHPUnit\Framework\TestCase;
use TomGould\AppleNews\Document\Components\ArticleThumbnail;

/**
* Tests for the ArticleThumbnail component.
*/
final class ArticleThumbnailTest extends TestCase
{
public function testBasicConstruction(): void
{
$thumbnail = new ArticleThumbnail('https://example.com/image.jpg');
$json = $thumbnail->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));
}
}
110 changes: 110 additions & 0 deletions tests/Document/Components/ArticleTitleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

namespace TomGould\AppleNews\Tests\Document\Components;

use PHPUnit\Framework\TestCase;
use TomGould\AppleNews\Document\Components\ArticleTitle;

/**
* Tests for the ArticleTitle component.
*/
final class ArticleTitleTest extends TestCase
{
public function testBasicConstruction(): void
{
$title = new ArticleTitle('Breaking News: Major Event');
$json = $title->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('<b>Bold Headline</b>'))
->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']);
}
}